diff --git a/.gitignore b/.gitignore index 3a0731ab2..fab43389a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Edit at https://www.toptal.com/developers/gitignore?templates=python *.rsa +*_rsa # AI Assistant Configuration .codex/ diff --git a/Dockerfile.app.local b/Dockerfile.app.local index 8b0b8a77f..0d0a39973 100644 --- a/Dockerfile.app.local +++ b/Dockerfile.app.local @@ -23,10 +23,11 @@ RUN npm run build FROM ${RUST_BASE_IMAGE} AS gateway-base WORKDIR /build -# 本地镜像优先缩短构建时间,保留 release 语义,但改用更快的 thin LTO。 +# 生产级 release 构建:保留 thin LTO,同时用 lld 缩短最终链接阶段。 ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \ CARGO_PROFILE_RELEASE_LTO=thin \ - CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 + CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 \ + RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=lld" RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ @@ -34,10 +35,12 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ build-essential \ ca-certificates \ + clang \ cmake \ git \ libclang-dev \ libssl-dev \ + lld \ pkg-config \ perl @@ -67,7 +70,8 @@ COPY crates/ ./crates/ RUN --mount=type=cache,id=aether-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \ --mount=type=cache,id=aether-cargo-git,target=/usr/local/cargo/git,sharing=locked \ --mount=type=cache,id=aether-cargo-target-local,target=/build/target,sharing=locked \ - cargo build --release --locked -p aether-gateway && \ + set -eux; \ + cargo build --release --locked -p aether-gateway --bin aether-gateway; \ cp target/release/aether-gateway /tmp/aether-gateway # ==================== 最小运行时打包 ==================== diff --git a/apps/aether-gateway/src/ai_serving/finalize/tests_sync.rs b/apps/aether-gateway/src/ai_serving/finalize/tests_sync.rs index 47d4a627f..c443c484b 100644 --- a/apps/aether-gateway/src/ai_serving/finalize/tests_sync.rs +++ b/apps/aether-gateway/src/ai_serving/finalize/tests_sync.rs @@ -172,6 +172,9 @@ fn aggregates_openai_responses_stream_completed_event_to_final_response() { let result = aggregate_openai_responses_stream_sync_response(body.as_bytes()) .expect("result should exist"); + let created_at = result["created_at"] + .as_i64() + .expect("created_at should be a unix timestamp"); assert_eq!( result, @@ -180,6 +183,9 @@ fn aggregates_openai_responses_stream_completed_event_to_final_response() { "object": "response", "model": "gpt-5", "status": "completed", + "created_at": created_at, + "completed_at": created_at, + "output_text": "Hello", "output": [{ "type": "message", "id": "resp_123_msg", @@ -215,6 +221,9 @@ fn aggregates_openai_responses_stream_tool_call_events_to_final_response() { let result = aggregate_openai_responses_stream_sync_response(body.as_bytes()) .expect("result should exist"); + let created_at = result["created_at"] + .as_i64() + .expect("created_at should be a unix timestamp"); assert_eq!( result, @@ -223,6 +232,9 @@ fn aggregates_openai_responses_stream_tool_call_events_to_final_response() { "object": "response", "model": "gpt-5", "status": "completed", + "created_at": created_at, + "completed_at": created_at, + "output_text": "", "output": [{ "type": "function_call", "id": "call_123", @@ -811,6 +823,9 @@ fn converts_claude_cli_response_to_openai_responses_response() { }), ) .expect("result should exist"); + let created_at = result["created_at"] + .as_i64() + .expect("created_at should be a unix timestamp"); assert_eq!( result, @@ -819,6 +834,9 @@ fn converts_claude_cli_response_to_openai_responses_response() { "object": "response", "status": "completed", "model": "claude-code-upstream", + "created_at": created_at, + "completed_at": created_at, + "output_text": "Hello Claude CLI", "output": [{ "type": "message", "id": "msg_cli_123_msg", @@ -868,6 +886,9 @@ fn converts_claude_cli_tool_use_to_openai_responses_function_call() { }), ) .expect("result should exist"); + let created_at = result["created_at"] + .as_i64() + .expect("created_at should be a unix timestamp"); assert_eq!( result, @@ -876,6 +897,9 @@ fn converts_claude_cli_tool_use_to_openai_responses_function_call() { "object": "response", "status": "completed", "model": "claude-code-upstream", + "created_at": created_at, + "completed_at": created_at, + "output_text": "Running tool.", "output": [ { "type": "message", @@ -933,6 +957,9 @@ fn converts_gemini_cli_response_to_openai_responses_response() { }), ) .expect("result should exist"); + let created_at = result["created_at"] + .as_i64() + .expect("created_at should be a unix timestamp"); assert_eq!( result, @@ -941,6 +968,9 @@ fn converts_gemini_cli_response_to_openai_responses_response() { "object": "response", "status": "completed", "model": "gemini-cli-upstream", + "created_at": created_at, + "completed_at": created_at, + "output_text": "Hello Gemini CLI", "output": [{ "type": "message", "id": "resp_cli_123_msg", @@ -995,6 +1025,9 @@ fn converts_gemini_cli_function_call_to_openai_responses_function_call() { }), ) .expect("result should exist"); + let created_at = result["created_at"] + .as_i64() + .expect("created_at should be a unix timestamp"); assert_eq!( result, @@ -1003,6 +1036,9 @@ fn converts_gemini_cli_function_call_to_openai_responses_function_call() { "object": "response", "status": "completed", "model": "gemini-cli-upstream", + "created_at": created_at, + "completed_at": created_at, + "output_text": "Need a tool.", "output": [ { "type": "message", diff --git a/apps/aether-gateway/src/ai_serving/planner/decision_input.rs b/apps/aether-gateway/src/ai_serving/planner/decision_input.rs index bf608f639..c450e1bc6 100644 --- a/apps/aether-gateway/src/ai_serving/planner/decision_input.rs +++ b/apps/aether-gateway/src/ai_serving/planner/decision_input.rs @@ -719,6 +719,8 @@ mod tests { provider_request_body: Some(json!({"model":"gpt-5","metadata":{}})), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, diff --git a/apps/aether-gateway/src/ai_serving/planner/mod.rs b/apps/aether-gateway/src/ai_serving/planner/mod.rs index af5e918d5..a58d3bd13 100644 --- a/apps/aether-gateway/src/ai_serving/planner/mod.rs +++ b/apps/aether-gateway/src/ai_serving/planner/mod.rs @@ -20,6 +20,7 @@ mod pool_scheduler; pub(crate) mod pool_scores; mod redaction; mod report_context; +mod request_gzip; mod route; mod runtime_miss; mod spec_metadata; @@ -46,6 +47,7 @@ pub(crate) use self::plan_builders::{ pub(crate) use self::pool_scores::{ build_provider_key_pool_score_upsert, provider_key_pool_score_id, provider_key_pool_score_scope, }; +pub(crate) use self::request_gzip::resolve_transport_request_gzip_policy; pub(crate) use self::route::is_matching_stream_request as planner_is_matching_stream_request; pub(crate) use self::runtime_miss::{ apply_local_runtime_candidate_terminal_reason, record_local_runtime_candidate_skip_reason, diff --git a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/payload.rs b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/payload.rs index a96ad5cae..dc8de9ee8 100644 --- a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/payload.rs +++ b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/payload.rs @@ -17,7 +17,8 @@ use crate::ai_serving::planner::report_context::{ use crate::ai_serving::planner::spec_metadata::local_same_format_provider_spec_metadata; use crate::ai_serving::planner::CandidateFailureDiagnostic; use crate::ai_serving::planner::{ - build_ai_execution_decision_response, AiExecutionDecisionResponseParts, + build_ai_execution_decision_response, resolve_transport_request_gzip_policy, + AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ resolve_transport_execution_timeouts, resolve_transport_profile, @@ -107,6 +108,11 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_ json!(crate::ai_serving::transport::GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME), ); } + if !resolved.compatibility_edits.is_empty() { + if let Ok(value) = serde_json::to_value(&resolved.compatibility_edits) { + extra_fields.insert("request_body_compatibility_edits".to_string(), value); + } + } let provider_api_format = resolved.provider_api_format.clone(); let effective_headers = input.effective_headers(&parts.headers); let report_context = append_local_failover_policy_to_value( @@ -175,8 +181,10 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_ provider_request_headers, provider_request_body, transport_profile: _, + compatibility_edits: _, request_redacted: _, } = resolved; + let request_gzip = resolve_transport_request_gzip_policy(&transport); let mut decision = build_ai_execution_decision_response(AiExecutionDecisionResponseParts { decision_is_stream: spec_metadata.require_streaming, @@ -203,6 +211,8 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_ provider_request_body: Some(provider_request_body), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip, proxy, transport_profile, timeouts: resolve_transport_execution_timeouts(&transport), diff --git a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs index cd42341cb..5f0d35522 100644 --- a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs +++ b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs @@ -19,7 +19,9 @@ use crate::ai_serving::transport::{ build_gemini_cli_v1internal_request, build_grok_browser_headers, build_grok_upstream_url, build_same_format_provider_headers, resolve_local_gemini_cli_request_auth, GeminiCliRequestAuth, GeminiCliRequestAuthSupport, GeminiCliRequestEnvelopeSupport, - GrokHeaderInput, SameFormatProviderHeadersInput, GEMINI_CLI_USER_AGENT, GROK_CHAT_PATH, + GrokHeaderInput, SameFormatProviderCompatibilityEdit, + SameFormatProviderCompatibilityEditAction, SameFormatProviderHeadersInput, + GEMINI_CLI_USER_AGENT, GROK_CHAT_PATH, }; use crate::ai_serving::{CandidateFailureDiagnostic, GatewayProviderTransportSnapshot}; use crate::{AppState, GatewayError}; @@ -107,6 +109,7 @@ pub(crate) struct LocalSameFormatProviderCandidatePayloadParts { pub(super) provider_request_headers: BTreeMap, pub(super) provider_request_body: Value, pub(super) transport_profile: Option, + pub(super) compatibility_edits: Vec, pub(super) request_redacted: bool, } @@ -153,8 +156,8 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts( let body_json = redaction.body_json.as_ref(); let mut transport = Arc::clone(&prepared.transport); - let Some(mut base_provider_request_body) = - super::super::request::build_same_format_provider_request_body( + let Some(base_provider_request) = + super::super::request::build_same_format_provider_request_body_with_compatibility_report( body_json, prepared.provider_api_format.as_str(), &prepared.mapped_model, @@ -190,6 +193,8 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts( .await; return Ok(None); }; + let mut base_provider_request_body = base_provider_request.body; + let mut compatibility_edits = base_provider_request.compatibility_edits; if let Some(mapping) = crate::system_features::reasoning_model_directive_mapping_for_api_format_and_model( state, @@ -198,10 +203,18 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts( ) .await { + let before_mapping = base_provider_request_body.clone(); crate::ai_serving::apply_model_directive_mapping_patch( &mut base_provider_request_body, &mapping, ); + if before_mapping != base_provider_request_body { + compatibility_edits.push(SameFormatProviderCompatibilityEdit { + field: "model_directive_mapping".to_string(), + action: SameFormatProviderCompatibilityEditAction::RuntimeRewrite, + detail: "applied configured model directive mapping patch".to_string(), + }); + } // Directive mapping is a deep-merge patch and may overwrite/add `stream`; // re-enforce stream-field policy afterward. // Kiro behavior classification already hard-requires upstream streaming, @@ -452,6 +465,7 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts( provider_request_headers, provider_request_body, transport_profile, + compatibility_edits, request_redacted: redaction.redacted, })) } diff --git a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request.rs b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request.rs index 329f575f0..10805d9fd 100644 --- a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request.rs +++ b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request.rs @@ -2,4 +2,5 @@ mod body; mod url; pub(super) use self::body::build_same_format_provider_request_body; +pub(super) use self::body::build_same_format_provider_request_body_with_compatibility_report; pub(super) use self::url::build_same_format_upstream_url; diff --git a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request/body.rs b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request/body.rs index 1b7963fed..a292ce453 100644 --- a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request/body.rs +++ b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/request/body.rs @@ -3,7 +3,9 @@ use serde_json::Value; use super::super::LocalSameFormatProviderSpec; use crate::ai_serving::transport::{ build_same_format_provider_request_body as build_same_format_provider_request_body_impl, + build_same_format_provider_request_body_with_compatibility_report as build_same_format_provider_request_body_with_compatibility_report_impl, SameFormatProviderFamily, SameFormatProviderRequestBodyInput, + SameFormatProviderRequestBodyOutput, }; pub(crate) fn build_same_format_provider_request_body( @@ -36,6 +38,38 @@ pub(crate) fn build_same_format_provider_request_body( }) } +pub(crate) fn build_same_format_provider_request_body_with_compatibility_report( + body_json: &Value, + provider_api_format: &str, + mapped_model: &str, + spec: LocalSameFormatProviderSpec, + body_rules: Option<&Value>, + request_headers: Option<&http::HeaderMap>, + upstream_is_stream: bool, + force_body_stream_field: bool, + kiro_auth: Option<&crate::ai_serving::transport::kiro::KiroRequestAuth>, + is_claude_code: bool, + enable_model_directives: bool, +) -> Option { + build_same_format_provider_request_body_with_compatibility_report_impl( + SameFormatProviderRequestBodyInput { + body_json, + mapped_model, + client_api_format: spec.api_format, + provider_api_format, + source_model: body_json.get("model").and_then(Value::as_str), + family: same_format_provider_family(spec.family), + body_rules, + request_headers, + upstream_is_stream, + force_body_stream_field, + kiro_auth_config: kiro_auth.map(|auth| &auth.auth_config), + is_claude_code, + enable_model_directives, + }, + ) +} + fn same_format_provider_family( family: super::super::LocalSameFormatProviderFamily, ) -> SameFormatProviderFamily { diff --git a/apps/aether-gateway/src/ai_serving/planner/redaction.rs b/apps/aether-gateway/src/ai_serving/planner/redaction.rs index 4fe2ab5af..5c40306b8 100644 --- a/apps/aether-gateway/src/ai_serving/planner/redaction.rs +++ b/apps/aether-gateway/src/ai_serving/planner/redaction.rs @@ -29,7 +29,6 @@ impl<'a> ProviderRequestRedaction<'a> { #[derive(Clone, Copy, Debug, Default)] struct ChatPiiRedactionFeatureSettings { enabled: Option, - inject_model_instruction: Option, } impl ChatPiiRedactionFeatureSettings { @@ -44,21 +43,11 @@ impl ChatPiiRedactionFeatureSettings { if let Some(enabled) = settings.get("enabled").and_then(Value::as_bool) { self.enabled = Some(enabled); } - if let Some(inject_model_instruction) = settings - .get("inject_model_instruction") - .and_then(Value::as_bool) - { - self.inject_model_instruction = Some(inject_model_instruction); - } } fn effective_enabled(self) -> bool { self.enabled.unwrap_or(false) } - - fn effective_inject_model_instruction(self) -> bool { - self.inject_model_instruction.unwrap_or(true) - } } pub(crate) fn request_identity_response_encoding_when_redacted( @@ -122,7 +111,7 @@ pub(crate) async fn resolve_provider_chat_pii_redaction<'a>( &body_bytes, format, build_redaction_session_config(hmac_key, &runtime_config, now_unix_secs), - MaskChatRequestOptions::runtime(feature_settings.effective_inject_model_instruction()), + MaskChatRequestOptions::runtime(), Some(&cache), ) .await @@ -190,3 +179,22 @@ fn redaction_mask_error_to_gateway_error(error: RedactionMaskError) -> GatewayEr }, } } + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::ChatPiiRedactionFeatureSettings; + + #[test] + fn chat_pii_redaction_feature_settings_only_control_enablement() { + let mut settings = ChatPiiRedactionFeatureSettings::default(); + settings.merge_from_value(Some(&json!({ + "chat_pii_redaction": { + "enabled": true + } + }))); + + assert!(settings.effective_enabled()); + } +} diff --git a/apps/aether-gateway/src/ai_serving/planner/request_gzip.rs b/apps/aether-gateway/src/ai_serving/planner/request_gzip.rs new file mode 100644 index 000000000..9fa314322 --- /dev/null +++ b/apps/aether-gateway/src/ai_serving/planner/request_gzip.rs @@ -0,0 +1,326 @@ +use aether_ai_serving::AiRequestGzipPolicy; +use serde_json::Value; + +use crate::ai_serving::is_openai_responses_family_format; + +use super::state::GatewayProviderTransportSnapshot; + +const DEFAULT_CODEX_REQUEST_GZIP_MIN_BYTES: usize = 64 * 1024; + +pub(crate) fn resolve_transport_request_gzip_policy( + transport: &GatewayProviderTransportSnapshot, +) -> Option { + transport_request_gzip_policy_from_config(transport.endpoint.config.as_ref()) + .or_else(|| transport_request_gzip_policy_from_config(transport.provider.config.as_ref())) + .or_else(|| default_transport_request_gzip_policy(transport)) +} + +fn default_transport_request_gzip_policy( + transport: &GatewayProviderTransportSnapshot, +) -> Option { + if !transport + .provider + .provider_type + .trim() + .eq_ignore_ascii_case("codex") + { + return None; + } + if !is_codex_request_gzip_endpoint_api_format(transport.endpoint.api_format.as_str()) { + return None; + } + + Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes: Some(DEFAULT_CODEX_REQUEST_GZIP_MIN_BYTES), + }) +} + +fn is_codex_request_gzip_endpoint_api_format(api_format: &str) -> bool { + is_openai_responses_family_format(api_format) + || api_format.trim().eq_ignore_ascii_case("openai:image") +} + +fn transport_request_gzip_policy_from_config( + config: Option<&Value>, +) -> Option { + let object = config?.as_object()?; + + for key in ["request_gzip", "request_body_gzip"] { + if let Some(policy) = object + .get(key) + .and_then(transport_request_gzip_policy_from_value) + { + return Some(policy); + } + } + + let enabled = first_config_bool( + object, + &["request_gzip_enabled", "request_body_gzip_enabled"], + ); + let min_bytes = first_config_usize( + object, + &["request_gzip_min_bytes", "request_body_gzip_min_bytes"], + ); + + match (enabled, min_bytes) { + (Some(false), _) => Some(AiRequestGzipPolicy { + enabled: Some(false), + min_bytes: None, + }), + (Some(true), min_bytes) => Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes, + }), + (None, Some(min_bytes)) => Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes: Some(min_bytes), + }), + (None, None) => None, + } +} + +fn transport_request_gzip_policy_from_value(value: &Value) -> Option { + if let Some(enabled) = value.as_bool() { + return Some(AiRequestGzipPolicy { + enabled: Some(enabled), + min_bytes: None, + }); + } + + let object = value.as_object()?; + let enabled = first_config_bool(object, &["enabled"]); + let min_bytes = first_config_usize(object, &["min_bytes"]); + + match (enabled, min_bytes) { + (Some(false), _) => Some(AiRequestGzipPolicy { + enabled: Some(false), + min_bytes: None, + }), + (Some(true), min_bytes) => Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes, + }), + (None, Some(min_bytes)) => Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes: Some(min_bytes), + }), + (None, None) => None, + } +} + +fn first_config_bool(object: &serde_json::Map, keys: &[&str]) -> Option { + keys.iter() + .find_map(|key| object.get(*key).and_then(config_bool)) +} + +fn config_bool(value: &Value) -> Option { + value.as_bool().or_else(|| { + value.as_str().and_then(|text| { + let normalized = text.trim(); + if normalized.eq_ignore_ascii_case("true") { + Some(true) + } else if normalized.eq_ignore_ascii_case("false") { + Some(false) + } else { + None + } + }) + }) +} + +fn first_config_usize(object: &serde_json::Map, keys: &[&str]) -> Option { + keys.iter() + .find_map(|key| object.get(*key).and_then(config_usize)) +} + +fn config_usize(value: &Value) -> Option { + value + .as_u64() + .and_then(|number| usize::try_from(number).ok()) + .or_else(|| { + value + .as_str() + .and_then(|text| text.trim().parse::().ok()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use aether_provider_transport::snapshot::{ + GatewayProviderTransportEndpoint, GatewayProviderTransportKey, + GatewayProviderTransportProvider, GatewayProviderTransportSnapshot, + }; + use serde_json::{json, Value}; + + fn sample_transport( + provider_type: &str, + endpoint_api_format: &str, + provider_config: Option, + endpoint_config: Option, + ) -> GatewayProviderTransportSnapshot { + GatewayProviderTransportSnapshot { + provider: GatewayProviderTransportProvider { + id: "provider-1".to_string(), + name: "Provider".to_string(), + provider_type: provider_type.to_string(), + website: None, + is_active: true, + keep_priority_on_conversion: false, + enable_format_conversion: true, + concurrent_limit: None, + max_retries: None, + proxy: None, + request_timeout_secs: None, + stream_first_byte_timeout_secs: None, + config: provider_config, + }, + endpoint: GatewayProviderTransportEndpoint { + id: "endpoint-1".to_string(), + provider_id: "provider-1".to_string(), + api_format: endpoint_api_format.to_string(), + api_family: None, + endpoint_kind: None, + is_active: true, + base_url: "https://api.example.test".to_string(), + header_rules: None, + body_rules: None, + max_retries: None, + custom_path: None, + config: endpoint_config, + format_acceptance_config: None, + proxy: None, + }, + key: GatewayProviderTransportKey { + id: "key-1".to_string(), + provider_id: "provider-1".to_string(), + name: "key".to_string(), + auth_type: "api_key".to_string(), + is_active: true, + api_formats: None, + auth_type_by_format: None, + allow_auth_channel_mismatch_formats: None, + allowed_models: None, + capabilities: None, + rate_multipliers: None, + global_priority_by_format: None, + expires_at_unix_secs: None, + proxy: None, + fingerprint: None, + upstream_metadata: None, + decrypted_api_key: "secret".to_string(), + decrypted_auth_config: None, + }, + } + } + + #[test] + fn endpoint_request_gzip_policy_overrides_provider_policy() { + let transport = sample_transport( + "openai", + "openai:responses", + Some(json!({"request_gzip": false})), + Some(json!({"request_gzip": {"enabled": true, "min_bytes": 1024}})), + ); + + assert_eq!( + resolve_transport_request_gzip_policy(&transport), + Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes: Some(1024), + }) + ); + } + + #[test] + fn endpoint_request_gzip_false_disables_provider_and_codex_defaults() { + let transport = sample_transport( + "codex", + "openai:responses", + Some(json!({"request_gzip": {"enabled": true, "min_bytes": 1024}})), + Some(json!({"request_gzip": false})), + ); + + assert_eq!( + resolve_transport_request_gzip_policy(&transport), + Some(AiRequestGzipPolicy { + enabled: Some(false), + min_bytes: None, + }) + ); + } + + #[test] + fn request_gzip_policy_supports_top_level_aliases() { + let transport = sample_transport( + "openai", + "openai:responses", + None, + Some(json!({ + "request_body_gzip_enabled": true, + "request_body_gzip_min_bytes": "4096" + })), + ); + + assert_eq!( + resolve_transport_request_gzip_policy(&transport), + Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes: Some(4096), + }) + ); + } + + #[test] + fn request_gzip_policy_treats_min_bytes_only_as_enabled() { + let transport = sample_transport( + "openai", + "openai:responses", + None, + Some(json!({"request_gzip_min_bytes": 1})), + ); + + assert_eq!( + resolve_transport_request_gzip_policy(&transport), + Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes: Some(1), + }) + ); + } + + #[test] + fn codex_responses_endpoint_gets_default_request_gzip_policy() { + let transport = sample_transport("codex", "openai:responses", None, None); + + assert_eq!( + resolve_transport_request_gzip_policy(&transport), + Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes: Some(DEFAULT_CODEX_REQUEST_GZIP_MIN_BYTES), + }) + ); + } + + #[test] + fn codex_image_endpoint_gets_default_request_gzip_policy() { + let transport = sample_transport("codex", "openai:image", None, None); + + assert_eq!( + resolve_transport_request_gzip_policy(&transport), + Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes: Some(DEFAULT_CODEX_REQUEST_GZIP_MIN_BYTES), + }) + ); + } + + #[test] + fn non_codex_endpoint_does_not_get_default_request_gzip_policy() { + let transport = sample_transport("openai", "openai:responses", None, None); + + assert_eq!(resolve_transport_request_gzip_policy(&transport), None); + } +} diff --git a/apps/aether-gateway/src/ai_serving/planner/specialized/files/decision.rs b/apps/aether-gateway/src/ai_serving/planner/specialized/files/decision.rs index 0cec190bc..67416febc 100644 --- a/apps/aether-gateway/src/ai_serving/planner/specialized/files/decision.rs +++ b/apps/aether-gateway/src/ai_serving/planner/specialized/files/decision.rs @@ -7,7 +7,8 @@ use crate::ai_serving::planner::report_context::{ }; use crate::ai_serving::planner::spec_metadata::local_gemini_files_spec_metadata; use crate::ai_serving::planner::{ - build_ai_execution_decision_response, AiExecutionDecisionResponseParts, + build_ai_execution_decision_response, resolve_transport_request_gzip_policy, + AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ resolve_transport_execution_timeouts, resolve_transport_profile, @@ -123,6 +124,7 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat upstream_url, file_name: _, } = resolved; + let request_gzip = resolve_transport_request_gzip_policy(&transport); let mut decision = build_ai_execution_decision_response(AiExecutionDecisionResponseParts { decision_is_stream: spec_metadata.require_streaming, @@ -154,6 +156,8 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned), + content_encoding: None, + request_gzip, proxy, transport_profile, timeouts: resolve_transport_execution_timeouts(&transport), diff --git a/apps/aether-gateway/src/ai_serving/planner/specialized/image/decision.rs b/apps/aether-gateway/src/ai_serving/planner/specialized/image/decision.rs index fad0f1d03..e65bcb816 100644 --- a/apps/aether-gateway/src/ai_serving/planner/specialized/image/decision.rs +++ b/apps/aether-gateway/src/ai_serving/planner/specialized/image/decision.rs @@ -5,7 +5,8 @@ use crate::ai_serving::planner::report_context::{ }; use crate::ai_serving::planner::spec_metadata::local_openai_image_spec_metadata; use crate::ai_serving::planner::{ - build_ai_execution_decision_response, AiExecutionDecisionResponseParts, + build_ai_execution_decision_response, resolve_transport_request_gzip_policy, + AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ resolve_transport_execution_timeouts, resolve_transport_profile, @@ -82,15 +83,6 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat "chatgpt_web_image".to_string(), serde_json::Value::Bool(true), ); - extra_fields.insert( - "local_failover_policy".to_string(), - serde_json::json!({ - "stop_status_codes": [400, 401, 403, 429, 500, 502, 503, 504], - "error_stop_patterns": [ - { "pattern": ".*" } - ] - }), - ); } let upstream_is_stream = resolved .provider_request_body @@ -143,6 +135,7 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat spec_metadata.api_format, provider_api_format.as_str(), ); + let request_gzip = resolve_transport_request_gzip_policy(&transport); let mut decision = build_ai_execution_decision_response(AiExecutionDecisionResponseParts { decision_is_stream: spec_metadata.require_streaming, @@ -169,6 +162,8 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat provider_request_body: Some(resolved.provider_request_body), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip, proxy, transport_profile, timeouts: resolve_transport_execution_timeouts(&transport), diff --git a/apps/aether-gateway/src/ai_serving/planner/specialized/video/decision.rs b/apps/aether-gateway/src/ai_serving/planner/specialized/video/decision.rs index 806749926..8d38e6ad8 100644 --- a/apps/aether-gateway/src/ai_serving/planner/specialized/video/decision.rs +++ b/apps/aether-gateway/src/ai_serving/planner/specialized/video/decision.rs @@ -5,7 +5,8 @@ use crate::ai_serving::planner::report_context::{ }; use crate::ai_serving::planner::spec_metadata::local_video_create_spec_metadata; use crate::ai_serving::planner::{ - build_ai_execution_decision_response, AiExecutionDecisionResponseParts, + build_ai_execution_decision_response, resolve_transport_request_gzip_policy, + AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ resolve_transport_execution_timeouts, resolve_transport_profile, @@ -103,6 +104,7 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat provider_request_body, upstream_url, } = resolved; + let request_gzip = resolve_transport_request_gzip_policy(&transport); let mut decision = build_ai_execution_decision_response(AiExecutionDecisionResponseParts { decision_is_stream: false, @@ -135,6 +137,8 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned), + content_encoding: None, + request_gzip, proxy, transport_profile, timeouts: resolve_transport_execution_timeouts(&transport), diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/codex/tests.rs b/apps/aether-gateway/src/ai_serving/planner/standard/codex/tests.rs index 2a986a44a..b002ea28e 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/codex/tests.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/codex/tests.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use super::{ apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers, }; +use crate::ai_serving::planner::standard::build_local_openai_responses_request_body; use http::{HeaderMap, HeaderValue}; use serde_json::json; @@ -36,6 +37,40 @@ fn applies_codex_defaults_when_body_rules_do_not_handle_fields() { assert!(body.get("reasoning").is_none()); } +#[test] +fn local_openai_responses_codex_body_wraps_string_input_for_backend() { + let body = json!({ + "model": "gpt-5", + "input": "hello" + }); + + let provider_request_body = build_local_openai_responses_request_body( + &body, + "gpt-5-upstream", + false, + false, + "codex", + "openai:responses", + None, + Some("key-123"), + &HeaderMap::new(), + false, + ) + .expect("codex local openai responses body should build"); + + assert_eq!( + provider_request_body["input"], + json!([{ + "type": "message", + "role": "user", + "content": [{ + "type": "input_text", + "text": "hello" + }] + }]) + ); +} + #[test] fn strips_store_for_compact_even_when_body_rules_handle_it() { let body_rules = json!([ diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/family/payload.rs b/apps/aether-gateway/src/ai_serving/planner/standard/family/payload.rs index f42ceddd7..033f53130 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/family/payload.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/family/payload.rs @@ -15,7 +15,8 @@ use crate::ai_serving::planner::report_context::{ use crate::ai_serving::planner::spec_metadata::local_standard_spec_metadata; use crate::ai_serving::planner::CandidateFailureDiagnostic; use crate::ai_serving::planner::{ - build_ai_execution_decision_response, AiExecutionDecisionResponseParts, + build_ai_execution_decision_response, resolve_transport_request_gzip_policy, + AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ resolve_transport_execution_timeouts, resolve_transport_profile, @@ -175,6 +176,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate( transport_profile: _, request_redacted: _, } = resolved; + let request_gzip = resolve_transport_request_gzip_policy(&transport); let mut decision = build_ai_execution_decision_response(AiExecutionDecisionResponseParts { decision_is_stream: spec_metadata.require_streaming, @@ -201,6 +203,8 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate( provider_request_body: Some(provider_request_body), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip, proxy, transport_profile, timeouts, diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/family/request.rs b/apps/aether-gateway/src/ai_serving/planner/standard/family/request.rs index a81ed5969..746ec622c 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/family/request.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/family/request.rs @@ -23,6 +23,7 @@ use crate::ai_serving::planner::spec_metadata::local_standard_spec_metadata; use crate::ai_serving::planner::standard::{ apply_codex_openai_responses_special_headers, apply_deepseek_tool_call_thinking_compat, is_deepseek_provider, request_body_build_failure_extra_data, + request_conversion_failure_extra_data, }; use crate::ai_serving::transport::kiro::{ build_kiro_provider_headers, build_kiro_provider_request_body, @@ -599,10 +600,14 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts( attempt.candidate_index, &attempt.candidate_id, "provider_request_body_build_failed", - request_body_build_failure_extra_data( + request_conversion_failure_extra_data( body_json, spec_metadata.api_format, provider_api_format, + Some(prepared_candidate.mapped_model.as_str()), + Some(parts.uri.path()), + upstream_is_stream, + "standard_family_request_conversion", ), ) .await; diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/mod.rs b/apps/aether-gateway/src/ai_serving/planner/standard/mod.rs index 630185ad8..45796eee2 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/mod.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/mod.rs @@ -62,7 +62,8 @@ pub(crate) use crate::ai_serving::{ normalize_openai_responses_request_to_openai_chat_request, parse_openai_tool_result_content, }; pub(crate) use aether_ai_serving::{ - request_body_build_failure_extra_data, same_format_provider_request_body_failure_extra_data, + request_body_build_failure_extra_data, request_conversion_failure_extra_data, + same_format_provider_request_body_failure_extra_data, }; pub(crate) fn build_standard_upstream_url( diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/payload.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/payload.rs index 2421cd045..4efda97be 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/payload.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/payload.rs @@ -6,7 +6,8 @@ use crate::ai_serving::planner::report_context::{ insert_provider_stream_event_api_format, LocalExecutionReportContextParts, }; use crate::ai_serving::planner::{ - build_ai_execution_decision_response, AiExecutionDecisionResponseParts, + build_ai_execution_decision_response, resolve_transport_request_gzip_policy, + AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ resolve_transport_execution_timeouts, resolve_transport_profile, @@ -104,15 +105,6 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate .eq_ignore_ascii_case("chatgpt_web") { extra_fields.insert("chatgpt_web_image".to_string(), serde_json::json!(true)); - extra_fields.insert( - "local_failover_policy".to_string(), - serde_json::json!({ - "stop_status_codes": [400, 401, 403, 429, 500, 502, 503, 504], - "error_stop_patterns": [ - { "pattern": ".*" } - ] - }), - ); } let super::request::LocalOpenAiChatCandidatePayloadParts { client_api_format, @@ -192,6 +184,7 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate ), &transport, ); + let request_gzip = resolve_transport_request_gzip_policy(&transport); let mut decision = build_ai_execution_decision_response(AiExecutionDecisionResponseParts { decision_is_stream, @@ -218,6 +211,8 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate provider_request_body: Some(provider_request_body), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip, proxy, transport_profile, timeouts, diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/request.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/request.rs index c89842b20..b89612c5f 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/request.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/request.rs @@ -25,6 +25,7 @@ use crate::ai_serving::planner::standard::{ apply_deepseek_tool_call_thinking_compat, build_cross_format_openai_chat_request_body, build_cross_format_openai_chat_upstream_url, build_local_openai_chat_request_body, build_local_openai_chat_upstream_url, request_body_build_failure_extra_data, + request_conversion_failure_extra_data, }; use crate::ai_serving::transport::auth::resolve_local_openai_bearer_auth; use crate::ai_serving::transport::kiro::{ @@ -601,10 +602,14 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts( candidate_index, candidate_id, "provider_request_body_build_failed", - request_body_build_failure_extra_data( + request_conversion_failure_extra_data( body_json, "openai:chat", provider_api_format.as_str(), + Some(prepared_candidate.mapped_model.as_str()), + Some(parts.uri.path()), + upstream_is_stream, + "openai_chat_request_conversion", ), ) .await; diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/stream.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/stream.rs index f707af36e..b60a245bb 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/stream.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/stream.rs @@ -326,6 +326,8 @@ mod tests { })), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, @@ -422,6 +424,8 @@ mod tests { provider_request_body: Some(json!({"model":"gpt-5.4","messages":[],"stream":true})), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, @@ -488,6 +492,8 @@ mod tests { provider_request_body, provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, @@ -595,6 +601,8 @@ mod tests { ), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/sync.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/sync.rs index 6ef2d62fb..ae44d683f 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/sync.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/sync.rs @@ -292,6 +292,8 @@ mod tests { })), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, @@ -387,6 +389,8 @@ mod tests { provider_request_body: Some(json!({"model":"gpt-5.4","messages":[],"stream":false})), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, @@ -458,6 +462,8 @@ mod tests { ), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/payload.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/payload.rs index fc8f98bbc..75785d27b 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/payload.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/payload.rs @@ -9,7 +9,8 @@ use crate::ai_serving::planner::report_context::{ }; use crate::ai_serving::planner::spec_metadata::local_openai_responses_spec_metadata; use crate::ai_serving::planner::{ - build_ai_execution_decision_response, AiExecutionDecisionResponseParts, + build_ai_execution_decision_response, resolve_transport_request_gzip_policy, + AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ resolve_transport_execution_timeouts, resolve_transport_profile, @@ -101,15 +102,6 @@ pub(crate) async fn maybe_build_local_openai_responses_decision_payload_for_cand .eq_ignore_ascii_case("chatgpt_web") { extra_fields.insert("chatgpt_web_image".to_string(), json!(true)); - extra_fields.insert( - "local_failover_policy".to_string(), - json!({ - "stop_status_codes": [400, 401, 403, 429, 500, 502, 503, 504], - "error_stop_patterns": [ - { "pattern": ".*" } - ] - }), - ); } insert_provider_stream_event_api_format( &mut extra_fields, @@ -212,6 +204,7 @@ pub(crate) async fn maybe_build_local_openai_responses_decision_payload_for_cand image_request_summary: _, request_redacted: _, } = resolved; + let request_gzip = resolve_transport_request_gzip_policy(&transport); let mut decision = build_ai_execution_decision_response(AiExecutionDecisionResponseParts { decision_is_stream: spec_metadata.require_streaming, @@ -238,6 +231,8 @@ pub(crate) async fn maybe_build_local_openai_responses_decision_payload_for_cand provider_request_body: Some(provider_request_body), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip, proxy, transport_profile, timeouts, diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/request.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/request.rs index 422ab7738..48c965594 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/request.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/request.rs @@ -27,6 +27,7 @@ use crate::ai_serving::planner::standard::{ apply_deepseek_tool_call_thinking_compat, build_cross_format_openai_responses_request_body, build_cross_format_openai_responses_upstream_url, build_local_openai_responses_request_body, build_local_openai_responses_upstream_url, request_body_build_failure_extra_data, + request_conversion_failure_extra_data, }; use crate::ai_serving::transport::antigravity::{ build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers, @@ -371,10 +372,14 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts( candidate_index, candidate_id, "provider_request_body_build_failed", - request_body_build_failure_extra_data( + request_conversion_failure_extra_data( body_json, spec_metadata.api_format, provider_api_format, + Some(mapped_model.as_str()), + Some(parts.uri.path()), + upstream_is_stream, + "openai_responses_request_conversion", ), ) .await; diff --git a/apps/aether-gateway/src/ai_serving/transport.rs b/apps/aether-gateway/src/ai_serving/transport.rs index 4b902dd79..d74f0ae33 100644 --- a/apps/aether-gateway/src/ai_serving/transport.rs +++ b/apps/aether-gateway/src/ai_serving/transport.rs @@ -72,8 +72,10 @@ pub(crate) use aether_provider_transport::{ build_local_openai_chat_upstream_url, build_local_openai_responses_upstream_url, build_openai_image_headers, build_openai_image_upstream_url, build_passthrough_headers, build_request_trace_proxy_value, build_same_format_provider_headers, - build_same_format_provider_request_body, build_same_format_provider_upstream_url, - build_standard_plan_fallback_headers, build_standard_plan_fallback_openai_chat_url, + build_same_format_provider_request_body, + build_same_format_provider_request_body_with_compatibility_report, + build_same_format_provider_upstream_url, build_standard_plan_fallback_headers, + build_standard_plan_fallback_openai_chat_url, build_standard_plan_fallback_openai_responses_url, build_standard_provider_request_headers, build_transport_request_url, build_transport_request_url_for_request_body, build_video_create_headers, build_video_create_request_body, build_video_create_upstream_url, @@ -106,12 +108,14 @@ pub(crate) use aether_provider_transport::{ GeminiCliRequestAuthUnsupportedReason, GeminiCliRequestEnvelopeSupport, GeminiFilesHeadersInput, GeminiFilesRequestBodyError, GeminiFilesRequestBodyParts, GrokHeaderInput, LocalResolvedOAuthRequestAuth, ProviderOpenAiImageHeadersInput, - ProviderVideoCreateFamily, ProviderVideoCreateHeadersInput, SameFormatProviderFamily, - SameFormatProviderHeadersInput, SameFormatProviderRequestBehavior, + ProviderVideoCreateFamily, ProviderVideoCreateHeadersInput, + SameFormatProviderCompatibilityEdit, SameFormatProviderCompatibilityEditAction, + SameFormatProviderFamily, SameFormatProviderHeadersInput, SameFormatProviderRequestBehavior, SameFormatProviderRequestBehaviorParams, SameFormatProviderRequestBodyInput, - SameFormatProviderUpstreamUrlParams, StandardPlanFallbackAcceptPolicy, - StandardPlanFallbackHeadersInput, StandardProviderRequestHeaders, - StandardProviderRequestHeadersInput, TransportRequestBodySemanticsError, - TransportRequestUrlParams, GEMINI_CLI_USER_AGENT, GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, - GROK_CHAT_PATH, GROK_INTERNAL_HEADER, GROK_RATE_LIMITS_PATH, WINDSURF_ENVELOPE_NAME, + SameFormatProviderRequestBodyOutput, SameFormatProviderUpstreamUrlParams, + StandardPlanFallbackAcceptPolicy, StandardPlanFallbackHeadersInput, + StandardProviderRequestHeaders, StandardProviderRequestHeadersInput, + TransportRequestBodySemanticsError, TransportRequestUrlParams, GEMINI_CLI_USER_AGENT, + GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, GROK_CHAT_PATH, GROK_INTERNAL_HEADER, + GROK_RATE_LIMITS_PATH, WINDSURF_ENVELOPE_NAME, }; diff --git a/apps/aether-gateway/src/data/candidates.rs b/apps/aether-gateway/src/data/candidates.rs index 1b38f7f1b..da2d2f3f0 100644 --- a/apps/aether-gateway/src/data/candidates.rs +++ b/apps/aether-gateway/src/data/candidates.rs @@ -11,9 +11,15 @@ pub(crate) async fn read_request_candidate_trace( request_id: &str, attempted_only: bool, ) -> Result, DataLayerError> { - let all_candidates = state - .list_request_candidates_by_request_id(request_id) - .await?; + let all_candidates = if attempted_only { + state + .list_attempted_request_candidates_by_request_id(request_id) + .await? + } else { + state + .list_request_candidates_by_request_id(request_id) + .await? + }; Ok(RequestCandidateTrace::from_candidates( request_id, all_candidates, diff --git a/apps/aether-gateway/src/data/state/catalog.rs b/apps/aether-gateway/src/data/state/catalog.rs index 6a52aa8ab..5661c54c1 100644 --- a/apps/aether-gateway/src/data/state/catalog.rs +++ b/apps/aether-gateway/src/data/state/catalog.rs @@ -19,6 +19,16 @@ impl GatewayDataState { } } + pub(crate) async fn list_attempted_request_candidates_by_request_id( + &self, + request_id: &str, + ) -> Result, DataLayerError> { + match &self.request_candidate_reader { + Some(repository) => repository.list_attempted_by_request_id(request_id).await, + None => Ok(Vec::new()), + } + } + pub(crate) async fn list_request_candidates_by_provider_id( &self, provider_id: &str, @@ -416,18 +426,24 @@ impl GatewayDataState { pub(crate) async fn cleanup_deleted_provider_catalog_refs( &self, provider_id: &str, + provider_deleted: bool, endpoint_ids: &[String], key_ids: &[String], ) -> Result<(), DataLayerError> { let cleaned = match &self.provider_catalog_writer { Some(repository) => { repository - .cleanup_deleted_provider_refs(provider_id, endpoint_ids, key_ids) + .cleanup_deleted_provider_refs( + provider_id, + provider_deleted, + endpoint_ids, + key_ids, + ) .await } None => Ok(()), }; - if !endpoint_ids.is_empty() || !key_ids.is_empty() { + if provider_deleted || !endpoint_ids.is_empty() || !key_ids.is_empty() { self.clear_provider_catalog_cache(); } cleaned diff --git a/apps/aether-gateway/src/data/state/runtime.rs b/apps/aether-gateway/src/data/state/runtime.rs index cbbce5584..88a5426bb 100644 --- a/apps/aether-gateway/src/data/state/runtime.rs +++ b/apps/aether-gateway/src/data/state/runtime.rs @@ -1124,6 +1124,16 @@ impl GatewayDataState { } } + pub(crate) async fn find_request_usage_by_request_id_shallow( + &self, + request_id: &str, + ) -> Result, DataLayerError> { + match &self.usage_reader { + Some(repository) => repository.find_by_request_id_shallow(request_id).await, + None => Ok(None), + } + } + pub(crate) async fn find_request_usage_by_id( &self, usage_id: &str, @@ -1958,6 +1968,14 @@ impl GatewayDataState { self.find_request_usage_by_request_id(request_id).await } + pub(crate) async fn read_request_usage_audit_shallow( + &self, + request_id: &str, + ) -> Result, DataLayerError> { + self.find_request_usage_by_request_id_shallow(request_id) + .await + } + pub(crate) async fn read_request_audit_bundle( &self, request_id: &str, diff --git a/apps/aether-gateway/src/dispatch/pool_scheduler.rs b/apps/aether-gateway/src/dispatch/pool_scheduler.rs index 1af61efe8..f8268d950 100644 --- a/apps/aether-gateway/src/dispatch/pool_scheduler.rs +++ b/apps/aether-gateway/src/dispatch/pool_scheduler.rs @@ -4,7 +4,9 @@ use std::sync::{ Arc, LazyLock, }; -use aether_admin::provider::pool as admin_provider_pool_pure; +use aether_admin::provider::{ + pool as admin_provider_pool_pure, status as admin_provider_status_pure, +}; use aether_data_contracts::repository::candidate_selection::{ StoredMinimalCandidateSelectionRow, StoredPoolKeyCandidateOrder, StoredPoolKeyCandidateRowsByKeyIdsQuery, StoredPoolKeyCandidateRowsQuery, @@ -1217,9 +1219,15 @@ fn pool_key_requires_reauth_for_scheduling( .map(str::trim) .unwrap_or_default(); if !invalid_reason.is_empty() { - if pool_oauth_reason_has_tag(invalid_reason, "[OAUTH_EXPIRED]") - || pool_oauth_reason_has_tag(invalid_reason, "[ACCOUNT_BLOCK]") - { + let account_state = admin_provider_status_pure::resolve_pool_account_state( + None, + key.upstream_metadata.as_ref(), + Some(invalid_reason), + ); + if account_state.blocked && !account_state.recoverable { + return true; + } + if pool_oauth_reason_has_tag(invalid_reason, "[ACCOUNT_BLOCK]") { return true; } if pool_oauth_reason_has_tag(invalid_reason, "[REQUEST_FAILED]") { @@ -1230,6 +1238,9 @@ fn pool_key_requires_reauth_for_scheduling( .expires_at_unix_secs .is_none_or(|expires_at| expires_at == 0 || expires_at <= now_unix_secs); } + if pool_oauth_reason_has_tag(invalid_reason, "[OAUTH_EXPIRED]") { + return false; + } return true; } @@ -3265,8 +3276,7 @@ mod tests { let mut key_a_invalid = sample_codex_pool_key("provider-a", "key-a-invalid"); key_a_invalid.oauth_invalid_at_unix_secs = Some(1_710_000_000); - key_a_invalid.oauth_invalid_reason = - Some("[OAUTH_EXPIRED] Codex Token 无效或已过期 (401)".to_string()); + key_a_invalid.oauth_invalid_reason = Some("[OAUTH_EXPIRED] token invalidated".to_string()); let exhausted_status_snapshot = json!({ "quota": { "provider_type": "codex", @@ -3370,8 +3380,7 @@ mod tests { let mut key_a_invalid = sample_codex_pool_key("provider-a", "key-a-invalid"); key_a_invalid.oauth_invalid_at_unix_secs = Some(1_710_000_000); - key_a_invalid.oauth_invalid_reason = - Some("[OAUTH_EXPIRED] Codex Token 无效或已过期 (401)".to_string()); + key_a_invalid.oauth_invalid_reason = Some("[OAUTH_EXPIRED] token invalidated".to_string()); key_a_invalid.status_snapshot = Some(json!({ "quota": { "provider_type": "codex", @@ -3463,6 +3472,9 @@ mod tests { key.oauth_invalid_reason = Some("[REQUEST_FAILED] 账号状态检查失败".to_string()); key.oauth_invalid_at_unix_secs = Some(100); assert!(!pool_key_requires_reauth_for_scheduling(&key, 300)); + + key.oauth_invalid_reason = Some("[OAUTH_EXPIRED] session expired".to_string()); + assert!(!pool_key_requires_reauth_for_scheduling(&key, 300)); } #[test] @@ -3471,6 +3483,10 @@ mod tests { key.oauth_invalid_reason = Some("[ACCOUNT_BLOCK] account has been deactivated".to_string()); assert!(pool_key_requires_reauth_for_scheduling(&key, 100)); + key.oauth_invalid_reason = Some("[OAUTH_EXPIRED] token invalidated".to_string()); + key.oauth_invalid_at_unix_secs = None; + assert!(pool_key_requires_reauth_for_scheduling(&key, 100)); + key.oauth_invalid_reason = Some("Kiro Token 无效或已过期".to_string()); key.oauth_invalid_at_unix_secs = None; assert!(pool_key_requires_reauth_for_scheduling(&key, 100)); diff --git a/apps/aether-gateway/src/execution_runtime/fallback.rs b/apps/aether-gateway/src/execution_runtime/fallback.rs index 62e79d69f..a1f1d925c 100644 --- a/apps/aether-gateway/src/execution_runtime/fallback.rs +++ b/apps/aether-gateway/src/execution_runtime/fallback.rs @@ -1007,6 +1007,100 @@ mod tests { ); } + #[tokio::test] + async fn provider_failover_rules_can_stop_rate_limit_status() { + let result = ExecutionResult { + request_id: "req-1".to_string(), + candidate_id: None, + status_code: 429, + headers: Default::default(), + body: None, + telemetry: None, + error: None, + }; + let local_report_context = serde_json::json!({ + "candidate_index": 0, + "retry_index": 0, + }); + let state = build_state_with_provider_config(Some(serde_json::json!({ + "failover_rules": { + "stop_on_status_codes": [429] + } + }))); + let plan = sample_plan(); + + assert!( + should_stop_local_candidate_failover_sync( + &state, + &plan, + "openai_chat_sync", + Some(&local_report_context), + &result, + Some("{\"error\":{\"message\":\"rate limited\"}}"), + ) + .await + ); + assert!( + !should_retry_next_local_candidate_sync( + &state, + &plan, + "openai_chat_sync", + Some(&local_report_context), + &result, + Some("{\"error\":{\"message\":\"rate limited\"}}"), + ) + .await + ); + } + + #[tokio::test] + async fn status_only_error_stop_rule_can_stop_rate_limit_status() { + let result = ExecutionResult { + request_id: "req-1".to_string(), + candidate_id: None, + status_code: 429, + headers: Default::default(), + body: None, + telemetry: None, + error: None, + }; + let local_report_context = serde_json::json!({ + "candidate_index": 0, + "retry_index": 0, + }); + let state = build_state_with_provider_config(Some(serde_json::json!({ + "failover_rules": { + "error_stop_patterns": [ + {"status_codes": [429]} + ] + } + }))); + let plan = sample_plan(); + + assert!( + should_stop_local_candidate_failover_sync( + &state, + &plan, + "openai_chat_sync", + Some(&local_report_context), + &result, + None, + ) + .await + ); + assert!( + !should_retry_next_local_candidate_sync( + &state, + &plan, + "openai_chat_sync", + Some(&local_report_context), + &result, + None, + ) + .await + ); + } + #[test] fn resolve_local_failover_policy_reads_regex_rules() { let state = build_state_with_provider_config(Some(serde_json::json!({ @@ -1039,6 +1133,32 @@ mod tests { ); } + #[test] + fn resolve_local_failover_policy_reads_status_only_error_stop_rules() { + let state = build_state_with_provider_config(Some(serde_json::json!({ + "failover_rules": { + "success_failover_patterns": [ + {"status_codes": [200]} + ], + "error_stop_patterns": [ + {"status_codes": [429]} + ] + } + }))); + let plan = sample_plan(); + let runtime = tokio::runtime::Runtime::new().expect("runtime should build"); + + let policy = runtime.block_on(resolve_local_failover_policy(&state, &plan, None)); + assert!(policy.success_failover_patterns.is_empty()); + assert_eq!( + policy.error_stop_patterns, + vec![LocalFailoverRegexRule { + pattern: String::new(), + status_codes: [429].into_iter().collect(), + }] + ); + } + #[tokio::test] async fn success_failover_pattern_can_retry_sync_candidate() { let result = ExecutionResult { @@ -1125,11 +1245,11 @@ mod tests { } #[tokio::test] - async fn chatgpt_web_report_context_stops_local_sync_failover_on_transport_errors() { + async fn report_context_failover_policy_does_not_override_provider_config() { let result = ExecutionResult { request_id: "req-1".to_string(), candidate_id: None, - status_code: 503, + status_code: 429, headers: Default::default(), body: None, telemetry: None, @@ -1150,7 +1270,7 @@ mod tests { let plan = sample_plan(); assert!( - should_stop_local_candidate_failover_sync( + should_retry_next_local_candidate_sync( &state, &plan, "openai_image_sync", @@ -1161,7 +1281,7 @@ mod tests { .await ); assert!( - !should_retry_next_local_candidate_sync( + !should_stop_local_candidate_failover_sync( &state, &plan, "openai_image_sync", diff --git a/apps/aether-gateway/src/execution_runtime/stream/execution.rs b/apps/aether-gateway/src/execution_runtime/stream/execution.rs index e6bfc9dfa..6e37f851f 100644 --- a/apps/aether-gateway/src/execution_runtime/stream/execution.rs +++ b/apps/aether-gateway/src/execution_runtime/stream/execution.rs @@ -3921,10 +3921,14 @@ mod tests { StreamFrame, StreamFramePayload, StreamFrameType, }; use aether_data::repository::candidates::InMemoryRequestCandidateRepository; + use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository; use aether_data::repository::usage::InMemoryUsageReadRepository; use aether_data_contracts::repository::candidates::{ RequestCandidateReadRepository, RequestCandidateStatus, }; + use aether_data_contracts::repository::provider_catalog::{ + StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider, + }; use aether_data_contracts::repository::usage::UsageReadRepository; use aether_usage_runtime::UsageRuntimeConfig; use async_stream::stream; @@ -3954,6 +3958,77 @@ mod tests { use crate::tunnel::{tunnel_protocol, TunnelProxyConn}; use crate::AppState; + fn provider_catalog_stop_429_for_plan( + plan: &ExecutionPlan, + ) -> InMemoryProviderCatalogReadRepository { + let provider_type = plan.provider_name.as_deref().unwrap_or("custom"); + let provider = StoredProviderCatalogProvider::new( + plan.provider_id.clone(), + plan.provider_id.clone(), + Some("https://provider.example".to_string()), + provider_type.to_string(), + ) + .expect("provider should build") + .with_transport_fields( + true, + false, + false, + None, + Some(3), + None, + None, + None, + Some(json!({ + "failover_rules": { + "stop_status_codes": [429] + } + })), + ); + let endpoint = StoredProviderCatalogEndpoint::new( + plan.endpoint_id.clone(), + plan.provider_id.clone(), + plan.provider_api_format.clone(), + None, + None, + true, + ) + .expect("endpoint should build") + .with_transport_fields( + "https://provider.example".to_string(), + None, + None, + Some(2), + None, + None, + None, + None, + ) + .expect("endpoint transport should build"); + let key = StoredProviderCatalogKey::new( + plan.key_id.clone(), + plan.provider_id.clone(), + plan.key_id.clone(), + "api_key".to_string(), + None, + true, + ) + .expect("key should build") + .with_transport_fields( + Some(json!([plan.provider_api_format.clone()])), + "plain-upstream-key".to_string(), + None, + None, + Some(json!({ "openai:chat": 1 })), + None, + None, + None, + None, + ) + .expect("key transport should build"); + + InMemoryProviderCatalogReadRepository::seed(vec![provider], vec![endpoint], vec![key]) + } + fn test_decision() -> GatewayControlDecision { GatewayControlDecision::synthetic( "/v1/chat/completions", @@ -5458,6 +5533,14 @@ mod tests { transport_profile: None, timeouts: None, }; + let state = state.with_data_state_for_tests( + crate::data::GatewayDataState::with_request_candidate_and_usage_repository_for_tests( + Arc::clone(&request_candidate_repository), + Arc::clone(&usage_repository), + ) + .with_provider_catalog_reader(Arc::new(provider_catalog_stop_429_for_plan(&plan))) + .with_encryption_key_for_tests("development-key"), + ); let trailer_error = connect_json_frame( 2, br#"{"error":{"code":"resource_exhausted","message":"an internal error occurred"}}"#, @@ -5493,10 +5576,7 @@ mod tests { "client_api_format": "claude:messages", "needs_conversion": true, "has_envelope": true, - "envelope_name": "windsurf:GetChatMessage", - "local_failover_policy": { - "stop_status_codes": [429] - } + "envelope_name": "windsurf:GetChatMessage" })), crate::clock::current_unix_ms(), Instant::now(), @@ -5590,6 +5670,14 @@ mod tests { transport_profile: None, timeouts: None, }; + let state = state.with_data_state_for_tests( + crate::data::GatewayDataState::with_request_candidate_and_usage_repository_for_tests( + Arc::clone(&request_candidate_repository), + Arc::clone(&usage_repository), + ) + .with_provider_catalog_reader(Arc::new(provider_catalog_stop_429_for_plan(&plan))) + .with_encryption_key_for_tests("development-key"), + ); let connect_error = connect_json_frame( 2, br#"{"error":{"code":"resource_exhausted","message":"quota exhausted"}}"#, @@ -5624,10 +5712,7 @@ mod tests { "client_api_format": "claude:messages", "needs_conversion": true, "has_envelope": true, - "envelope_name": "windsurf:GetChatMessage", - "local_failover_policy": { - "stop_status_codes": [429] - } + "envelope_name": "windsurf:GetChatMessage" })), crate::clock::current_unix_ms(), Instant::now(), diff --git a/apps/aether-gateway/src/execution_runtime/sync/execution/policy.rs b/apps/aether-gateway/src/execution_runtime/sync/execution/policy.rs index a8f5fd799..36368d0a6 100644 --- a/apps/aether-gateway/src/execution_runtime/sync/execution/policy.rs +++ b/apps/aether-gateway/src/execution_runtime/sync/execution/policy.rs @@ -16,6 +16,8 @@ pub(super) fn decode_execution_result_body( }; if let Some(json_body) = body.json_body { + remove_header_case_insensitive(headers, "content-encoding"); + remove_header_case_insensitive(headers, "content-length"); headers .entry("content-type".to_string()) .or_insert_with(|| "application/json".to_string()); @@ -34,3 +36,49 @@ pub(super) fn decode_execution_result_body( Ok((Vec::new(), None, None)) } + +fn remove_header_case_insensitive(headers: &mut BTreeMap, name: &str) { + if let Some(existing_key) = headers + .keys() + .find(|key| key.eq_ignore_ascii_case(name)) + .cloned() + { + headers.remove(&existing_key); + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use aether_contracts::ResponseBody; + use serde_json::json; + + use super::decode_execution_result_body; + + #[test] + fn decoded_json_body_drops_stale_content_encoding_headers() { + let mut headers = BTreeMap::from([ + ("content-encoding".to_string(), "gzip".to_string()), + ("content-length".to_string(), "999".to_string()), + ]); + + let (body_bytes, body_json, body_base64) = decode_execution_result_body( + Some(ResponseBody { + json_body: Some(json!({"ok": true})), + body_bytes_b64: None, + }), + &mut headers, + ) + .expect("body should decode"); + + assert_eq!(body_json, Some(json!({"ok": true}))); + assert_eq!(body_base64, None); + assert_eq!(body_bytes, br#"{"ok":true}"#); + assert_eq!(headers.get("content-encoding"), None); + assert_eq!( + headers.get("content-length").cloned(), + Some(body_bytes.len().to_string()) + ); + } +} diff --git a/apps/aether-gateway/src/execution_runtime/tests.rs b/apps/aether-gateway/src/execution_runtime/tests.rs index 2ab76dfae..7334edccd 100644 --- a/apps/aether-gateway/src/execution_runtime/tests.rs +++ b/apps/aether-gateway/src/execution_runtime/tests.rs @@ -63,6 +63,8 @@ fn missing_exact_provider_request_payload(decision_kind: &str) -> AiExecutionDec provider_request_body: None, provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, diff --git a/apps/aether-gateway/src/executor/outcome.rs b/apps/aether-gateway/src/executor/outcome.rs index df8d1c88b..889749fbe 100644 --- a/apps/aether-gateway/src/executor/outcome.rs +++ b/apps/aether-gateway/src/executor/outcome.rs @@ -111,6 +111,38 @@ impl LocalExecutionRuntimeMissContext { } Some(summaries.join(" | ")) } + + pub(crate) fn all_provider_request_body_build_failures_detail(&self) -> Option { + if self.candidate_contexts.is_empty() + || !self.candidate_contexts.iter().all(|candidate| { + candidate.candidate.status == RequestCandidateStatus::Skipped + && candidate + .candidate + .skip_reason + .as_deref() + .map(str::trim) + .is_some_and(|value| value == "provider_request_body_build_failed") + }) + { + return None; + } + + let diagnostic = self + .candidate_contexts + .iter() + .find_map(runtime_miss_candidate_failure_diagnostic)?; + let mut detail = format!("上游请求体转换失败:{}", diagnostic.message); + if diagnostic.path != "$" { + detail.push_str(&format!(";字段路径:{}", diagnostic.path)); + } + detail.push_str("(原因代码: provider_request_body_build_failed)"); + Some(detail) + } +} + +struct RuntimeMissFailureDiagnostic { + path: String, + message: String, } pub(crate) async fn build_local_execution_exhaustion( @@ -835,6 +867,41 @@ fn candidate_extra_data_string(candidate: &StoredRequestCandidate, key: &str) -> .map(ToOwned::to_owned) } +fn runtime_miss_candidate_failure_diagnostic( + candidate: &RuntimeMissCandidateContext, +) -> Option { + let extra_data = candidate.candidate.extra_data.as_ref()?.as_object()?; + let diagnostic = extra_data + .get("failure_diagnostic") + .and_then(Value::as_object) + .filter(|diagnostic| diagnostic.get("safe_to_show") != Some(&Value::Bool(false))) + .or_else(|| { + extra_data + .get("request_conversion_error") + .and_then(Value::as_object) + }) + .or_else(|| { + extra_data + .get("request_body_build_error") + .and_then(Value::as_object) + })?; + let message = diagnostic + .get("message") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty())?; + let path = diagnostic + .get("path") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or("$"); + Some(RuntimeMissFailureDiagnostic { + path: path.to_string(), + message: message.to_string(), + }) +} + fn build_runtime_miss_candidate_endpoint_url( candidate: &StoredRequestCandidate, endpoint: &StoredProviderCatalogEndpoint, @@ -1036,7 +1103,8 @@ mod tests { use super::{ apply_runtime_miss_usage_routing, beautify_local_execution_client_error_message, request_candidate_represents_provider_execution, - select_last_runtime_miss_executed_candidate, RuntimeMissCandidateContext, + select_last_runtime_miss_executed_candidate, LocalExecutionRuntimeMissContext, + RuntimeMissCandidateContext, }; use crate::constants::EXECUTION_PATH_LOCAL_EXECUTION_RUNTIME_MISS; use crate::state::LocalExecutionRuntimeMissDiagnostic; @@ -1161,4 +1229,68 @@ mod tests { assert!(select_last_runtime_miss_executed_candidate(&contexts).is_none()); } + + #[test] + fn runtime_miss_context_surfaces_request_conversion_field_diagnostic() { + let skipped_candidate = StoredRequestCandidate::new( + "cand-skipped".to_string(), + "req-1".to_string(), + Some("user-1".to_string()), + Some("api-key-1".to_string()), + Some("alice".to_string()), + Some("default".to_string()), + 0, + 0, + Some("provider-1".to_string()), + Some("endpoint-1".to_string()), + Some("provider-key-1".to_string()), + RequestCandidateStatus::Skipped, + Some("provider_request_body_build_failed".to_string()), + false, + None, + None, + None, + None, + None, + Some(json!({ + "failure_diagnostic": { + "kind": "request_conversion", + "path": "$.n", + "message": "openai:chat 字段 n 不能无损转换到 openai:responses:OpenAI Responses request has no canonical equivalent for this Chat field", + "safe_to_show": true + }, + "request_conversion_error": { + "path": "$.n", + "message": "compat" + } + })), + None, + 100, + None, + None, + ) + .expect("candidate should build"); + + let context = LocalExecutionRuntimeMissContext { + candidate_contexts: vec![RuntimeMissCandidateContext { + candidate: skipped_candidate, + provider_name: Some("openai".to_string()), + key_name: Some("prod".to_string()), + client_api_format: Some("openai:chat".to_string()), + provider_api_format: Some("openai:responses".to_string()), + global_model_name: Some("gpt-5".to_string()), + selected_provider_model_name: Some("gpt-5-upstream".to_string()), + endpoint_url: Some("https://api.openai.example/v1/responses".to_string()), + }], + ..LocalExecutionRuntimeMissContext::default() + }; + + let detail = context + .all_provider_request_body_build_failures_detail() + .expect("detail should include conversion diagnostic"); + + assert!(detail.contains("字段 n")); + assert!(detail.contains("字段路径:$.n")); + assert!(detail.contains("provider_request_body_build_failed")); + } } diff --git a/apps/aether-gateway/src/handlers/admin/observability/monitoring/trace.rs b/apps/aether-gateway/src/handlers/admin/observability/monitoring/trace.rs index c79857934..3eda959e0 100644 --- a/apps/aether-gateway/src/handlers/admin/observability/monitoring/trace.rs +++ b/apps/aether-gateway/src/handlers/admin/observability/monitoring/trace.rs @@ -89,7 +89,7 @@ async fn resolve_admin_monitoring_trace( { let usage = app .data - .read_request_usage_audit(request_id) + .read_request_usage_audit_shallow(request_id) .await .map_err(|err| GatewayError::Internal(err.to_string()))?; return Ok(Some(ResolvedAdminMonitoringTrace { trace, usage })); @@ -98,7 +98,7 @@ async fn resolve_admin_monitoring_trace( let mut usage_candidates = Vec::new(); if let Some(usage) = app .data - .read_request_usage_audit(request_id) + .read_request_usage_audit_shallow(request_id) .await .map_err(|err| GatewayError::Internal(err.to_string()))? { diff --git a/apps/aether-gateway/src/handlers/admin/observability/usage/detail_routes.rs b/apps/aether-gateway/src/handlers/admin/observability/usage/detail_routes.rs index a2ae6fd1a..bfa5d7c52 100644 --- a/apps/aether-gateway/src/handlers/admin/observability/usage/detail_routes.rs +++ b/apps/aether-gateway/src/handlers/admin/observability/usage/detail_routes.rs @@ -14,17 +14,76 @@ use aether_admin::observability::usage::{ admin_usage_bad_request_response, admin_usage_data_unavailable_response, admin_usage_provider_key_name, ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL, }; -use aether_data_contracts::repository::usage::UsageBodyField; +use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageBodyField}; use axum::{ body::Body, http, response::{IntoResponse, Response}, Json, }; -use serde_json::json; +use serde_json::{json, Value}; use std::collections::BTreeMap; use tokio::try_join; +struct AdminUsageDetailBodyValue { + value: Option, + load_failed: bool, +} + +async fn resolve_admin_usage_detail_request_body( + state: &AdminAppState<'_>, + item: &StoredRequestUsageAudit, +) -> AdminUsageDetailBodyValue { + match admin_usage_resolve_request_capture_body_for_item(state, item, None).await { + Ok(body) => AdminUsageDetailBodyValue { + value: body, + load_failed: false, + }, + Err(err) => { + tracing::warn!( + error = ?err, + usage_id = %item.id, + request_id = %item.request_id, + field = UsageBodyField::RequestBody.as_storage_field(), + "failed to resolve admin usage detail body" + ); + let value = admin_usage_resolve_request_capture_body(item, None); + AdminUsageDetailBodyValue { + load_failed: value.is_none(), + value, + } + } + } +} + +async fn resolve_admin_usage_detail_body_value( + state: &AdminAppState<'_>, + item: &StoredRequestUsageAudit, + field: UsageBodyField, +) -> AdminUsageDetailBodyValue { + let inline_body = item.body_value(field); + match admin_usage_resolve_body_value(state, item, inline_body, field).await { + Ok(body) => AdminUsageDetailBodyValue { + value: body, + load_failed: false, + }, + Err(err) => { + tracing::warn!( + error = ?err, + usage_id = %item.id, + request_id = %item.request_id, + field = field.as_storage_field(), + "failed to resolve admin usage detail body" + ); + let value = inline_body.cloned(); + AdminUsageDetailBodyValue { + load_failed: value.is_none(), + value, + } + } + } +} + pub(super) async fn maybe_build_local_admin_usage_detail_response( state: &AdminAppState<'_>, request_context: &AdminRequestContext<'_>, @@ -174,32 +233,40 @@ pub(super) async fn maybe_build_local_admin_usage_detail_response( let provider_key_name = admin_usage_provider_key_name(&item, &provider_key_names); let mut detail_item = item.clone(); + let mut body_load_errors = serde_json::Map::new(); let request_body = if include_bodies { - let (request_body, provider_request_body, response_body, client_response_body) = try_join!( - admin_usage_resolve_request_capture_body_for_item(state, &item, None), - admin_usage_resolve_body_value( + let (request_body, provider_request_body, response_body, client_response_body) = tokio::join!( + resolve_admin_usage_detail_request_body(state, &item), + resolve_admin_usage_detail_body_value( state, &item, - item.provider_request_body.as_ref(), UsageBodyField::ProviderRequestBody, ), - admin_usage_resolve_body_value( + resolve_admin_usage_detail_body_value( state, &item, - item.response_body.as_ref(), UsageBodyField::ResponseBody, ), - admin_usage_resolve_body_value( + resolve_admin_usage_detail_body_value( state, &item, - item.client_response_body.as_ref(), UsageBodyField::ClientResponseBody, ), - )?; - detail_item.provider_request_body = provider_request_body; - detail_item.response_body = response_body; - detail_item.client_response_body = client_response_body; - request_body + ); + for (field, resolved) in [ + (UsageBodyField::RequestBody, &request_body), + (UsageBodyField::ProviderRequestBody, &provider_request_body), + (UsageBodyField::ResponseBody, &response_body), + (UsageBodyField::ClientResponseBody, &client_response_body), + ] { + if resolved.load_failed { + body_load_errors.insert(field.as_storage_field().to_string(), json!(true)); + } + } + detail_item.provider_request_body = provider_request_body.value; + detail_item.response_body = response_body.value; + detail_item.client_response_body = client_response_body.value; + request_body.value } else { None }; @@ -207,7 +274,7 @@ pub(super) async fn maybe_build_local_admin_usage_detail_response( // request_body 已通过 request capture 解析;其余 detached body 在上方并行加载。 } let default_headers = admin_usage_curl_headers(); - let payload = build_admin_usage_detail_payload( + let mut payload = build_admin_usage_detail_payload( &detail_item, &users_by_id, &api_key_names, @@ -218,6 +285,11 @@ pub(super) async fn maybe_build_local_admin_usage_detail_response( request_body, &default_headers, ); + payload["body_load_errors"] = if include_bodies && !body_load_errors.is_empty() { + Value::Object(body_load_errors) + } else { + Value::Null + }; return Ok(Some(attach_admin_audit_response( Json(payload).into_response(), diff --git a/apps/aether-gateway/src/handlers/admin/observability/usage/summary_routes.rs b/apps/aether-gateway/src/handlers/admin/observability/usage/summary_routes.rs index 3b36f45dd..559b8a36a 100644 --- a/apps/aether-gateway/src/handlers/admin/observability/usage/summary_routes.rs +++ b/apps/aether-gateway/src/handlers/admin/observability/usage/summary_routes.rs @@ -5,13 +5,12 @@ use crate::handlers::admin::request::{AdminAppState, AdminRequestContext}; use crate::handlers::admin::shared::query_param_value; use crate::GatewayError; use aether_admin::observability::usage::{ - admin_usage_bad_request_response, admin_usage_client_family, - admin_usage_data_unavailable_response, admin_usage_has_fallback, admin_usage_is_failed, - admin_usage_matches_search, admin_usage_matches_username, admin_usage_parse_ids, - admin_usage_parse_limit, admin_usage_parse_offset, admin_usage_provider_key_name, - admin_usage_record_json, build_admin_usage_active_requests_response, - build_admin_usage_records_response, build_admin_usage_summary_stats_response_from_summary, - ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL, + admin_usage_bad_request_response, admin_usage_data_unavailable_response, + admin_usage_has_fallback, admin_usage_is_failed, admin_usage_matches_search, + admin_usage_matches_username, admin_usage_parse_ids, admin_usage_parse_limit, + admin_usage_parse_offset, admin_usage_provider_key_name, admin_usage_record_json, + build_admin_usage_active_requests_response, build_admin_usage_records_response, + build_admin_usage_summary_stats_response_from_summary, ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL, }; use aether_data::repository::users::StoredUserSummary; use aether_data_contracts::repository::{ @@ -195,29 +194,51 @@ async fn resolve_admin_usage_attempt_flags_by_usage_id( .collect()) } -async fn resolve_admin_usage_image_progress_by_request_id( +#[derive(Default)] +struct AdminUsageActiveCandidateState { + image_progress_by_request_id: BTreeMap, + state_overrides_by_request_id: BTreeMap, +} + +async fn resolve_admin_usage_active_candidate_state( state: &AdminAppState<'_>, items: &[StoredRequestUsageAudit], -) -> Result, GatewayError> { +) -> Result { if !state.has_request_candidate_data_reader() || items.is_empty() { - return Ok(BTreeMap::new()); + return Ok(AdminUsageActiveCandidateState::default()); } let request_ids = items .iter() .map(|item| item.request_id.clone()) .collect::>(); - let mut progress_by_request_id = BTreeMap::new(); + let active_usage_by_request_id = items + .iter() + .filter(|item| matches!(item.status.as_str(), "pending" | "streaming")) + .map(|item| (item.request_id.clone(), item)) + .collect::>(); + let mut candidate_state = AdminUsageActiveCandidateState::default(); for request_id in request_ids { let candidates = state .app() .read_request_candidates_by_request_id(&request_id) .await?; if let Some(progress) = latest_admin_usage_image_progress(&candidates) { - progress_by_request_id.insert(request_id, progress); + candidate_state + .image_progress_by_request_id + .insert(request_id.clone(), progress); + } + if active_usage_by_request_id.contains_key(&request_id) { + if let Some(override_payload) = + admin_usage_terminal_candidate_state_override(&candidates) + { + candidate_state + .state_overrides_by_request_id + .insert(request_id, override_payload); + } } } - Ok(progress_by_request_id) + Ok(candidate_state) } fn latest_admin_usage_image_progress( @@ -246,6 +267,82 @@ fn latest_admin_usage_image_progress( .map(|(_, _, _, progress)| progress) } +fn admin_usage_current_candidate( + candidates: &[StoredRequestCandidate], +) -> Option<&StoredRequestCandidate> { + candidates + .iter() + .filter(|candidate| { + !matches!( + candidate.status, + RequestCandidateStatus::Available + | RequestCandidateStatus::Unused + | RequestCandidateStatus::Skipped + ) + }) + .max_by_key(|candidate| { + ( + candidate.candidate_index, + candidate.retry_index, + candidate + .started_at_unix_ms + .or(candidate.finished_at_unix_ms) + .unwrap_or(candidate.created_at_unix_ms), + ) + }) +} + +fn admin_usage_unix_millis_to_rfc3339(unix_ms: u64) -> Option { + let secs = i64::try_from(unix_ms / 1_000).ok()?; + let nanos = u32::try_from(unix_ms % 1_000) + .ok()? + .saturating_mul(1_000_000); + chrono::DateTime::::from_timestamp(secs, nanos) + .map(|timestamp| timestamp.to_rfc3339()) +} + +fn admin_usage_terminal_candidate_state_override( + candidates: &[StoredRequestCandidate], +) -> Option { + let candidate = admin_usage_current_candidate(candidates)?; + + let status = match candidate.status { + RequestCandidateStatus::Success => "completed", + RequestCandidateStatus::Failed => "failed", + RequestCandidateStatus::Cancelled => "cancelled", + _ => return None, + }; + let latency_ms = candidate.latency_ms.or_else(|| { + Some( + candidate + .finished_at_unix_ms? + .saturating_sub(candidate.started_at_unix_ms?), + ) + }); + let mut payload = json!({ "status": status }); + if let Some(latency_ms) = latency_ms { + payload["response_time_ms"] = json!(latency_ms); + if let Some(response_time_updated_at) = candidate + .finished_at_unix_ms + .or_else(|| { + candidate + .started_at_unix_ms + .map(|started_at| started_at.saturating_add(latency_ms)) + }) + .and_then(admin_usage_unix_millis_to_rfc3339) + { + payload["response_time_updated_at"] = json!(response_time_updated_at); + } + } + if let Some(status_code) = candidate.status_code { + payload["status_code"] = json!(status_code); + } + if let Some(error_message) = candidate.error_message.as_ref() { + payload["error_message"] = json!(error_message); + } + Some(payload) +} + fn admin_usage_matches_attempt_status( item: &StoredRequestUsageAudit, status: &str, @@ -264,19 +361,6 @@ fn admin_usage_matches_attempt_status( } } -fn admin_usage_matches_client_family( - item: &StoredRequestUsageAudit, - client_family: Option<&str>, -) -> bool { - let Some(client_family) = client_family - .map(str::trim) - .filter(|value| !value.is_empty()) - else { - return true; - }; - admin_usage_client_family(item).is_some_and(|value| value.eq_ignore_ascii_case(client_family)) -} - fn admin_usage_bool_query_param(query: Option<&str>, name: &str) -> bool { query_param_value(query, name) .as_deref() @@ -290,15 +374,24 @@ fn admin_usage_bool_query_param(query: Option<&str>, name: &str) -> bool { .unwrap_or(false) } -fn admin_usage_is_unknown_label(value: &str) -> bool { - matches!( - value.trim().to_ascii_lowercase().as_str(), - "unknown" | "unknow" - ) +fn admin_usage_include_total_query_param(query: Option<&str>) -> bool { + query_param_value(query, "include_total") + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(|value| { + !(value == "0" + || value.eq_ignore_ascii_case("false") + || value.eq_ignore_ascii_case("no") + || value.eq_ignore_ascii_case("off")) + }) + .unwrap_or(true) } -fn admin_usage_has_unknown_model_or_provider(item: &StoredRequestUsageAudit) -> bool { - admin_usage_is_unknown_label(&item.model) || admin_usage_is_unknown_label(&item.provider_name) +fn admin_usage_fast_page_total(offset: usize, limit: usize, record_count: usize) -> usize { + offset + .saturating_add(record_count) + .saturating_add(usize::from(limit > 0 && record_count == limit)) } #[allow(clippy::too_many_arguments)] @@ -314,6 +407,7 @@ fn build_admin_usage_records_response_with_attempt_flags( total: usize, limit: usize, offset: usize, + total_is_estimated: bool, ) -> Response { let records: Vec<_> = items .iter() @@ -343,6 +437,7 @@ fn build_admin_usage_records_response_with_attempt_flags( "total": total, "limit": limit, "offset": offset, + "total_is_estimated": total_is_estimated, })) .into_response() } @@ -485,6 +580,8 @@ fn build_admin_usage_keyword_search_query( provider_name: base_query.provider_name.clone(), model: base_query.model.clone(), api_format: base_query.api_format.clone(), + client_family: base_query.client_family.clone(), + exclude_unknown_model_or_provider: base_query.exclude_unknown_model_or_provider, statuses: base_query.statuses.clone(), exclude_status_codes: base_query.exclude_status_codes.clone(), is_stream: base_query.is_stream, @@ -583,6 +680,7 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response( state.has_auth_api_key_data_reader(), &BTreeMap::new(), &BTreeMap::new(), + &BTreeMap::new(), ))); }; state @@ -606,15 +704,16 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response( }; let api_key_names = admin_usage_api_key_names(state, &items).await?; let provider_key_names = admin_usage_provider_key_names(state, &items).await?; - let image_progress_by_request_id = - resolve_admin_usage_image_progress_by_request_id(state, &items).await?; + let active_candidate_state = + resolve_admin_usage_active_candidate_state(state, &items).await?; return Ok(Some(build_admin_usage_active_requests_response( &items, &api_key_names, state.has_auth_api_key_data_reader(), &provider_key_names, - &image_progress_by_request_id, + &active_candidate_state.image_progress_by_request_id, + &active_candidate_state.state_overrides_by_request_id, ))); } Some("records") @@ -642,6 +741,8 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response( let client_family_filter = query_param_value(query, "client_family"); let hide_unknown_records = admin_usage_bool_query_param(query, "hide_unknown") || admin_usage_bool_query_param(query, "hide_unknown_records"); + let include_total = admin_usage_include_total_query_param(query); + let total_only = admin_usage_bool_query_param(query, "total_only"); let limit = match admin_usage_parse_limit(query) { Ok(value) => value, Err(detail) => return Ok(Some(admin_usage_bad_request_response(detail))), @@ -665,13 +766,6 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response( offset, ))); }; - let base_query = build_admin_usage_records_query( - created_from_unix_secs, - created_until_unix_secs, - query, - None, - None, - ); let active_search = search.as_deref().filter(|value| !value.trim().is_empty()); let active_username_filter = username_filter .as_deref() @@ -679,10 +773,16 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response( let active_client_family_filter = client_family_filter .as_deref() .filter(|value| !value.trim().is_empty()); - let (usage, total) = if hide_unknown_records - || attempt_status_filter.is_some() - || active_client_family_filter.is_some() - { + let mut base_query = build_admin_usage_records_query( + created_from_unix_secs, + created_until_unix_secs, + query, + None, + None, + ); + base_query.client_family = active_client_family_filter.map(str::to_owned); + base_query.exclude_unknown_model_or_provider = hide_unknown_records; + let (usage, total, total_is_estimated) = if attempt_status_filter.is_some() { let mut usage = state.list_usage_audits(&base_query).await?; let user_ids: Vec = usage .iter() @@ -719,18 +819,20 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response( &attempt_flags_by_usage_id, request_candidate_reader_available, ) - }) && admin_usage_matches_client_family(item, active_client_family_filter) - && (!hide_unknown_records - || !admin_usage_has_unknown_model_or_provider(item)) + }) }); sort_usage_newest_first(&mut usage); let total = usage.len(); - let records = usage - .into_iter() - .skip(offset) - .take(limit) - .collect::>(); - (records, total) + let records = if total_only { + Vec::new() + } else { + usage + .into_iter() + .skip(offset) + .take(limit) + .collect::>() + }; + (records, total, false) } else if active_search.is_some() || active_username_filter.is_some() { let keywords = active_search .map(parse_admin_usage_search_keywords) @@ -750,30 +852,53 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response( None, None, ); - let total = usize::try_from( - state - .count_usage_audits_by_keyword_search(&keyword_query) - .await?, - ) - .unwrap_or(usize::MAX); - let paged_query = UsageAuditKeywordSearchQuery { - limit: Some(limit), - offset: Some(offset), - ..keyword_query - }; - ( - state - .list_usage_audits_by_keyword_search(&paged_query) - .await?, - total, - ) - } else { - let total = usize::try_from(state.count_usage_audits(&base_query).await?) + if total_only { + let total = usize::try_from( + state + .count_usage_audits_by_keyword_search(&keyword_query) + .await?, + ) .unwrap_or(usize::MAX); - let mut paged_query = base_query.clone(); - paged_query.limit = Some(limit); - paged_query.offset = Some(offset); - (state.list_usage_audits(&paged_query).await?, total) + (Vec::new(), total, false) + } else { + let paged_query = UsageAuditKeywordSearchQuery { + limit: Some(limit), + offset: Some(offset), + ..keyword_query.clone() + }; + let records = state + .list_usage_audits_by_keyword_search(&paged_query) + .await?; + let total = if include_total { + usize::try_from( + state + .count_usage_audits_by_keyword_search(&keyword_query) + .await?, + ) + .unwrap_or(usize::MAX) + } else { + admin_usage_fast_page_total(offset, limit, records.len()) + }; + (records, total, !include_total) + } + } else { + if total_only { + let total = usize::try_from(state.count_usage_audits(&base_query).await?) + .unwrap_or(usize::MAX); + (Vec::new(), total, false) + } else { + let mut paged_query = base_query.clone(); + paged_query.limit = Some(limit); + paged_query.offset = Some(offset); + let records = state.list_usage_audits(&paged_query).await?; + let total = if include_total { + usize::try_from(state.count_usage_audits(&base_query).await?) + .unwrap_or(usize::MAX) + } else { + admin_usage_fast_page_total(offset, limit, records.len()) + }; + (records, total, !include_total) + } }; let user_ids: Vec = usage @@ -801,6 +926,7 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response( total, limit, offset, + total_is_estimated, ))); } _ => {} @@ -808,3 +934,88 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response( Ok(None) } + +#[cfg(test)] +mod tests { + use aether_data_contracts::repository::candidates::{ + RequestCandidateStatus, StoredRequestCandidate, + }; + + use super::admin_usage_terminal_candidate_state_override; + + fn sample_candidate( + candidate_index: i32, + status: RequestCandidateStatus, + status_code: Option, + latency_ms: Option, + error_message: Option<&str>, + ) -> StoredRequestCandidate { + StoredRequestCandidate::new( + format!("candidate-{candidate_index}"), + "req-1".to_string(), + Some("user-1".to_string()), + Some("api-key-1".to_string()), + Some("alice".to_string()), + Some("default".to_string()), + candidate_index, + 0, + Some("provider-1".to_string()), + Some("endpoint-1".to_string()), + Some("provider-key-1".to_string()), + status, + None, + false, + status_code, + None, + error_message.map(str::to_string), + latency_ms, + None, + None, + None, + 1_000, + Some(1_000), + Some(10_210), + ) + .expect("candidate should build") + } + + #[test] + fn admin_usage_active_override_uses_current_terminal_candidate_latency() { + let candidate = sample_candidate( + 0, + RequestCandidateStatus::Success, + Some(200), + Some(9_210), + None, + ); + + let payload = + admin_usage_terminal_candidate_state_override(&[candidate]).expect("override"); + + assert_eq!(payload["status"], "completed"); + assert_eq!(payload["response_time_ms"], 9_210); + assert_eq!( + payload["response_time_updated_at"], + "1970-01-01T00:00:10.210+00:00" + ); + } + + #[test] + fn admin_usage_active_override_ignores_terminal_candidate_when_newer_attempt_is_live() { + let failed = sample_candidate( + 0, + RequestCandidateStatus::Failed, + Some(503), + Some(1_000), + Some("first attempt failed"), + ); + let mut streaming = + sample_candidate(1, RequestCandidateStatus::Streaming, None, None, None); + streaming.started_at_unix_ms = Some(10_500); + streaming.finished_at_unix_ms = None; + + let payload = admin_usage_terminal_candidate_state_override(&[failed, streaming]); + + assert!(payload.is_none()); + } +} diff --git a/apps/aether-gateway/src/handlers/admin/provider/delete_task.rs b/apps/aether-gateway/src/handlers/admin/provider/delete_task.rs index 1fa60a009..7f54dc790 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/delete_task.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/delete_task.rs @@ -94,7 +94,7 @@ pub(crate) async fn run_admin_provider_delete_task( .map(|item| item.id.clone()) .collect::>(); let key_ids = keys.iter().map(|item| item.id.clone()).collect::>(); - app.cleanup_deleted_provider_catalog_refs(&provider.id, &endpoint_ids, &key_ids) + app.cleanup_deleted_provider_catalog_refs(&provider.id, true, &endpoint_ids, &key_ids) .await?; task.stage = "deleting_models".to_string(); diff --git a/apps/aether-gateway/src/handlers/admin/provider/oauth/errors.rs b/apps/aether-gateway/src/handlers/admin/provider/oauth/errors.rs index ce0daab9d..a76588d6d 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/oauth/errors.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/oauth/errors.rs @@ -21,7 +21,10 @@ fn oauth_invalid_reason_is_account_level_block(reason: Option<&str>) -> bool { snapshot.blocked && !matches!( snapshot.code.trim().to_ascii_lowercase().as_str(), - "oauth_token_invalid" | "oauth_expired" | "oauth_refresh_failed" + "oauth_token_invalid" + | "oauth_token_expired" + | "oauth_expired" + | "oauth_refresh_failed" ) } diff --git a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/invalid.rs b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/invalid.rs index fb2316048..55e3c156f 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/invalid.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/invalid.rs @@ -13,6 +13,10 @@ pub(super) fn codex_looks_like_token_invalidated(message: Option<&str>) -> bool admin_provider_quota_pure::codex_looks_like_token_invalidated(message) } +pub(super) fn codex_looks_like_token_expired(message: Option<&str>) -> bool { + admin_provider_quota_pure::codex_looks_like_token_expired(message) +} + pub(super) fn codex_looks_like_workspace_deactivated(message: Option<&str>) -> bool { admin_provider_quota_pure::codex_looks_like_workspace_deactivated(message) } diff --git a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/mod.rs b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/mod.rs index 0549975b3..47f6d0fcd 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/mod.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/mod.rs @@ -3,7 +3,7 @@ mod parse; mod plan; use self::invalid::{ - codex_build_invalid_state, codex_looks_like_token_invalidated, + codex_build_invalid_state, codex_looks_like_token_expired, codex_looks_like_token_invalidated, codex_looks_like_workspace_deactivated, codex_soft_request_failure_reason, codex_structured_invalid_reason, }; @@ -275,6 +275,7 @@ pub(crate) async fn refresh_codex_provider_quota_locally( } 403 => { let candidate_reason = if codex_looks_like_token_invalidated(err_msg.as_deref()) + || codex_looks_like_token_expired(err_msg.as_deref()) { codex_structured_invalid_reason(403, err_msg.as_deref()) } else { diff --git a/apps/aether-gateway/src/handlers/admin/provider/pool_admin/read_routes/keys.rs b/apps/aether-gateway/src/handlers/admin/provider/pool_admin/read_routes/keys.rs index 698e1f82a..b63a7b1c6 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/pool_admin/read_routes/keys.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/pool_admin/read_routes/keys.rs @@ -304,6 +304,7 @@ fn admin_pool_trimmed_string(value: Option<&Value>) -> Option { fn admin_pool_account_code_status_filter(code: &str) -> Option<&'static str> { match code.trim().to_ascii_lowercase().as_str() { "oauth_token_invalid" => Some("invalid"), + "oauth_token_expired" => Some("expired"), "account_banned" | "account_suspended" => Some("account_banned"), "account_disabled" => Some("account_disabled"), "workspace_deactivated" => Some("workspace_deactivated"), diff --git a/apps/aether-gateway/src/handlers/admin/request/provider/catalog.rs b/apps/aether-gateway/src/handlers/admin/request/provider/catalog.rs index 5ba857dfb..8a60fe7a3 100644 --- a/apps/aether-gateway/src/handlers/admin/request/provider/catalog.rs +++ b/apps/aether-gateway/src/handlers/admin/request/provider/catalog.rs @@ -220,11 +220,17 @@ impl<'a> AdminAppState<'a> { pub(crate) async fn cleanup_deleted_provider_catalog_refs( &self, provider_id: &str, + provider_deleted: bool, endpoint_ids: &[String], key_ids: &[String], ) -> Result<(), GatewayError> { self.app - .cleanup_deleted_provider_catalog_refs(provider_id, endpoint_ids, key_ids) + .cleanup_deleted_provider_catalog_refs( + provider_id, + provider_deleted, + endpoint_ids, + key_ids, + ) .await } } diff --git a/apps/aether-gateway/src/handlers/admin/request/provider/tasks.rs b/apps/aether-gateway/src/handlers/admin/request/provider/tasks.rs index d324a66a1..83120471d 100644 --- a/apps/aether-gateway/src/handlers/admin/request/provider/tasks.rs +++ b/apps/aether-gateway/src/handlers/admin/request/provider/tasks.rs @@ -260,7 +260,7 @@ impl<'a> AdminAppState<'a> { affected += 1; } } - self.cleanup_deleted_provider_catalog_refs(&provider.id, &[], &deleted_key_ids) + self.cleanup_deleted_provider_catalog_refs(&provider.id, false, &[], &deleted_key_ids) .await?; Ok(affected) @@ -295,7 +295,7 @@ impl<'a> AdminAppState<'a> { let deleted = self.delete_provider_catalog_key(&key.id).await?; if deleted { let deleted_key_ids = [key.id.clone()]; - self.cleanup_deleted_provider_catalog_refs(&provider.id, &[], &deleted_key_ids) + self.cleanup_deleted_provider_catalog_refs(&provider.id, false, &[], &deleted_key_ids) .await?; } Ok(deleted) @@ -356,7 +356,7 @@ impl<'a> AdminAppState<'a> { affected = affected.saturating_add(1); } } - self.cleanup_deleted_provider_catalog_refs(&provider.id, &[], &deleted_key_ids) + self.cleanup_deleted_provider_catalog_refs(&provider.id, false, &[], &deleted_key_ids) .await?; return Ok(Json( diff --git a/apps/aether-gateway/src/handlers/proxy/mod.rs b/apps/aether-gateway/src/handlers/proxy/mod.rs index af8c30278..164659c30 100644 --- a/apps/aether-gateway/src/handlers/proxy/mod.rs +++ b/apps/aether-gateway/src/handlers/proxy/mod.rs @@ -1797,13 +1797,21 @@ pub(crate) async fn proxy_request( .all_candidates_skipped_for_reason(AUTH_API_KEY_CONCURRENCY_LIMIT_SKIP_REASON) || local_execution_runtime_miss_context .all_candidates_skipped_for_reason(LEGACY_API_KEY_CONCURRENCY_LIMIT_SKIP_REASON); - let local_execution_runtime_miss_detail = local_execution_runtime_miss_detail( - control_decision, - local_execution_runtime_miss_diagnostic.as_ref(), - auth_api_key_concurrency_limited, - stream_request, - ) - .unwrap_or_else(|| "当前 AI 请求无法在本地执行:没有匹配到可用的执行路径".to_string()); + let local_execution_runtime_miss_detail = (!auth_api_key_concurrency_limited) + .then(|| { + local_execution_runtime_miss_context + .all_provider_request_body_build_failures_detail() + }) + .flatten() + .or_else(|| { + local_execution_runtime_miss_detail( + control_decision, + local_execution_runtime_miss_diagnostic.as_ref(), + auth_api_key_concurrency_limited, + stream_request, + ) + }) + .unwrap_or_else(|| "当前 AI 请求无法在本地执行:没有匹配到可用的执行路径".to_string()); let local_execution_failure_path = if auth_api_key_concurrency_limited { EXECUTION_PATH_LOCAL_API_KEY_CONCURRENCY_LIMITED } else { diff --git a/apps/aether-gateway/src/handlers/public/support/user_me_usage.rs b/apps/aether-gateway/src/handlers/public/support/user_me_usage.rs index dee32610c..d37d662a2 100644 --- a/apps/aether-gateway/src/handlers/public/support/user_me_usage.rs +++ b/apps/aether-gateway/src/handlers/public/support/user_me_usage.rs @@ -4,11 +4,14 @@ use aether_ai_serving::UPSTREAM_IS_STREAM_KEY; use aether_billing::{ normalize_input_tokens_for_billing, normalize_total_input_context_for_cache_hit_rate, }; -use aether_data_contracts::repository::usage::{ - StoredRequestUsageAudit, StoredUsageBreakdownSummaryRow, StoredUsageDailySummary, - UsageAuditKeywordSearchQuery, UsageAuditListQuery, UsageBreakdownGroupBy, - UsageBreakdownSummaryQuery, UsageCacheAffinityIntervalGroupBy, UsageCacheAffinityIntervalQuery, - UsageDashboardSummaryQuery, +use aether_data_contracts::repository::{ + candidates::{RequestCandidateStatus, StoredRequestCandidate}, + usage::{ + StoredRequestUsageAudit, StoredUsageBreakdownSummaryRow, StoredUsageDailySummary, + UsageAuditKeywordSearchQuery, UsageAuditListQuery, UsageBreakdownGroupBy, + UsageBreakdownSummaryQuery, UsageCacheAffinityIntervalGroupBy, + UsageCacheAffinityIntervalQuery, UsageDashboardSummaryQuery, + }, }; use axum::{ body::Body, @@ -17,7 +20,7 @@ use axum::{ Json, }; use chrono::Utc; -use serde_json::json; +use serde_json::{json, Value}; use crate::GatewayError; @@ -531,6 +534,8 @@ fn build_users_me_usage_active_payload(item: &StoredRequestUsageAudit) -> serde_ "rate_multiplier": item.settlement_rate_multiplier(), "response_time_ms": item.response_time_ms, "first_byte_time_ms": item.first_byte_time_ms, + "updated_at": unix_secs_to_rfc3339(item.updated_at_unix_secs), + "response_time_updated_at": users_me_usage_response_time_updated_at(item), "status_code": item.status_code, "error_message": item.error_message, "api_format": item.api_format, @@ -573,6 +578,118 @@ fn build_users_me_usage_active_payload(item: &StoredRequestUsageAudit) -> serde_ payload } +fn users_me_usage_response_time_updated_at(item: &StoredRequestUsageAudit) -> Option { + item.response_time_ms?; + if matches!(item.status.as_str(), "pending" | "streaming") + && item.updated_at_unix_secs <= item.created_at_unix_ms + { + return None; + } + unix_secs_to_rfc3339(item.updated_at_unix_secs) +} + +fn unix_millis_to_rfc3339(unix_ms: u64) -> Option { + let secs = i64::try_from(unix_ms / 1_000).ok()?; + let nanos = u32::try_from(unix_ms % 1_000) + .ok()? + .saturating_mul(1_000_000); + chrono::DateTime::::from_timestamp(secs, nanos).map(|timestamp| timestamp.to_rfc3339()) +} + +fn users_me_usage_current_candidate( + candidates: &[StoredRequestCandidate], +) -> Option<&StoredRequestCandidate> { + candidates + .iter() + .filter(|candidate| { + !matches!( + candidate.status, + RequestCandidateStatus::Available + | RequestCandidateStatus::Unused + | RequestCandidateStatus::Skipped + ) + }) + .max_by_key(|candidate| { + ( + candidate.candidate_index, + candidate.retry_index, + candidate + .started_at_unix_ms + .or(candidate.finished_at_unix_ms) + .unwrap_or(candidate.created_at_unix_ms), + ) + }) +} + +fn users_me_usage_terminal_candidate_state_override( + candidates: &[StoredRequestCandidate], +) -> Option { + let candidate = users_me_usage_current_candidate(candidates)?; + + let status = match candidate.status { + RequestCandidateStatus::Success => "completed", + RequestCandidateStatus::Failed => "failed", + RequestCandidateStatus::Cancelled => "cancelled", + _ => return None, + }; + let latency_ms = candidate.latency_ms.or_else(|| { + Some( + candidate + .finished_at_unix_ms? + .saturating_sub(candidate.started_at_unix_ms?), + ) + }); + let mut payload = json!({ "status": status }); + if let Some(latency_ms) = latency_ms { + payload["response_time_ms"] = json!(latency_ms); + if let Some(response_time_updated_at) = candidate + .finished_at_unix_ms + .or_else(|| { + candidate + .started_at_unix_ms + .map(|started_at| started_at.saturating_add(latency_ms)) + }) + .and_then(unix_millis_to_rfc3339) + { + payload["response_time_updated_at"] = json!(response_time_updated_at); + } + } + if let Some(status_code) = candidate.status_code { + payload["status_code"] = json!(status_code); + } + if let Some(error_message) = candidate.error_message.as_ref() { + payload["error_message"] = json!(error_message); + } + Some(payload) +} + +async fn resolve_users_me_usage_active_state_overrides_by_request_id( + state: &AppState, + items: &[StoredRequestUsageAudit], +) -> Result, GatewayError> { + if !state.has_request_candidate_data_reader() || items.is_empty() { + return Ok(BTreeMap::new()); + } + + let active_request_ids = items + .iter() + .filter(|item| matches!(item.status.as_str(), "pending" | "streaming")) + .map(|item| item.request_id.clone()) + .collect::>(); + let mut overrides = BTreeMap::new(); + for request_id in active_request_ids { + let candidates = state + .read_request_candidates_by_request_id(&request_id) + .await?; + if let Some(override_payload) = + users_me_usage_terminal_candidate_state_override(&candidates) + { + overrides.insert(request_id, override_payload); + } + } + Ok(overrides) +} + fn users_me_usage_is_failed(item: &StoredRequestUsageAudit) -> bool { let has_failure_signal = item.status_code.is_some_and(|value| value >= 400) || item @@ -934,6 +1051,8 @@ pub(super) async fn handle_users_me_usage_get( provider_name: None, model: None, api_format: None, + client_family: None, + exclude_unknown_model_or_provider: false, statuses: None, exclude_status_codes: Vec::new(), is_stream: None, @@ -988,6 +1107,8 @@ pub(super) async fn handle_users_me_usage_get( provider_name: None, model: None, api_format: None, + client_family: None, + exclude_unknown_model_or_provider: false, statuses: None, exclude_status_codes: Vec::new(), is_stream: None, @@ -1015,6 +1136,8 @@ pub(super) async fn handle_users_me_usage_get( provider_name: None, model: None, api_format: None, + client_family: None, + exclude_unknown_model_or_provider: false, statuses: None, exclude_status_codes: Vec::new(), is_stream: None, @@ -1152,6 +1275,8 @@ pub(super) async fn handle_users_me_usage_active_get( provider_name: None, model: None, api_format: None, + client_family: None, + exclude_unknown_model_or_provider: false, statuses: Some(vec!["pending".to_string(), "streaming".to_string()]), exclude_status_codes: Vec::new(), is_stream: None, @@ -1181,11 +1306,35 @@ pub(super) async fn handle_users_me_usage_active_get( .filter(|item| !users_me_usage_is_failed(item)) .collect::>() }; + let active_state_overrides = + match resolve_users_me_usage_active_state_overrides_by_request_id(state, &items).await { + Ok(value) => value, + Err(err) => { + return build_auth_error_response( + http::StatusCode::INTERNAL_SERVER_ERROR, + format!("user active usage candidate lookup failed: {err:?}"), + false, + ); + } + }; Json(json!({ "requests": items .iter() - .map(build_users_me_usage_active_payload) + .map(|item| { + let mut payload = build_users_me_usage_active_payload(item); + if let (Some(payload), Some(overrides)) = ( + payload.as_object_mut(), + active_state_overrides + .get(&item.request_id) + .and_then(Value::as_object), + ) { + for (key, value) in overrides { + payload.insert(key.clone(), value.clone()); + } + } + payload + }) .collect::>(), })) .into_response() @@ -1377,13 +1526,16 @@ async fn build_usage_heatmap_summaries( mod tests { use std::collections::BTreeMap; - use aether_data_contracts::repository::usage::StoredRequestUsageAudit; + use aether_data_contracts::repository::{ + candidates::{RequestCandidateStatus, StoredRequestCandidate}, + usage::StoredRequestUsageAudit, + }; use serde_json::json; use super::{ build_users_me_usage_active_payload, build_users_me_usage_record_payload, users_me_usage_client_is_stream, users_me_usage_is_failed, - users_me_usage_upstream_is_stream, + users_me_usage_terminal_candidate_state_override, users_me_usage_upstream_is_stream, }; fn sample_usage(status: &str) -> StoredRequestUsageAudit { @@ -1428,6 +1580,41 @@ mod tests { .expect("usage should build") } + fn sample_candidate( + status: RequestCandidateStatus, + status_code: Option, + latency_ms: Option, + error_message: Option<&str>, + ) -> StoredRequestCandidate { + StoredRequestCandidate::new( + "candidate-1".to_string(), + "req-1".to_string(), + Some("user-1".to_string()), + Some("api-key-1".to_string()), + Some("alice".to_string()), + Some("default".to_string()), + 0, + 0, + Some("provider-1".to_string()), + Some("endpoint-1".to_string()), + Some("provider-key-1".to_string()), + status, + None, + false, + status_code, + None, + error_message.map(str::to_string), + latency_ms, + None, + None, + None, + 1_000, + Some(1_000), + Some(10_210), + ) + .expect("candidate should build") + } + #[test] fn user_usage_record_payload_rehydrates_cache_creation_total_from_classified_fields() { let item = StoredRequestUsageAudit { @@ -1460,6 +1647,45 @@ mod tests { assert_eq!(payload["cache_creation_ephemeral_1h_input_tokens"], 6); } + #[test] + fn user_usage_active_override_uses_terminal_candidate_latency() { + let candidate = sample_candidate( + RequestCandidateStatus::Success, + Some(200), + Some(9_210), + None, + ); + + let payload = + users_me_usage_terminal_candidate_state_override(&[candidate]).expect("override"); + + assert_eq!(payload["status"], "completed"); + assert_eq!(payload["response_time_ms"], 9_210); + assert_eq!(payload["status_code"], 200); + assert_eq!( + payload["response_time_updated_at"], + "1970-01-01T00:00:10.210+00:00" + ); + } + + #[test] + fn user_usage_active_override_ignores_terminal_candidate_when_newer_attempt_is_live() { + let failed = sample_candidate( + RequestCandidateStatus::Failed, + Some(503), + Some(1_000), + Some("first attempt failed"), + ); + let mut streaming = sample_candidate(RequestCandidateStatus::Streaming, None, None, None); + streaming.candidate_index = 1; + streaming.started_at_unix_ms = Some(10_500); + streaming.finished_at_unix_ms = None; + + let payload = users_me_usage_terminal_candidate_state_override(&[failed, streaming]); + + assert!(payload.is_none()); + } + #[test] fn user_usage_payload_keeps_claude_effective_input_when_cache_read_is_large() { let item = StoredRequestUsageAudit { diff --git a/apps/aether-gateway/src/handlers/shared/catalog.rs b/apps/aether-gateway/src/handlers/shared/catalog.rs index cd63314a3..62bf1b52c 100644 --- a/apps/aether-gateway/src/handlers/shared/catalog.rs +++ b/apps/aether-gateway/src/handlers/shared/catalog.rs @@ -265,14 +265,16 @@ fn build_provider_key_oauth_status_snapshot(key: &StoredProviderCatalogKey) -> V if let Some(reason) = tagged_oauth_invalid_reason(invalid_reason.as_deref(), OAUTH_EXPIRED_PREFIX) { + let (code, label) = + admin_provider_status_pure::oauth_token_snapshot_status_parts(reason.as_str()); return json!({ - "code": "invalid", - "label": "已失效", + "code": code, + "label": label, "reason": reason, "expires_at": expires_at_unix_secs, "invalid_at": invalid_at_unix_secs, "source": "oauth_invalid", - "requires_reauth": true, + "requires_reauth": code == "invalid", "expiring_soon": false, }); } diff --git a/apps/aether-gateway/src/handlers/shared/normalize.rs b/apps/aether-gateway/src/handlers/shared/normalize.rs index 07349b320..7d1fe4e6e 100644 --- a/apps/aether-gateway/src/handlers/shared/normalize.rs +++ b/apps/aether-gateway/src/handlers/shared/normalize.rs @@ -352,7 +352,8 @@ fn normalize_chat_pii_redaction_feature_settings( fn normalize_chat_pii_redaction_feature_object( feature: &mut Map, ) -> Result<(), String> { - for key in ["enabled", "inject_model_instruction"] { + feature.remove("inject_model_instruction"); + for key in ["enabled"] { if let Some(value) = feature.get(key) { if !value.is_boolean() { return Err(format!("chat_pii_redaction.{key} 必须是布尔值")); @@ -450,7 +451,7 @@ mod tests { fn user_self_feature_update_preserves_notification_push_permission() { let normalized = normalize_user_self_feature_settings_update( Some(json!({ - "chat_pii_redaction": {"enabled": true, "inject_model_instruction": false}, + "chat_pii_redaction": {"enabled": true}, "notification_push_service": {"enabled": false} })), Some(json!({ diff --git a/apps/aether-gateway/src/orchestration/classifier.rs b/apps/aether-gateway/src/orchestration/classifier.rs index ed0263f48..10f285068 100644 --- a/apps/aether-gateway/src/orchestration/classifier.rs +++ b/apps/aether-gateway/src/orchestration/classifier.rs @@ -61,11 +61,8 @@ pub(crate) fn classify_local_failover( } if input.status_code >= 400 - && input.response_text.is_some_and(|text| { - policy - .error_stop_patterns - .iter() - .any(|rule| local_failover_regex_rule_matches(rule, text, input.status_code)) + && policy.error_stop_patterns.iter().any(|rule| { + local_failover_regex_rule_matches(rule, input.response_text, input.status_code) }) { return LocalFailoverClassification::StopErrorPattern; @@ -76,7 +73,7 @@ pub(crate) fn classify_local_failover( policy .success_failover_patterns .iter() - .any(|rule| local_failover_regex_rule_matches(rule, text, input.status_code)) + .any(|rule| local_failover_regex_rule_matches(rule, Some(text), input.status_code)) }) { return LocalFailoverClassification::RetrySuccessPattern; @@ -190,14 +187,23 @@ fn first_non_empty_json_text( fn local_failover_regex_rule_matches( rule: &LocalFailoverRegexRule, - response_text: &str, + response_text: Option<&str>, status_code: u16, ) -> bool { if !rule.status_codes.is_empty() && !rule.status_codes.contains(&status_code) { return false; } - Regex::new(&rule.pattern) + let pattern = rule.pattern.trim(); + if pattern.is_empty() { + return !rule.status_codes.is_empty(); + } + + let Some(response_text) = response_text else { + return false; + }; + + Regex::new(pattern) .ok() .is_some_and(|regex| regex.is_match(response_text)) } @@ -260,6 +266,50 @@ mod tests { ); } + #[test] + fn classifier_detects_error_stop_pattern_without_status_codes_on_any_error_status() { + let policy = LocalFailoverPolicy { + error_stop_patterns: vec![LocalFailoverRegexRule { + pattern: "content_policy_violation".to_string(), + status_codes: BTreeSet::new(), + }], + ..LocalFailoverPolicy::default() + }; + + for status_code in [400, 429, 503] { + assert_eq!( + classify_local_failover( + &policy, + LocalFailoverInput::new( + status_code, + Some("{\"error\":\"content_policy_violation\"}") + ) + ), + LocalFailoverClassification::StopErrorPattern + ); + } + } + + #[test] + fn classifier_detects_status_only_error_stop_rule_without_response_text() { + let policy = LocalFailoverPolicy { + error_stop_patterns: vec![LocalFailoverRegexRule { + pattern: String::new(), + status_codes: [429].into_iter().collect(), + }], + ..LocalFailoverPolicy::default() + }; + + assert_eq!( + classify_local_failover(&policy, LocalFailoverInput::new(429, None)), + LocalFailoverClassification::StopErrorPattern + ); + assert_eq!( + classify_local_failover(&policy, LocalFailoverInput::new(503, None)), + LocalFailoverClassification::RetryUpstreamFailure + ); + } + #[test] fn classifier_detects_success_continue_status_code() { let policy = LocalFailoverPolicy { diff --git a/apps/aether-gateway/src/orchestration/effects.rs b/apps/aether-gateway/src/orchestration/effects.rs index 8371a6a62..222500b5e 100644 --- a/apps/aether-gateway/src/orchestration/effects.rs +++ b/apps/aether-gateway/src/orchestration/effects.rs @@ -2031,7 +2031,7 @@ mod tests { } #[tokio::test] - async fn oauth_invalidation_marks_codex_key_invalid() { + async fn oauth_invalidation_marks_codex_key_expired() { let state = codex_state(); let plan = sample_codex_plan(); @@ -2069,7 +2069,7 @@ mod tests { .and_then(|value| value.get("oauth")) .and_then(|value| value.get("code")) .and_then(Value::as_str), - Some("invalid") + Some("expired") ); } diff --git a/apps/aether-gateway/src/orchestration/policy.rs b/apps/aether-gateway/src/orchestration/policy.rs index e150f688d..24815f89c 100644 --- a/apps/aether-gateway/src/orchestration/policy.rs +++ b/apps/aether-gateway/src/orchestration/policy.rs @@ -25,27 +25,8 @@ pub(crate) struct LocalFailoverRegexRule { pub(crate) async fn resolve_local_failover_policy( state: &AppState, plan: &ExecutionPlan, - report_context: Option<&serde_json::Value>, + _report_context: Option<&serde_json::Value>, ) -> LocalFailoverPolicy { - if let Some(policy) = local_failover_policy_from_report_context(report_context) { - debug!( - event_name = "local_failover_policy_loaded", - log_type = "debug", - request_id = %plan.request_id, - provider_id = %plan.provider_id, - endpoint_id = %plan.endpoint_id, - key_id = %plan.key_id, - source = "report_context", - max_retries = ?policy.max_retries, - stop_status_code_count = policy.stop_status_codes.len(), - continue_status_code_count = policy.continue_status_codes.len(), - success_failover_pattern_count = policy.success_failover_patterns.len(), - error_stop_pattern_count = policy.error_stop_patterns.len(), - "gateway loaded local failover policy from report context" - ); - return policy; - } - let transport = match state .read_provider_transport_snapshot(&plan.provider_id, &plan.endpoint_id, &plan.key_id) .await @@ -201,31 +182,39 @@ fn parse_regex_rules( rules: &serde_json::Map, key: &str, ) -> Vec { + let allow_status_only = key == "error_stop_patterns"; rules .get(key) .and_then(Value::as_array) .into_iter() .flat_map(|items| items.iter()) - .filter_map(parse_regex_rule) + .filter_map(|value| parse_regex_rule(value, allow_status_only)) .collect() } -fn parse_regex_rule(value: &serde_json::Value) -> Option { +fn parse_regex_rule( + value: &serde_json::Value, + allow_status_only: bool, +) -> Option { let object = value.as_object()?; let pattern = object .get("pattern") .and_then(Value::as_str) .map(str::trim) - .filter(|value| !value.is_empty())?; + .unwrap_or_default(); + let status_codes: BTreeSet = object + .get("status_codes") + .and_then(Value::as_array) + .into_iter() + .flat_map(|values| values.iter()) + .filter_map(|value| parse_u64_value(value).and_then(|value| u16::try_from(value).ok())) + .collect(); + if pattern.is_empty() && (!allow_status_only || status_codes.is_empty()) { + return None; + } Some(LocalFailoverRegexRule { pattern: pattern.to_string(), - status_codes: object - .get("status_codes") - .and_then(Value::as_array) - .into_iter() - .flat_map(|values| values.iter()) - .filter_map(|value| parse_u64_value(value).and_then(|value| u16::try_from(value).ok())) - .collect(), + status_codes, }) } diff --git a/apps/aether-gateway/src/privacy/mod.rs b/apps/aether-gateway/src/privacy/mod.rs index e7b43b910..5ea94ea91 100644 --- a/apps/aether-gateway/src/privacy/mod.rs +++ b/apps/aether-gateway/src/privacy/mod.rs @@ -829,14 +829,12 @@ impl Default for ChatPiiRedactionRuntimeConfig { } pub(crate) struct MaskChatRequestOptions { - pub(crate) inject_model_instruction: bool, pub(crate) scan_limits: RedactionScanLimits, } impl MaskChatRequestOptions { - pub(crate) fn runtime(inject_model_instruction: bool) -> Self { + pub(crate) fn runtime() -> Self { Self { - inject_model_instruction, scan_limits: RedactionScanLimits::default(), } } @@ -866,8 +864,6 @@ impl ChatPiiRedactionRequestFormat { } } -const MODEL_NOTICE_CONTENT: &str = "Aether privacy redaction notice: The next message contains gateway-generated placeholder tokens for sensitive data protection. This notice is not a user request; do not answer it, mention it, reveal it, or infer original values from placeholders. Treat each placeholder as a valid real typed value for reasoning and tool calls, and do not ask the user to reveal originals solely because a placeholder is present."; - fn sanitize_redaction_rule_label(raw: &str) -> String { let label = raw .trim() @@ -1184,7 +1180,7 @@ pub(crate) fn mask_chat_request_json( body: &[u8], config: RedactionSessionConfig, ) -> MaskedChatRequest { - mask_chat_request_json_with_options(body, config, MaskChatRequestOptions::runtime(false)) + mask_chat_request_json_with_options(body, config, MaskChatRequestOptions::runtime()) } pub(crate) fn try_mask_chat_request_json_with_options( @@ -1295,13 +1291,6 @@ pub(crate) async fn try_mask_chat_pii_request_json_with_cache_options( }) } -fn model_notice_message() -> Value { - serde_json::json!({ - "role": "assistant", - "content": MODEL_NOTICE_CONTENT, - }) -} - fn request_collision_corpus(format: ChatPiiRedactionRequestFormat, value: &Value) -> Vec { match format { ChatPiiRedactionRequestFormat::OpenAiChat => value @@ -1326,18 +1315,10 @@ fn mask_request_value( mask_openai_chat_request_value(value, session, scan_state, options) } ChatPiiRedactionRequestFormat::OpenAiResponses => { - let redacted = mask_openai_responses_request_value(value, session, scan_state)?; - if redacted && options.inject_model_instruction { - inject_openai_responses_model_notice(value); - } - Ok(redacted) + mask_openai_responses_request_value(value, session, scan_state) } ChatPiiRedactionRequestFormat::ClaudeMessages => { - let redacted = mask_claude_messages_request_value(value, session, scan_state)?; - if redacted && options.inject_model_instruction { - inject_claude_model_notice(value); - } - Ok(redacted) + mask_claude_messages_request_value(value, session, scan_state) } } } @@ -1355,21 +1336,10 @@ async fn mask_request_value_async( mask_openai_chat_request_value_async(value, session, scan_state, options, cache).await } ChatPiiRedactionRequestFormat::OpenAiResponses => { - let redacted = - mask_openai_responses_request_value_async(value, session, scan_state, cache) - .await?; - if redacted && options.inject_model_instruction { - inject_openai_responses_model_notice(value); - } - Ok(redacted) + mask_openai_responses_request_value_async(value, session, scan_state, cache).await } ChatPiiRedactionRequestFormat::ClaudeMessages => { - let redacted = - mask_claude_messages_request_value_async(value, session, scan_state, cache).await?; - if redacted && options.inject_model_instruction { - inject_claude_model_notice(value); - } - Ok(redacted) + mask_claude_messages_request_value_async(value, session, scan_state, cache).await } } } @@ -1385,16 +1355,10 @@ fn mask_openai_chat_request_value( }; let mut redacted = false; - let mut notice_inserted = false; let mut index = 0; while index < messages.len() { let message_redacted = mask_chat_message_value(&mut messages[index], session, scan_state)?; redacted |= message_redacted; - if options.inject_model_instruction && message_redacted && !notice_inserted { - messages.insert(index, model_notice_message()); - notice_inserted = true; - index += 1; - } index += 1; } Ok(redacted) @@ -1412,17 +1376,11 @@ async fn mask_openai_chat_request_value_async( }; let mut redacted = false; - let mut notice_inserted = false; let mut index = 0; while index < messages.len() { let message_redacted = mask_chat_message_value_async(&mut messages[index], session, scan_state, cache).await?; redacted |= message_redacted; - if options.inject_model_instruction && message_redacted && !notice_inserted { - messages.insert(index, model_notice_message()); - notice_inserted = true; - index += 1; - } index += 1; } Ok(redacted) @@ -2150,56 +2108,6 @@ async fn mask_json_string_async( Ok(true) } -fn inject_openai_responses_model_notice(value: &mut Value) { - let Some(request) = value.as_object_mut() else { - return; - }; - match request.get_mut("instructions") { - Some(Value::String(instructions)) => prepend_model_notice(instructions), - Some(_) => {} - None => { - request.insert( - "instructions".to_string(), - Value::String(MODEL_NOTICE_CONTENT.to_string()), - ); - } - } -} - -fn inject_claude_model_notice(value: &mut Value) { - let Some(request) = value.as_object_mut() else { - return; - }; - match request.get_mut("system") { - Some(Value::String(system)) => prepend_model_notice(system), - Some(Value::Array(parts)) => parts.insert( - 0, - serde_json::json!({ - "type": "text", - "text": MODEL_NOTICE_CONTENT, - }), - ), - Some(_) => {} - None => { - request.insert( - "system".to_string(), - Value::String(MODEL_NOTICE_CONTENT.to_string()), - ); - } - } -} - -fn prepend_model_notice(text: &mut String) { - if text.contains(MODEL_NOTICE_CONTENT) { - return; - } - if text.trim().is_empty() { - *text = MODEL_NOTICE_CONTENT.to_string(); - } else { - *text = format!("{MODEL_NOTICE_CONTENT}\n\n{text}"); - } -} - pub(crate) struct RestoredSyncResponseBody { pub(crate) body: Vec, pub(crate) restored: bool, @@ -4408,7 +4316,7 @@ mod tests { &raw, ChatPiiRedactionRequestFormat::ClaudeMessages, test_config(), - MaskChatRequestOptions::runtime(true), + MaskChatRequestOptions::runtime(), ) .expect("claude messages request should mask"); @@ -4417,11 +4325,7 @@ mod tests { let masked_json: serde_json::Value = serde_json::from_slice(&masked.body).expect("masked request should stay valid JSON"); assert_eq!(masked_json["metadata"]["owner"], "metadata@example.com"); - assert!(masked_json["system"][0]["text"] - .as_str() - .expect("notice should remain a string") - .contains("Aether privacy redaction notice")); - assert!(!masked_json["system"][1]["text"] + assert!(!masked_json["system"][0]["text"] .as_str() .expect("system text should remain a string") .contains("alice@example.com")); @@ -4454,7 +4358,7 @@ mod tests { &raw, ChatPiiRedactionRequestFormat::OpenAiChat, test_config(), - MaskChatRequestOptions::runtime(false), + MaskChatRequestOptions::runtime(), ) .expect("chat request should mask"); @@ -4497,7 +4401,7 @@ mod tests { &raw, ChatPiiRedactionRequestFormat::OpenAiResponses, test_config(), - MaskChatRequestOptions::runtime(true), + MaskChatRequestOptions::runtime(), ) .expect("responses request should mask"); @@ -4509,7 +4413,6 @@ mod tests { let instructions = masked_json["instructions"] .as_str() .expect("instructions should remain a string"); - assert!(instructions.contains("Aether privacy redaction notice")); assert!(!instructions.contains("alice@example.com")); assert!(!masked_json["input"][0]["content"][0]["text"] .as_str() @@ -4995,7 +4898,7 @@ mod tests { let masked = mask_chat_request_json_with_options( &serde_json::to_vec(&request).expect("request should serialize"), build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 600), - MaskChatRequestOptions::runtime(false), + MaskChatRequestOptions::runtime(), ); let masked_json: serde_json::Value = @@ -5032,7 +4935,7 @@ mod tests { let masked = mask_chat_request_json_with_options( &serde_json::to_vec(&request).expect("request should serialize"), build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 600), - MaskChatRequestOptions::runtime(true), + MaskChatRequestOptions::runtime(), ); assert!(!masked.redacted); @@ -5041,9 +4944,6 @@ mod tests { assert_eq!(masked_json, request); assert!(masked_json.to_string().contains("alice@example.com")); assert!(!masked_json.to_string().contains(" bool { let trimmed_reason = invalid_reason.trim(); - if oauth_invalid_reason_has_tag(trimmed_reason, "[OAUTH_EXPIRED]") { - return true; - } let account_state = admin_provider_status_pure::resolve_pool_account_state( Some(provider_type), @@ -433,6 +430,7 @@ fn oauth_account_state_code_is_hard_block(code: &str) -> bool { | "account_forbidden" | "account_blocked" | "account_verification" + | "oauth_token_invalid" ) } diff --git a/apps/aether-gateway/src/scheduler/candidate/tests/selection.rs b/apps/aether-gateway/src/scheduler/candidate/tests/selection.rs index 84a1bea74..9a7bcbf15 100644 --- a/apps/aether-gateway/src/scheduler/candidate/tests/selection.rs +++ b/apps/aether-gateway/src/scheduler/candidate/tests/selection.rs @@ -2064,7 +2064,7 @@ async fn keeps_codex_candidate_selectable_when_oauth_token_is_expired() { let mut key = sample_key("key-codex", "provider-codex", Some(10)); key.auth_type = "oauth".to_string(); key.oauth_invalid_at_unix_secs = Some(1_710_000_000); - key.oauth_invalid_reason = Some("Codex Token 无效或已过期".to_string()); + key.oauth_invalid_reason = Some("[OAUTH_EXPIRED] session expired".to_string()); key }], )); diff --git a/apps/aether-gateway/src/state/catalog.rs b/apps/aether-gateway/src/state/catalog.rs index 01d8e2934..d6998bb87 100644 --- a/apps/aether-gateway/src/state/catalog.rs +++ b/apps/aether-gateway/src/state/catalog.rs @@ -564,11 +564,17 @@ impl AppState { pub(crate) async fn cleanup_deleted_provider_catalog_refs( &self, provider_id: &str, + provider_deleted: bool, endpoint_ids: &[String], key_ids: &[String], ) -> Result<(), GatewayError> { self.data - .cleanup_deleted_provider_catalog_refs(provider_id, endpoint_ids, key_ids) + .cleanup_deleted_provider_catalog_refs( + provider_id, + provider_deleted, + endpoint_ids, + key_ids, + ) .await .map_err(|err| GatewayError::Internal(err.to_string()))?; for key_id in key_ids { @@ -770,6 +776,25 @@ impl AppState { tasks.insert(task.task_id.clone(), task); } + pub(crate) fn reserve_provider_delete_task( + &self, + task: LocalProviderDeleteTaskState, + ) -> LocalProviderDeleteTaskState { + let mut tasks = self + .provider_delete_tasks + .lock() + .expect("provider delete tasks cache should lock"); + if let Some(existing) = tasks + .values() + .find(|existing| existing.provider_id == task.provider_id && existing.is_active()) + .cloned() + { + return existing; + } + tasks.insert(task.task_id.clone(), task.clone()); + task + } + pub(crate) fn get_provider_delete_task( &self, task_id: &str, diff --git a/apps/aether-gateway/src/state/oauth.rs b/apps/aether-gateway/src/state/oauth.rs index f99fb6d1a..2f620b47d 100644 --- a/apps/aether-gateway/src/state/oauth.rs +++ b/apps/aether-gateway/src/state/oauth.rs @@ -171,7 +171,10 @@ fn oauth_invalid_reason_is_account_block(reason: Option<&str>) -> bool { snapshot.blocked && !matches!( snapshot.code.trim().to_ascii_lowercase().as_str(), - "oauth_token_invalid" | "oauth_expired" | "oauth_refresh_failed" + "oauth_token_invalid" + | "oauth_token_expired" + | "oauth_expired" + | "oauth_refresh_failed" ) } @@ -356,14 +359,16 @@ fn build_oauth_status_snapshot_value(key: &StoredProviderCatalogKey) -> Value { let invalid_reason = trimmed_reason(key.oauth_invalid_reason.as_deref()); if let Some(reason) = tagged_reason(invalid_reason.as_deref(), OAUTH_EXPIRED_PREFIX) { + let (code, label) = + aether_admin::provider::status::oauth_token_snapshot_status_parts(reason.as_str()); return json!({ - "code": "invalid", - "label": "已失效", + "code": code, + "label": label, "reason": reason, "expires_at": expires_at_unix_secs, "invalid_at": invalid_at_unix_secs, "source": "oauth_invalid", - "requires_reauth": true, + "requires_reauth": code == "invalid", "expiring_soon": false, }); } @@ -1291,6 +1296,7 @@ impl AppState { let deleted_key_ids = [key_id.to_string()]; self.cleanup_deleted_provider_catalog_refs( &transport.provider.id, + false, &[], &deleted_key_ids, ) diff --git a/apps/aether-gateway/src/state/types.rs b/apps/aether-gateway/src/state/types.rs index 2a9141410..1106ef8cd 100644 --- a/apps/aether-gateway/src/state/types.rs +++ b/apps/aether-gateway/src/state/types.rs @@ -11,6 +11,12 @@ pub(crate) struct LocalProviderDeleteTaskState { pub message: String, } +impl LocalProviderDeleteTaskState { + pub(crate) fn is_active(&self) -> bool { + matches!(self.status.as_str(), "pending" | "running") + } +} + #[derive(Debug, Clone, PartialEq)] pub(crate) enum LocalMutationOutcome { Applied(T), diff --git a/apps/aether-gateway/src/task_runtime/mod.rs b/apps/aether-gateway/src/task_runtime/mod.rs index 9d6c70d7d..77f8323c3 100644 --- a/apps/aether-gateway/src/task_runtime/mod.rs +++ b/apps/aether-gateway/src/task_runtime/mod.rs @@ -46,6 +46,7 @@ pub(crate) const TASK_KEY_STATS_HOURLY_AGG: &str = "maintenance.stats.hourly.agg pub(crate) const TASK_KEY_USAGE_SYNC_REPORT: &str = "usage.sync.report"; pub(crate) const TASK_KEY_PROVIDER_OAUTH_ACCOUNT_REFRESH: &str = "provider.oauth.account.refresh"; pub(crate) const TASK_KEY_PROVIDER_BALANCE_REFRESH: &str = "provider.ops.balance.refresh"; +const PROVIDER_DELETE_LOCK_TTL_SECS: u64 = 60 * 60 * 6; const RETRY_ONCE: RetryPolicy = RetryPolicy { max_attempts: 1 }; @@ -501,17 +502,23 @@ pub(crate) async fn submit_provider_delete_task( }; let task_id = Uuid::new_v4().simple().to_string()[..16].to_string(); - state.put_provider_delete_task(crate::LocalProviderDeleteTaskState { - task_id: task_id.clone(), - provider_id: provider.id.clone(), - status: "pending".to_string(), - stage: "queued".to_string(), - total_keys: 0, - deleted_keys: 0, - total_endpoints: 0, - deleted_endpoints: 0, - message: "delete task submitted".to_string(), - }); + let reserved = + state + .as_ref() + .reserve_provider_delete_task(crate::LocalProviderDeleteTaskState { + task_id: task_id.clone(), + provider_id: provider.id.clone(), + status: "pending".to_string(), + stage: "queued".to_string(), + total_keys: 0, + deleted_keys: 0, + total_endpoints: 0, + deleted_endpoints: 0, + message: "delete task submitted".to_string(), + }); + if reserved.task_id != task_id { + return Ok(Some(reserved.task_id)); + } let app = state.cloned_app(); let provider_id = provider.id.clone(); @@ -555,7 +562,7 @@ pub(crate) async fn submit_provider_delete_task( spawn_named("task-runtime-provider-delete", async move { let lock_key = format!("task_runtime:lock:{TASK_KEY_PROVIDER_DELETE}:{provider_id}"); - let lock_ttl = std::time::Duration::from_secs(60 * 15); + let lock_ttl = std::time::Duration::from_secs(PROVIDER_DELETE_LOCK_TTL_SECS); let lock = app .runtime_state .lock_try_acquire(&lock_key, app.tunnel.local_instance_id(), lock_ttl) @@ -563,6 +570,17 @@ pub(crate) async fn submit_provider_delete_task( .ok() .flatten(); if lock.is_none() { + app.put_provider_delete_task(crate::LocalProviderDeleteTaskState { + task_id: run_id.clone(), + provider_id: provider_id.clone(), + status: "failed".to_string(), + stage: "skipped".to_string(), + total_keys: 0, + deleted_keys: 0, + total_endpoints: 0, + deleted_endpoints: 0, + message: "provider delete skipped: another node is running this task".to_string(), + }); let _ = update_run_status( &app, &run_id, diff --git a/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/compact.rs b/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/compact.rs index 945eb82c8..26a0d8a86 100644 --- a/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/compact.rs +++ b/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/compact.rs @@ -389,6 +389,9 @@ async fn gateway_executes_openai_responses_compact_openai_family_upstream_stream assert_eq!(response.status(), StatusCode::OK); let response_json: serde_json::Value = response.json().await.expect("body should parse"); + let created_at = response_json["created_at"] + .as_i64() + .expect("created_at should be a unix timestamp"); assert_eq!( response_json, json!({ @@ -396,6 +399,9 @@ async fn gateway_executes_openai_responses_compact_openai_family_upstream_stream "object": "response", "model": "gpt-5", "status": "completed", + "created_at": created_at, + "completed_at": created_at, + "output_text": "Hello Compact", "output": [{ "type": "message", "id": "resp_compact_openai_family_123_msg", diff --git a/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/cross_format.rs b/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/cross_format.rs index f80799e25..f27b32026 100644 --- a/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/cross_format.rs +++ b/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/cross_format.rs @@ -389,6 +389,9 @@ async fn gateway_executes_openai_responses_cross_format_upstream_stream_via_loca assert_eq!(response_status, StatusCode::OK); let response_json: serde_json::Value = serde_json::from_str(&response_body).expect("body should parse"); + let created_at = response_json["created_at"] + .as_i64() + .expect("created_at should be a unix timestamp"); assert_eq!( response_json, json!({ @@ -396,6 +399,9 @@ async fn gateway_executes_openai_responses_cross_format_upstream_stream_via_loca "object": "response", "status": "completed", "model": "gemini-2.5-pro-upstream", + "created_at": created_at, + "completed_at": created_at, + "output_text": "Hello Gemini CLI", "output": [{ "type": "message", "id": "upstream-cli-stream-123_msg", @@ -842,6 +848,9 @@ async fn gateway_executes_openai_responses_cross_format_function_call_upstream_s assert_eq!(response.status(), StatusCode::OK); let response_json: serde_json::Value = response.json().await.expect("body should parse"); + let created_at = response_json["created_at"] + .as_i64() + .expect("created_at should be a unix timestamp"); assert_eq!( response_json, json!({ @@ -849,6 +858,9 @@ async fn gateway_executes_openai_responses_cross_format_function_call_upstream_s "object": "response", "status": "completed", "model": "gemini-2.5-pro-upstream", + "created_at": created_at, + "completed_at": created_at, + "output_text": "Need a tool.", "output": [ { "type": "message", @@ -1412,6 +1424,9 @@ async fn gateway_executes_openai_responses_antigravity_cross_format_upstream_str assert_eq!(response.status(), StatusCode::OK); let response_json: serde_json::Value = response.json().await.expect("body should parse"); + let created_at = response_json["created_at"] + .as_i64() + .expect("created_at should be a unix timestamp"); assert_eq!( response_json, json!({ @@ -1419,6 +1434,9 @@ async fn gateway_executes_openai_responses_antigravity_cross_format_upstream_str "object": "response", "status": "completed", "model": "claude-sonnet-4-5", + "created_at": created_at, + "completed_at": created_at, + "output_text": "Hello Antigravity", "output": [{ "type": "message", "id": "resp-local-stream_msg", diff --git a/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/direct.rs b/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/direct.rs index 935d8f4e9..a4fa874ee 100644 --- a/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/direct.rs +++ b/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/direct.rs @@ -405,6 +405,9 @@ async fn gateway_executes_openai_responses_sync_upstream_stream_via_local_finali assert_eq!(response.status(), StatusCode::OK); let response_json: serde_json::Value = response.json().await.expect("body should parse"); + let created_at = response_json["created_at"] + .as_i64() + .expect("created_at should be a unix timestamp"); assert_eq!( response_json, json!({ @@ -412,6 +415,9 @@ async fn gateway_executes_openai_responses_sync_upstream_stream_via_local_finali "object": "response", "model": "gpt-5-upstream", "status": "completed", + "created_at": created_at, + "completed_at": created_at, + "output_text": "Hello", "output": [{ "type": "message", "id": "resp_stream_001_msg", diff --git a/apps/aether-gateway/src/tests/ai_execute/stream/pii_redaction.rs b/apps/aether-gateway/src/tests/ai_execute/stream/pii_redaction.rs index 96c74dc0f..4eb34747b 100644 --- a/apps/aether-gateway/src/tests/ai_execute/stream/pii_redaction.rs +++ b/apps/aether-gateway/src/tests/ai_execute/stream/pii_redaction.rs @@ -112,7 +112,6 @@ fn auth_repository_with_redaction_feature_settings() -> Arc serde_json::Value { ]) } -fn chat_pii_redaction_feature_settings( - enabled: bool, - inject_model_instruction: bool, -) -> serde_json::Value { +fn chat_pii_redaction_feature_settings(enabled: bool) -> serde_json::Value { json!({ "chat_pii_redaction": { "enabled": enabled, - "inject_model_instruction": inject_model_instruction, } }) } @@ -245,7 +241,6 @@ fn chat_pii_redaction_feature_settings( fn auth_repository_with_redaction_feature_settings( test_id: &str, feature_enabled: bool, - inject_model_instruction: bool, ) -> Arc { let snapshot = auth_snapshot(&format!("api-key-{test_id}"), &format!("user-{test_id}")); let key_hash = hash_api_key(&format!("sk-client-{test_id}")); @@ -257,10 +252,7 @@ fn auth_repository_with_redaction_feature_settings( .with_export_records(vec![auth_export_record( &snapshot, key_hash, - Some(chat_pii_redaction_feature_settings( - feature_enabled, - inject_model_instruction, - )), + Some(chat_pii_redaction_feature_settings(feature_enabled)), )]), ) } @@ -372,8 +364,7 @@ async fn run_sync_redaction_case_with_system_config( }), ); let (provider_url, provider_handle) = start_server(provider_app).await; - let auth_repository = - auth_repository_with_redaction_feature_settings(test_id, feature_enabled, true); + let auth_repository = auth_repository_with_redaction_feature_settings(test_id, feature_enabled); let candidate_selection_repository = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![ candidate_row(test_id), @@ -522,14 +513,9 @@ async fn ai_execute_sync_pii_redaction_round_trip_impl() { assert!(provider_body_text.contains(" Arc Option { Some(timestamp.to_rfc3339()) } +fn admin_usage_response_time_updated_at(item: &StoredRequestUsageAudit) -> Option { + item.response_time_ms?; + if matches!(item.status.as_str(), "pending" | "streaming") + && item.updated_at_unix_secs <= item.created_at_unix_ms + { + return None; + } + unix_secs_to_rfc3339(item.updated_at_unix_secs) +} + pub fn admin_usage_parse_limit(query: Option<&str>) -> Result { match query_param_value(query, "limit") { None => Ok(100), @@ -1196,6 +1206,8 @@ fn admin_usage_active_request_json( "actual_cost": round_to(item.actual_total_cost_usd, 6), "response_time_ms": item.response_time_ms, "first_byte_time_ms": item.first_byte_time_ms, + "updated_at": unix_secs_to_rfc3339(item.updated_at_unix_secs), + "response_time_updated_at": admin_usage_response_time_updated_at(item), "status_code": item.status_code, "error_message": item.error_message, "provider": item.provider_name, @@ -2259,6 +2271,7 @@ pub fn build_admin_usage_active_requests_response( auth_api_key_reader_available: bool, provider_key_names: &BTreeMap, image_progress_by_request_id: &BTreeMap, + state_overrides_by_request_id: &BTreeMap, ) -> Response { let payload: Vec<_> = items .iter() @@ -2266,12 +2279,23 @@ pub fn build_admin_usage_active_requests_response( let provider_key_name = admin_usage_provider_key_name(item, provider_key_names); let api_key_name = admin_usage_api_key_name(item, api_key_names, auth_api_key_reader_available); - admin_usage_active_request_json( + let mut payload = admin_usage_active_request_json( item, api_key_name, provider_key_name, image_progress_by_request_id.get(&item.request_id), - ) + ); + if let (Some(payload), Some(overrides)) = ( + payload.as_object_mut(), + state_overrides_by_request_id + .get(&item.request_id) + .and_then(Value::as_object), + ) { + for (key, value) in overrides { + payload.insert(key.clone(), value.clone()); + } + } + payload }) .collect(); diff --git a/crates/aether-admin/src/provider/quota.rs b/crates/aether-admin/src/provider/quota.rs index 3712f19e1..d5ee67d67 100644 --- a/crates/aether-admin/src/provider/quota.rs +++ b/crates/aether-admin/src/provider/quota.rs @@ -948,10 +948,29 @@ pub fn codex_build_invalid_state( pub fn codex_looks_like_token_invalidated(message: Option<&str>) -> bool { let lowered = message.unwrap_or_default().trim().to_ascii_lowercase(); - lowered.contains("token invalid") + lowered.contains("token_invalidated") + || lowered.contains("authentication token has been invalidated") + || lowered.contains("token has been invalidated") || lowered.contains("token invalidated") - || lowered.contains("session has expired") + || lowered.contains("invalidated") + || lowered.contains("revoked") + || lowered.contains("已撤销") + || lowered.contains("被撤销") + || lowered.contains("撤销") + || lowered.contains("作废") +} + +pub fn codex_looks_like_token_expired(message: Option<&str>) -> bool { + let lowered = message.unwrap_or_default().trim().to_ascii_lowercase(); + lowered.contains("session has expired") || lowered.contains("session expired") + || lowered.contains("access token expired") + || lowered.contains("expired access token") + || lowered.contains("token has expired") + || lowered.contains("token expired") + || lowered.contains("security token included in the request is expired") + || lowered.contains("已过期") + || lowered.contains("过期") } fn codex_looks_like_account_deactivated(message: Option<&str>) -> bool { @@ -980,7 +999,15 @@ pub fn codex_structured_invalid_reason(status_code: u16, upstream_message: Optio } if codex_looks_like_token_invalidated(Some(message)) { let detail = if message.is_empty() { - "Codex Token 无效或已过期" + "Codex Token 已失效" + } else { + message + }; + return format!("{OAUTH_EXPIRED_PREFIX}{detail}"); + } + if codex_looks_like_token_expired(Some(message)) { + let detail = if message.is_empty() { + "Codex Token 已过期" } else { message }; @@ -988,7 +1015,7 @@ pub fn codex_structured_invalid_reason(status_code: u16, upstream_message: Optio } if status_code == 401 { let detail = if message.is_empty() { - "Codex Token 无效或已过期 (401)" + "Codex Token 已过期 (401)" } else { message }; @@ -1021,6 +1048,7 @@ pub fn codex_runtime_invalid_reason( 401 => Some(codex_structured_invalid_reason(401, upstream_message)), 402 => Some(codex_structured_invalid_reason(402, upstream_message)), 403 if codex_looks_like_token_invalidated(upstream_message) + || codex_looks_like_token_expired(upstream_message) || codex_looks_like_account_deactivated(upstream_message) => { Some(codex_structured_invalid_reason(403, upstream_message)) @@ -1848,12 +1876,19 @@ mod tests { } #[test] - fn auto_remove_structured_reason_keeps_oauth_expired_token_invalid() { - assert!(!should_auto_remove_structured_reason(Some( + fn auto_remove_structured_reason_removes_oauth_token_invalidated() { + assert!(should_auto_remove_structured_reason(Some( "[OAUTH_EXPIRED] token invalidated" ))); } + #[test] + fn auto_remove_structured_reason_keeps_oauth_token_expired() { + assert!(!should_auto_remove_structured_reason(Some( + "[OAUTH_EXPIRED] session expired" + ))); + } + #[test] fn auto_remove_refresh_failed_after_access_token_expiry() { let mut key = StoredProviderCatalogKey::new( @@ -1945,7 +1980,7 @@ mod tests { } #[test] - fn oauth_token_invalid_is_not_auto_remove_proof_by_itself() { + fn oauth_token_invalid_is_auto_remove_proof_by_itself() { let mut key = StoredProviderCatalogKey::new( "key-1".to_string(), "provider-1".to_string(), @@ -1958,7 +1993,7 @@ mod tests { key.expires_at_unix_secs = Some(1_000); key.oauth_invalid_reason = Some("oauth_token_invalid".to_string()); - assert!(!super::should_auto_remove_oauth_invalid_key( + assert!(super::should_auto_remove_oauth_invalid_key( &key, Some("oauth_token_invalid"), false, diff --git a/crates/aether-admin/src/provider/status.rs b/crates/aether-admin/src/provider/status.rs index 5f02eb2b9..6ee81a15a 100644 --- a/crates/aether-admin/src/provider/status.rs +++ b/crates/aether-admin/src/provider/status.rs @@ -26,6 +26,10 @@ const ACCOUNT_BLOCK_REASON_KEYWORDS: &[&str] = &[ "账户访问被禁止", "访问受限", "账户访问受限", + "oauth_token_invalid", + "oauth_token_expired", + "token_invalidated", + "session expired", "authentication token has been invalidated", "token has been invalidated", "codex token 无效或已过期", @@ -43,6 +47,7 @@ const AUTO_REMOVABLE_ACCOUNT_STATE_CODES: &[&str] = &[ "account_quarantined", "workspace_deactivated", "account_forbidden", + "oauth_token_invalid", ]; #[derive(Debug, Clone, Default, PartialEq, Eq)] @@ -122,8 +127,79 @@ fn looks_like_account_verification(reason: &str) -> bool { .any(|keyword| lowered.contains(keyword)) } +pub fn oauth_token_reason_is_expired(reason: &str) -> bool { + let lowered = reason.trim().to_ascii_lowercase(); + !lowered.is_empty() + && [ + "oauth_token_expired", + "session has expired", + "session expired", + "access token expired", + "expired access token", + "token expired", + "token has expired", + "security token included in the request is expired", + "已过期", + "过期", + ] + .iter() + .any(|keyword| lowered.contains(keyword)) +} + +pub fn oauth_token_reason_is_hard_invalid(reason: &str) -> bool { + let lowered = reason.trim().to_ascii_lowercase(); + if lowered.is_empty() { + return false; + } + + if [ + "oauth_token_invalid", + "token_invalidated", + "authentication token has been invalidated", + "token has been invalidated", + "token invalidated", + "invalidated", + "revoked", + "已撤销", + "被撤销", + "撤销", + "已作废", + "作废", + "已失效", + "token 失效", + "令牌失效", + ] + .iter() + .any(|keyword| lowered.contains(keyword)) + { + return true; + } + + (lowered.contains("token 无效") || lowered.contains("令牌无效")) + && !oauth_token_reason_is_expired(reason) +} + +pub fn oauth_token_account_status_parts(reason: &str) -> (&'static str, &'static str) { + if oauth_token_reason_is_hard_invalid(reason) { + ("oauth_token_invalid", "Token 失效") + } else { + ("oauth_token_expired", "Token 过期") + } +} + +pub fn oauth_token_snapshot_status_parts(reason: &str) -> (&'static str, &'static str) { + if oauth_token_reason_is_hard_invalid(reason) { + ("invalid", "已失效") + } else { + ("expired", "已过期") + } +} + fn classify_block_reason(reason: &str) -> (&'static str, &'static str) { let lowered = reason.to_ascii_lowercase(); + if oauth_token_reason_is_hard_invalid(reason) || oauth_token_reason_is_expired(reason) { + return oauth_token_account_status_parts(reason); + } if [ "authentication token has been invalidated", "token has been invalidated", @@ -352,17 +428,18 @@ fn resolve_from_oauth_invalid_reason(reason: Option<&str>) -> Option Option { + from_raw(body) +} + +pub fn from_raw(body_json: &Value) -> Option { + 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 { let embedding = request.embedding.as_ref()?; let contents = embedding_input_to_contents(&embedding.input)?; @@ -42,6 +89,67 @@ pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option { Some(Value::Object(output)) } +fn contents_to_embedding_input(contents: &[Value]) -> Option { + if contents.is_empty() { + return None; + } + let parsed = contents + .iter() + .map(embedding_content_from_value) + .collect::>>()?; + 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::>>()?, + )); + } + Some(CanonicalEmbeddingInput::Multimodal(parsed)) +} + +fn embedding_content_from_value(value: &Value) -> Option { + 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::>>()?, + ), + None => None, + }, + }; + (!content.is_empty()).then_some(content) +} + fn embedding_input_to_contents(input: &CanonicalEmbeddingInput) -> Option> { match input { CanonicalEmbeddingInput::String(text) => { diff --git a/crates/aether-ai-formats/src/formats/claude/messages/response.rs b/crates/aether-ai-formats/src/formats/claude/messages/response.rs index a951cfe82..3afc85673 100644 --- a/crates/aether-ai-formats/src/formats/claude/messages/response.rs +++ b/crates/aether-ai-formats/src/formats/claude/messages/response.rs @@ -5,7 +5,8 @@ use serde_json::{json, Value}; use crate::{ formats::context::FormatContext, protocol::canonical::{ - canonical_blocks_to_claude, canonical_stop_reason_to_claude, canonical_usage_to_claude, + 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, @@ -28,6 +29,27 @@ pub fn from_raw(body_json: &Value) -> Option { 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") @@ -49,19 +71,7 @@ pub fn from_raw(body_json: &Value) -> Option { content, stop_reason, usage: claude_usage_to_canonical(body.get("usage")), - extensions: claude_extensions( - body, - &[ - "id", - "type", - "role", - "model", - "content", - "stop_reason", - "stop_sequence", - "usage", - ], - ), + extensions, }) } @@ -86,12 +96,23 @@ pub fn to_raw(canonical: &CanonicalResponse) -> Value { "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() { - object.extend(namespace_extension_object( - &canonical.extensions, - "claude", - object, - )); + let mut extra = namespace_extension_object(&canonical.extensions, "claude", object); + extra.remove("raw_stop_reason"); + extra.remove("raw_stop_sequence"); + object.extend(extra); } response } diff --git a/crates/aether-ai-formats/src/formats/claude/messages/stream.rs b/crates/aether-ai-formats/src/formats/claude/messages/stream.rs index 31b25108c..720f6dd63 100644 --- a/crates/aether-ai-formats/src/formats/claude/messages/stream.rs +++ b/crates/aether-ai-formats/src/formats/claude/messages/stream.rs @@ -322,8 +322,7 @@ impl ClaudeProviderState { let finish_reason = map_claude_stop_reason( delta.get("stop_reason").and_then(Value::as_str), delta.get("stop_reason").and_then(Value::as_str) == Some("tool_use"), - ) - .map(ToOwned::to_owned); + ); let (id, model) = self.identity(report_context); out.push(CanonicalStreamFrame { id, diff --git a/crates/aether-ai-formats/src/formats/context.rs b/crates/aether-ai-formats/src/formats/context.rs index fe5d54cb8..bbef7dcca 100644 --- a/crates/aether-ai-formats/src/formats/context.rs +++ b/crates/aether-ai-formats/src/formats/context.rs @@ -1,5 +1,6 @@ use std::{error::Error, fmt}; +use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; #[derive(Debug, Clone, Default)] @@ -31,6 +32,15 @@ impl FormatContext { 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() @@ -47,13 +57,116 @@ impl FormatContext { } } +#[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, +} + +impl ConversionFieldRecord { + pub fn new( + field: impl Into, + status: ConversionFieldStatus, + detail: Option, + ) -> 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, +} + +impl ConversionReport { + pub fn new(source_format: impl Into, target_format: impl Into) -> Self { + Self { + source_format: source_format.into(), + target_format: target_format.into(), + fields: Vec::new(), + } + } + + pub fn record( + &mut self, + field: impl Into, + status: ConversionFieldStatus, + detail: Option, + ) { + self.fields + .push(ConversionFieldRecord::new(field, status, detail)); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Converted { + 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 }, + 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 { @@ -70,6 +183,49 @@ impl fmt::Display for FormatError { 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}") + } } } } diff --git a/crates/aether-ai-formats/src/formats/conversion/request.rs b/crates/aether-ai-formats/src/formats/conversion/request.rs index c556c963f..f389dafff 100644 --- a/crates/aether-ai-formats/src/formats/conversion/request.rs +++ b/crates/aether-ai-formats/src/formats/conversion/request.rs @@ -723,15 +723,13 @@ mod tests { "file": {"file_data": "data:application/pdf;base64,JVBERi0x"} }), json!({"type": "text", "text": "[File: https://example.com/report.pdf]"}), - json!({ - "type": "text", - "text": "[Claude tool_result document content omitted: text/plain]" - }), + json!({"type": "text", "text": "document body"}), ] ); let block_content_json = Value::Array(block_content.clone()).to_string(); assert!(!block_content_json.contains("\"source\"")); - assert!(!block_content_json.contains("document body")); + assert!(block_content_json.contains("document body")); + assert!(!block_content_json.contains("content omitted")); } #[test] @@ -814,6 +812,34 @@ mod tests { } } + #[test] + fn openai_responses_request_normalizer_strips_content_cache_control() { + let body = json!({ + "model": "gpt-5.1", + "input": [{ + "type": "message", + "role": "user", + "content": [{ + "type": "input_text", + "text": "stable project brief", + "cache_control": {"type": "ephemeral"} + }] + }], + "prompt_cache_key": "cache_123" + }); + + let converted = registry::convert_request( + "openai:responses", + "openai:responses", + &body, + &FormatContext::default(), + ) + .expect("responses request"); + + assert_eq!(converted["prompt_cache_key"], "cache_123"); + assert!(!converted["input"].to_string().contains("cache_control")); + } + #[test] fn claude_output_config_effort_controls_responses_reasoning() { let body = json!({ @@ -932,4 +958,90 @@ mod tests { "data:image/png;base64,AAAA" ); } + + #[test] + fn claude_request_to_responses_rejects_unrepresentable_tool_result_blocks() { + let body = json!({ + "model": "claude-sonnet", + "messages": [{ + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": "toolu_read", + "content": [{ + "type": "image", + "source": { + "type": "unsupported", + "media_type": "image/png", + "data": "AAAA" + } + }] + }] + }], + "max_tokens": 128, + }); + + let error = registry::convert_request( + "claude:messages", + "openai:responses", + &body, + &FormatContext::default(), + ) + .expect_err("unrepresentable Claude tool_result block should fail closed"); + + assert!(matches!( + error, + registry::FormatError::LossyConversionBlocked { + ref source_format, + ref target_format, + ref field, + .. + } if source_format == "claude:messages" + && target_format == "openai:responses" + && field == "messages[].content[].tool_result.content" + )); + } + + #[test] + fn claude_request_to_openai_chat_rejects_unrepresentable_tool_result_blocks() { + let body = json!({ + "model": "claude-sonnet", + "messages": [{ + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": "toolu_read", + "content": [{ + "type": "image", + "source": { + "type": "unsupported", + "media_type": "image/png", + "data": "AAAA" + } + }] + }] + }], + "max_tokens": 128, + }); + + let error = registry::convert_request( + "claude:messages", + "openai:chat", + &body, + &FormatContext::default(), + ) + .expect_err("unrepresentable Claude tool_result block should fail closed for Chat"); + + assert!(matches!( + error, + registry::FormatError::LossyConversionBlocked { + ref source_format, + ref target_format, + ref field, + .. + } if source_format == "claude:messages" + && target_format == "openai:chat" + && field == "messages[].content[].tool_result.content" + )); + } } diff --git a/crates/aether-ai-formats/src/formats/conversion/response.rs b/crates/aether-ai-formats/src/formats/conversion/response.rs index 10d96fad2..3fda75f0f 100644 --- a/crates/aether-ai-formats/src/formats/conversion/response.rs +++ b/crates/aether-ai-formats/src/formats/conversion/response.rs @@ -6,7 +6,10 @@ use serde_json::{json, Value}; -use crate::formats::{context::FormatContext, registry}; +use crate::formats::{ + context::FormatContext, + openai::responses::response::ensure_modern_openai_responses_response_fields, registry, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct OpenAiResponsesResponseUsage { @@ -218,7 +221,7 @@ pub fn build_openai_responses_response_with_content( })); } output.extend(function_calls); - json!({ + let mut response = json!({ "id": response_id, "object": "response", "status": "completed", @@ -229,7 +232,11 @@ pub fn build_openai_responses_response_with_content( "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 { @@ -273,6 +280,26 @@ mod tests { 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] diff --git a/crates/aether-ai-formats/src/formats/doubao/embedding/request.rs b/crates/aether-ai-formats/src/formats/doubao/embedding/request.rs index 3f4228e52..486754cd8 100644 --- a/crates/aether-ai-formats/src/formats/doubao/embedding/request.rs +++ b/crates/aether-ai-formats/src/formats/doubao/embedding/request.rs @@ -5,6 +5,10 @@ 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 { + crate::formats::openai::embedding::request::from_namespace(body, "doubao") +} + pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option { let embedding = request.embedding.as_ref()?; let items = embedding.input.as_string_items()?; diff --git a/crates/aether-ai-formats/src/formats/gemini/embedding/request.rs b/crates/aether-ai-formats/src/formats/gemini/embedding/request.rs index 7d69e5236..087fb52c5 100644 --- a/crates/aether-ai-formats/src/formats/gemini/embedding/request.rs +++ b/crates/aether-ai-formats/src/formats/gemini/embedding/request.rs @@ -1,8 +1,137 @@ use serde_json::{json, Map, Value}; use crate::formats::context::FormatContext; -use crate::formats::openai::embedding::request::mapped_embedding_model; -use crate::protocol::canonical::{CanonicalEmbeddingRequest, CanonicalRequest}; +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 { + from_raw(body) +} + +pub fn from_raw(body_json: &Value) -> Option { + 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, + requests: &[Value], +) -> Option { + if requests.is_empty() { + return None; + } + let items = requests + .iter() + .map(|request| parse_gemini_embedding_request_object(request.as_object()?)) + .collect::>>()?; + 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, + task: Option, +} + +fn parse_gemini_embedding_request_object( + request: &Map, +) -> Option { + 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::>>()? + .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 { let embedding = request.embedding.as_ref()?; diff --git a/crates/aether-ai-formats/src/formats/gemini/generate_content/request.rs b/crates/aether-ai-formats/src/formats/gemini/generate_content/request.rs index 550d53fb3..b5ba03277 100644 --- a/crates/aether-ai-formats/src/formats/gemini/generate_content/request.rs +++ b/crates/aether-ai-formats/src/formats/gemini/generate_content/request.rs @@ -15,7 +15,7 @@ use crate::{ 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_openai_extra_body, + 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, @@ -173,10 +173,6 @@ pub fn from_raw(body_json: &Value, request_path: &str) -> Option 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()), @@ -676,12 +673,21 @@ fn canonical_tool_to_gemini_declaration(tool: &CanonicalToolDefinition) -> Value 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(), - tool.parameters + raw_parameters .clone() + .or_else(|| tool.parameters.clone()) .map(|mut schema| { - clean_gemini_schema(&mut schema); + if raw_parameters.is_none() { + clean_gemini_schema(&mut schema); + } schema }) .unwrap_or_else(|| json!({})), @@ -808,7 +814,7 @@ mod tests { use crate::CanonicalContentBlock; #[test] - fn canonical_tool_result_to_gemini_request_omits_function_response_id() { + 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()); @@ -831,7 +837,7 @@ mod tests { .and_then(Value::as_object) .expect("functionResponse should exist"); - assert!(!function_response.contains_key("id")); + assert_eq!(function_response["id"], "call_1"); assert_eq!(function_response["name"], "lookup"); assert_eq!( function_response["response"], diff --git a/crates/aether-ai-formats/src/formats/gemini/generate_content/response.rs b/crates/aether-ai-formats/src/formats/gemini/generate_content/response.rs index ad85b7632..149a89a99 100644 --- a/crates/aether-ai-formats/src/formats/gemini/generate_content/response.rs +++ b/crates/aether-ai-formats/src/formats/gemini/generate_content/response.rs @@ -55,6 +55,18 @@ pub fn from_raw(body_json: &Value) -> Option { { 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") @@ -64,10 +76,7 @@ pub fn from_raw(body_json: &Value) -> Option { role: CanonicalRole::Assistant, content, stop_reason, - extensions: gemini_extensions( - candidate_object, - &["index", "content", "finishReason", "finish_reason"], - ), + extensions, }); } outputs.retain(gemini_response_output_has_visible_content); @@ -177,7 +186,13 @@ fn canonical_to_gemini_response( }); 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()); } } diff --git a/crates/aether-ai-formats/src/formats/gemini/generate_content/stream.rs b/crates/aether-ai-formats/src/formats/gemini/generate_content/stream.rs index d94786fbb..e643a7d8e 100644 --- a/crates/aether-ai-formats/src/formats/gemini/generate_content/stream.rs +++ b/crates/aether-ai-formats/src/formats/gemini/generate_content/stream.rs @@ -298,13 +298,8 @@ impl GeminiProviderState { candidate_object.get("finishReason").and_then(Value::as_str) { let has_tool_calls = !self.tool_calls.is_empty(); - let mut finish_reason = normalize_openai_finish_reason(match finish_reason { - "STOP" => Some("stop"), - "MAX_TOKENS" => Some("length"), - "SAFETY" | "RECITATION" | "BLOCKLIST" | "PROHIBITED_CONTENT" | "SPII" - | "OTHER" => Some("content_filter"), - other => Some(other), - }); + let mut finish_reason = + normalize_openai_finish_reason(map_gemini_stream_finish_reason(finish_reason)); if has_tool_calls && finish_reason.as_deref().is_none_or(|value| value == "stop") { finish_reason = Some("tool_calls".to_string()); } @@ -343,6 +338,23 @@ impl GeminiProviderState { } } +fn map_gemini_stream_finish_reason(value: &str) -> Option<&str> { + match value { + "STOP" => Some("stop"), + "MAX_TOKENS" => Some("length"), + "SAFETY" + | "RECITATION" + | "LANGUAGE" + | "BLOCKLIST" + | "PROHIBITED_CONTENT" + | "SPII" + | "IMAGE_SAFETY" + | "IMAGE_PROHIBITED_CONTENT" + | "IMAGE_RECITATION" => Some("content_filter"), + other => Some(other), + } +} + #[derive(Default)] struct GeminiClientToolState { call_id: String, diff --git a/crates/aether-ai-formats/src/formats/openai/chat/request.rs b/crates/aether-ai-formats/src/formats/openai/chat/request.rs index e1369f0fe..1cf38324f 100644 --- a/crates/aether-ai-formats/src/formats/openai/chat/request.rs +++ b/crates/aether-ai-formats/src/formats/openai/chat/request.rs @@ -5,10 +5,11 @@ use crate::{ protocol::canonical::{ canonical_extension_object_mut, canonical_message_to_openai_chat_messages, canonical_response_format_to_openai, canonical_tool_choice_to_openai, - canonical_tool_to_openai, 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_to_canonical, openai_tools_to_canonical, write_openai_generation_config, + 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_to_canonical, + openai_tools_to_canonical, write_openai_generation_config, CanonicalContentBlock, CanonicalInstruction, CanonicalRequest, CanonicalRole, CanonicalThinkingConfig, OPENAI_RESPONSES_EXTENSION_NAMESPACE, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE, }, @@ -19,6 +20,9 @@ pub fn from(body: &Value, _ctx: &FormatContext) -> Option { } pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option { + 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) @@ -103,7 +107,6 @@ pub fn from_raw(body_json: &Value) -> Option { "top_p", "top_k", "stop", - "stream", "tools", "tool_choice", "parallel_tool_calls", @@ -220,6 +223,94 @@ pub fn to_raw(canonical: &CanonicalRequest) -> Value { Value::Object(output) } +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) -> 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) -> 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, 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<&'static str> { match value.trim().to_ascii_lowercase().as_str() { "low" => Some("low"), diff --git a/crates/aether-ai-formats/src/formats/openai/chat/response.rs b/crates/aether-ai-formats/src/formats/openai/chat/response.rs index b5029d66f..e137b265e 100644 --- a/crates/aether-ai-formats/src/formats/openai/chat/response.rs +++ b/crates/aether-ai-formats/src/formats/openai/chat/response.rs @@ -70,6 +70,13 @@ pub fn from_raw(body_json: &Value) -> Option { } 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") @@ -79,7 +86,7 @@ pub fn from_raw(body_json: &Value) -> Option { role: CanonicalRole::Assistant, content, stop_reason, - extensions: BTreeMap::new(), + extensions, }); } let first_output = outputs.first()?; @@ -123,10 +130,21 @@ pub fn to_raw(canonical: &CanonicalResponse) -> Value { .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": canonical_stop_reason_to_openai(output.stop_reason.as_ref()), + "finish_reason": finish_reason, }) .as_object() .map(|choice| { diff --git a/crates/aether-ai-formats/src/formats/openai/chat/stream.rs b/crates/aether-ai-formats/src/formats/openai/chat/stream.rs index 231ca34fe..1499da185 100644 --- a/crates/aether-ai-formats/src/formats/openai/chat/stream.rs +++ b/crates/aether-ai-formats/src/formats/openai/chat/stream.rs @@ -2,6 +2,9 @@ use std::collections::{BTreeMap, BTreeSet}; use serde_json::{json, Map, Value}; +use crate::formats::openai::responses::response::{ + ensure_modern_openai_responses_response_fields, openai_responses_current_timestamp, +}; use crate::formats::shared::response::build_generated_tool_call_id; use crate::formats::shared::sse::{encode_done_sse, encode_json_sse}; use crate::formats::shared::stream_core::common::*; @@ -680,6 +683,136 @@ impl OpenAIResponsesProviderState { self.emit_ready_tool_call(report_context, out, index); } + fn emit_custom_tool_call_item( + &mut self, + report_context: &Value, + out: &mut Vec, + item: &Map, + output_index: Option, + ) { + if item.get("type").and_then(Value::as_str) != Some("custom_tool_call") { + return; + } + let name = item + .get("name") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or("custom_tool") + .to_string(); + let arguments = tool_arguments_from_maybe_json_string( + item.get("input").or_else(|| item.get("arguments")), + "input", + ); + self.emit_generic_tool_call_item(report_context, out, item, output_index, name, arguments); + } + + fn emit_shell_tool_call_item( + &mut self, + report_context: &Value, + out: &mut Vec, + item: &Map, + output_index: Option, + ) { + let item_type = item.get("type").and_then(Value::as_str).unwrap_or_default(); + let name = match item_type { + "local_shell_call" => "local_shell", + "shell_call" => "shell", + _ => return, + }; + let arguments = tool_arguments_from_named_fields( + item, + &[ + "action", + "environment", + "status", + "created_by", + "max_output_length", + ], + ); + self.emit_generic_tool_call_item( + report_context, + out, + item, + output_index, + name.to_string(), + arguments, + ); + } + + fn emit_apply_patch_tool_call_item( + &mut self, + report_context: &Value, + out: &mut Vec, + item: &Map, + output_index: Option, + ) { + if item.get("type").and_then(Value::as_str) != Some("apply_patch_call") { + return; + } + let arguments = tool_arguments_from_named_fields(item, &["operation", "status"]); + self.emit_generic_tool_call_item( + report_context, + out, + item, + output_index, + "apply_patch".to_string(), + arguments, + ); + } + + fn emit_computer_tool_call_item( + &mut self, + report_context: &Value, + out: &mut Vec, + item: &Map, + output_index: Option, + ) { + if item.get("type").and_then(Value::as_str) != Some("computer_call") { + return; + } + let arguments = tool_arguments_from_named_fields( + item, + &["action", "actions", "pending_safety_checks", "status"], + ); + self.emit_generic_tool_call_item( + report_context, + out, + item, + output_index, + "computer".to_string(), + arguments, + ); + } + + fn emit_generic_tool_call_item( + &mut self, + report_context: &Value, + out: &mut Vec, + item: &Map, + output_index: Option, + name: String, + arguments: String, + ) { + self.ensure_started(report_context, out); + let key = item + .get("call_id") + .or_else(|| item.get("id")) + .and_then(Value::as_str) + .map(ToOwned::to_owned); + let index = self.tool_index_for_key(key, output_index); + let state = self.tool_calls.entry(index).or_default(); + state.call_id = item + .get("call_id") + .or_else(|| item.get("id")) + .and_then(Value::as_str) + .unwrap_or(state.call_id.as_str()) + .to_string(); + state.name = name; + Self::merge_tool_call_arguments(state, &arguments); + self.emit_ready_tool_call(report_context, out, index); + } + fn emit_missing_tool_result( &mut self, report_context: &Value, @@ -753,6 +886,54 @@ impl OpenAIResponsesProviderState { self.emit_missing_tool_result(report_context, out, index, tool_use_id, name, &content); } + fn emit_generic_tool_result_item( + &mut self, + report_context: &Value, + out: &mut Vec, + item: &Map, + output_index: Option, + ) { + let item_type = item.get("type").and_then(Value::as_str).unwrap_or_default(); + let is_supported_result = matches!( + item_type, + "custom_tool_call_output" + | "local_shell_call_output" + | "shell_call_output" + | "apply_patch_call_output" + | "computer_call_output" + ); + if !is_supported_result { + return; + } + let tool_use_id = item + .get("call_id") + .or_else(|| item.get("tool_call_id")) + .or_else(|| item.get("id")) + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()) + .unwrap_or("call_auto_0") + .to_string(); + let index = + self.tool_index_for_key(Some(format!("{item_type}:{tool_use_id}")), output_index); + let content = openai_tool_result_content_from_value( + item.get("output") + .or_else(|| item.get("content")) + .or_else(|| item.get("delta")), + ); + let name = match item_type { + "local_shell_call_output" => Some("local_shell".to_string()), + "shell_call_output" => Some("shell".to_string()), + "apply_patch_call_output" => Some("apply_patch".to_string()), + "computer_call_output" => Some("computer".to_string()), + _ => item + .get("name") + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()) + .map(ToOwned::to_owned), + }; + self.emit_missing_tool_result(report_context, out, index, tool_use_id, name, &content); + } + fn emit_message_item( &mut self, report_context: &Value, @@ -867,6 +1048,79 @@ impl OpenAIResponsesProviderState { }); } + fn emit_output_item( + &mut self, + report_context: &Value, + out: &mut Vec, + item: &Map, + output_index: Option, + final_item: bool, + ) { + match item.get("type").and_then(Value::as_str).unwrap_or_default() { + "function_call" => self.emit_tool_call_item(report_context, out, item, output_index), + "function_call_output" => { + self.emit_tool_result_item(report_context, out, item, output_index); + } + "custom_tool_call" => { + self.emit_custom_tool_call_item(report_context, out, item, output_index); + } + "local_shell_call" | "shell_call" => { + self.emit_shell_tool_call_item(report_context, out, item, output_index); + } + "apply_patch_call" => { + self.emit_apply_patch_tool_call_item(report_context, out, item, output_index); + } + "computer_call" => { + self.emit_computer_tool_call_item(report_context, out, item, output_index); + } + "custom_tool_call_output" + | "local_shell_call_output" + | "shell_call_output" + | "apply_patch_call_output" + | "computer_call_output" => { + self.emit_generic_tool_result_item(report_context, out, item, output_index); + } + "message" => self.emit_message_item(report_context, out, item, output_index), + "reasoning" if final_item => self.emit_reasoning_item(report_context, out, item), + "reasoning" => self.ensure_started(report_context, out), + "image_generation_call" => { + self.emit_image_generation_item( + report_context, + out, + item, + output_index, + final_item, + ); + } + "web_search_call" | "file_search_call" | "code_interpreter_call" | "mcp_call" => { + if !final_item { + self.ensure_started(report_context, out); + } + } + _ => out.push(self.unknown_frame(report_context, Value::Object(item.clone()))), + } + } + + fn emit_response_output_items( + &mut self, + report_context: &Value, + out: &mut Vec, + response: &Map, + ) { + for (output_index, raw_item) in response + .get("output") + .and_then(Value::as_array) + .into_iter() + .flatten() + .enumerate() + { + let Some(item) = raw_item.as_object() else { + continue; + }; + self.emit_output_item(report_context, out, item, Some(output_index), true); + } + } + pub fn push_line( &mut self, report_context: &Value, @@ -958,7 +1212,55 @@ impl OpenAIResponsesProviderState { self.emit_missing_text(report_context, &mut out, key, text); } } - "response.reasoning_summary_text.delta" => { + "response.refusal.delta" => { + let piece = value + .get("delta") + .and_then(Value::as_str) + .unwrap_or_default(); + if !piece.is_empty() { + let key = Self::text_part_key_from_event(&value); + self.emit_text_delta(report_context, &mut out, key, piece); + } + } + "response.refusal.done" => { + let refusal = value + .get("refusal") + .and_then(Value::as_str) + .or_else(|| { + value + .get("part") + .and_then(Value::as_object) + .and_then(|part| part.get("refusal")) + .and_then(Value::as_str) + }) + .unwrap_or_default(); + if !refusal.is_empty() { + let key = Self::text_part_key_from_event(&value); + self.emit_missing_text(report_context, &mut out, key, refusal); + } + } + "response.audio.transcript.delta" => { + let piece = value + .get("delta") + .and_then(Value::as_str) + .unwrap_or_default(); + if !piece.is_empty() { + let key = Self::text_part_key_from_event(&value); + self.emit_text_delta(report_context, &mut out, key, piece); + } + } + "response.audio.transcript.done" => { + let transcript = value + .get("transcript") + .and_then(Value::as_str) + .or_else(|| value.get("text").and_then(Value::as_str)) + .unwrap_or_default(); + if !transcript.is_empty() { + let key = Self::text_part_key_from_event(&value); + self.emit_missing_text(report_context, &mut out, key, transcript); + } + } + "response.reasoning_text.delta" | "response.reasoning_summary_text.delta" => { let piece = value .get("delta") .and_then(Value::as_str) @@ -983,7 +1285,7 @@ impl OpenAIResponsesProviderState { }); } } - "response.reasoning_summary_text.done" => { + "response.reasoning_text.done" | "response.reasoning_summary_text.done" => { let text = value .get("text") .and_then(Value::as_str) @@ -1024,32 +1326,70 @@ impl OpenAIResponsesProviderState { .get("output_index") .and_then(Value::as_u64) .map(|value| value as usize); - match item.get("type").and_then(Value::as_str).unwrap_or_default() { - "function_call" => { - self.emit_tool_call_item(report_context, &mut out, item, output_index); - } - "function_call_output" => { - self.emit_tool_result_item(report_context, &mut out, item, output_index); - } - "message" => { - self.emit_message_item(report_context, &mut out, item, output_index); - } - "reasoning" => { - self.ensure_started(report_context, &mut out); - } - "image_generation_call" => { - self.emit_image_generation_item( - report_context, - &mut out, - item, - output_index, - false, - ); - } - _ => { - out.push(self.unknown_frame(report_context, Value::Object(item.clone()))); - } + self.emit_output_item(report_context, &mut out, item, output_index, false); + } + "response.custom_tool_call_input.delta" => { + let delta = value + .get("delta") + .and_then(Value::as_str) + .unwrap_or_default(); + if delta.is_empty() { + return Ok(out); } + self.ensure_started(report_context, &mut out); + let key = value + .get("item_id") + .or_else(|| value.get("call_id")) + .or_else(|| value.get("id")) + .and_then(Value::as_str) + .map(ToOwned::to_owned); + let output_index = value + .get("output_index") + .and_then(Value::as_u64) + .map(|value| value as usize); + let index = self.tool_index_for_key(key, output_index); + let state = self.tool_calls.entry(index).or_default(); + if state.name.is_empty() { + state.name = value + .get("name") + .and_then(Value::as_str) + .unwrap_or("custom_tool") + .to_string(); + } + state.arguments.push_str(delta); + self.emit_ready_tool_call(report_context, &mut out, index); + } + "response.custom_tool_call_input.done" => { + let input = value + .get("input") + .and_then(Value::as_str) + .unwrap_or_default(); + self.ensure_started(report_context, &mut out); + let key = value + .get("item_id") + .or_else(|| value.get("call_id")) + .or_else(|| value.get("id")) + .and_then(Value::as_str) + .map(ToOwned::to_owned); + let output_index = value + .get("output_index") + .and_then(Value::as_u64) + .map(|value| value as usize); + let index = self.tool_index_for_key(key, output_index); + let state = self.tool_calls.entry(index).or_default(); + if state.name.is_empty() { + state.name = value + .get("name") + .and_then(Value::as_str) + .unwrap_or("custom_tool") + .to_string(); + } + let arguments = tool_arguments_from_maybe_json_string( + Some(&Value::String(input.to_string())), + "input", + ); + Self::merge_tool_call_arguments(state, &arguments); + self.emit_ready_tool_call(report_context, &mut out, index); } "response.function_call_arguments.delta" => { let delta = value @@ -1196,32 +1536,28 @@ impl OpenAIResponsesProviderState { .get("output_index") .and_then(Value::as_u64) .map(|value| value as usize); - match item.get("type").and_then(Value::as_str).unwrap_or_default() { - "function_call" => { - self.emit_tool_call_item(report_context, &mut out, item, output_index); - } - "function_call_output" => { - self.emit_tool_result_item(report_context, &mut out, item, output_index); - } - "message" => { - self.emit_message_item(report_context, &mut out, item, output_index); - } - "reasoning" => { - self.emit_reasoning_item(report_context, &mut out, item); - } - "image_generation_call" => { - self.emit_image_generation_item( - report_context, - &mut out, - item, - output_index, - true, - ); - } - _ => { - out.push(self.unknown_frame(report_context, Value::Object(item.clone()))); - } - } + self.emit_output_item(report_context, &mut out, item, output_index, true); + } + "response.incomplete" => { + let Some(response) = value.get("response").and_then(Value::as_object) else { + return Ok(out); + }; + self.ensure_started(report_context, &mut out); + let (id, model) = self.identity(report_context); + self.emit_response_output_items(report_context, &mut out, response); + + out.push(CanonicalStreamFrame { + id, + model, + event: CanonicalStreamEvent::Finish { + finish_reason: Some(openai_responses_incomplete_finish_reason(&value)), + usage: canonical_usage_from_openai_usage(response.get("usage")), + }, + }); + self.finished = true; + } + event_type if openai_responses_stream_event_is_known_noop(event_type) => { + self.ensure_started(report_context, &mut out); } event_type if openai_stream_payload_is_terminal_error(&value) => { self.finished = true; @@ -1240,67 +1576,13 @@ impl OpenAIResponsesProviderState { } out.push(self.unknown_frame(report_context, payload)); } - "response.completed" => { + "response.completed" | "response.done" => { let Some(response) = value.get("response").and_then(Value::as_object) else { return Ok(out); }; self.ensure_started(report_context, &mut out); let (id, model) = self.identity(report_context); - - for (output_index, raw_item) in response - .get("output") - .and_then(Value::as_array) - .into_iter() - .flatten() - .enumerate() - { - let Some(item) = raw_item.as_object() else { - continue; - }; - match item.get("type").and_then(Value::as_str).unwrap_or_default() { - "message" => { - self.emit_message_item( - report_context, - &mut out, - item, - Some(output_index), - ); - } - "function_call" => { - self.emit_tool_call_item( - report_context, - &mut out, - item, - Some(output_index), - ); - } - "function_call_output" => { - self.emit_tool_result_item( - report_context, - &mut out, - item, - Some(output_index), - ); - } - "reasoning" => { - self.emit_reasoning_item(report_context, &mut out, item); - } - "image_generation_call" => { - self.emit_image_generation_item( - report_context, - &mut out, - item, - Some(output_index), - true, - ); - } - _ => { - out.push( - self.unknown_frame(report_context, Value::Object(item.clone())), - ); - } - } - } + self.emit_response_output_items(report_context, &mut out, response); let finish_reason = if self.tool_calls.is_empty() { Some("stop".to_string()) @@ -1399,6 +1681,7 @@ fn web_search_query_from_arguments(arguments: &str) -> String { pub struct OpenAIResponsesClientEmitter { response_id: Option, model: Option, + created_at: Option, message_item_id: Option, reasoning_item_id: Option, started: bool, @@ -1744,13 +2027,28 @@ impl OpenAIResponsesClientEmitter { } fn in_progress_response(&self) -> Value { - json!({ + let mut response = json!({ "id": self.response_id(), "object": "response", "model": self.model(), "status": "in_progress", "output": [], - }) + }); + if let (Some(created_at), Some(response_object)) = + (self.created_at, response.as_object_mut()) + { + response_object.insert("created_at".to_string(), Value::from(created_at)); + } + response + } + + fn ensure_created_at(&mut self) -> i64 { + if let Some(created_at) = self.created_at { + return created_at; + } + let created_at = openai_responses_current_timestamp(); + self.created_at = Some(created_at); + created_at } fn allocate_output_index(&mut self) -> usize { @@ -1787,6 +2085,7 @@ impl OpenAIResponsesClientEmitter { if self.started { return Ok(Vec::new()); } + self.ensure_created_at(); self.started = true; let mut out = self.encode_response_event( "response.created", @@ -2118,6 +2417,7 @@ impl OpenAIResponsesClientEmitter { "output_index": output_index, "item_id": item_id.clone(), "call_id": item_id.clone(), + "name": name, "arguments": state.arguments.as_str(), }), )?); @@ -2217,7 +2517,12 @@ impl OpenAIResponsesClientEmitter { Ok(out) } - fn completed_response(&self, usage: CanonicalUsage) -> Value { + fn terminal_response( + &self, + usage: CanonicalUsage, + status: &str, + incomplete_reason: Option<&str>, + ) -> Value { let mut ordered_output = Vec::new(); let summary = if self.reasoning_summary_parts.is_empty() { if self.reasoning.trim().is_empty() { @@ -2336,17 +2641,35 @@ impl OpenAIResponsesClientEmitter { } ordered_output.sort_by_key(|(output_index, _)| *output_index); - json!({ + let mut response = json!({ "id": self.response_id(), "object": "response", - "status": "completed", + "status": status, "model": self.model(), "output": ordered_output .into_iter() .map(|(_, item)| item) .collect::>(), "usage": openai_responses_usage_from_usage(&usage), - }) + }); + if let Some(reason) = incomplete_reason { + response["incomplete_details"] = json!({ "reason": reason }); + } + if let Some(response_object) = response.as_object_mut() { + if let Some(created_at) = self.created_at { + response_object.insert("created_at".to_string(), Value::from(created_at)); + } + ensure_modern_openai_responses_response_fields(response_object); + } + response + } + + fn completed_response(&self, usage: CanonicalUsage) -> Value { + self.terminal_response(usage, "completed", None) + } + + fn incomplete_response(&self, usage: CanonicalUsage, reason: &str) -> Value { + self.terminal_response(usage, "incomplete", Some(reason)) } pub fn emit(&mut self, frame: CanonicalStreamFrame) -> Result, AiSurfaceFinalizeError> { @@ -2619,7 +2942,10 @@ impl OpenAIResponsesClientEmitter { self.encode_response_event(event.as_str(), payload) } CanonicalStreamEvent::UnknownEvent(_) => Ok(Vec::new()), - CanonicalStreamEvent::Finish { usage, .. } => { + CanonicalStreamEvent::Finish { + finish_reason, + usage, + } => { if self.finished { return Ok(Vec::new()); } @@ -2629,11 +2955,22 @@ impl OpenAIResponsesClientEmitter { out.extend(self.finish_tool_items()?); out.extend(self.finish_tool_result_items()?); let usage = usage.unwrap_or_default(); + let (event_type, response) = match finish_reason.as_deref() { + Some("length") => ( + "response.incomplete", + self.incomplete_response(usage, "max_output_tokens"), + ), + Some("content_filter") => ( + "response.incomplete", + self.incomplete_response(usage, "content_filter"), + ), + _ => ("response.completed", self.completed_response(usage)), + }; out.extend(self.encode_response_event( - "response.completed", + event_type, json!({ - "type": "response.completed", - "response": self.completed_response(usage), + "type": event_type, + "response": response, }), )?); self.finished = true; @@ -2706,6 +3043,95 @@ fn openai_tool_result_content_from_value(value: Option<&Value>) -> String { } } +fn tool_arguments_from_maybe_json_string(value: Option<&Value>, fallback_key: &str) -> String { + match value { + Some(Value::String(text)) => { + let trimmed = text.trim(); + if trimmed.is_empty() { + return String::new(); + } + match serde_json::from_str::(trimmed) { + Ok(Value::Object(_)) => trimmed.to_string(), + Ok(parsed) => single_field_tool_arguments(fallback_key, parsed), + Err(_) => single_field_tool_arguments(fallback_key, Value::String(text.clone())), + } + } + Some(value @ Value::Object(_)) => value.to_string(), + Some(Value::Null) | None => String::new(), + Some(value) => single_field_tool_arguments(fallback_key, value.clone()), + } +} + +fn single_field_tool_arguments(key: &str, value: Value) -> String { + let mut arguments = Map::new(); + arguments.insert(key.to_string(), value); + Value::Object(arguments).to_string() +} + +fn tool_arguments_from_named_fields(item: &Map, field_names: &[&str]) -> String { + let mut arguments = Map::new(); + for field_name in field_names { + if let Some(value) = item.get(*field_name) { + arguments.insert((*field_name).to_string(), value.clone()); + } + } + if arguments.is_empty() { + String::new() + } else { + Value::Object(arguments).to_string() + } +} + +fn openai_responses_stream_event_is_known_noop(event_type: &str) -> bool { + matches!( + event_type, + "response.queued" + | "response.output_text.annotation.added" + | "response.audio.delta" + | "response.audio.done" + | "response.code_interpreter_call.in_progress" + | "response.code_interpreter_call.interpreting" + | "response.code_interpreter_call.completed" + | "response.code_interpreter_call_code.delta" + | "response.code_interpreter_call_code.done" + | "response.file_search_call.in_progress" + | "response.file_search_call.searching" + | "response.file_search_call.completed" + | "response.image_generation_call.in_progress" + | "response.image_generation_call.generating" + | "response.image_generation_call.partial_image" + | "response.image_generation_call.completed" + | "response.mcp_call.in_progress" + | "response.mcp_call.completed" + | "response.mcp_call.failed" + | "response.mcp_call_arguments.delta" + | "response.mcp_call_arguments.done" + | "response.mcp_list_tools.in_progress" + | "response.mcp_list_tools.completed" + | "response.mcp_list_tools.failed" + | "response.web_search_call.in_progress" + | "response.web_search_call.searching" + | "response.web_search_call.completed" + ) +} + +fn openai_responses_incomplete_finish_reason(payload: &Value) -> String { + let reason = payload + .get("response") + .and_then(Value::as_object) + .and_then(|response| response.get("incomplete_details")) + .and_then(Value::as_object) + .and_then(|details| details.get("reason")) + .and_then(Value::as_str) + .unwrap_or_default(); + match reason { + "content_filter" => "content_filter", + "tool_calls" | "function_call" => "tool_calls", + _ => "length", + } + .to_string() +} + #[cfg(test)] mod tests { use super::*; @@ -3043,6 +3469,9 @@ mod tests { assert!(sse.contains("\"response_id\":\"resp_stream_123\"")); assert!(sse.contains("\"item_id\":\"resp_stream_123_msg\"")); assert!(sse.contains("\"text\":\"Hello\"")); + assert!(sse.contains("\"output_text\":\"Hello\"")); + assert!(sse.contains("\"created_at\":")); + assert!(sse.contains("\"completed_at\":")); assert_eq!(response_sequence_numbers(&sse), (1..=9).collect::>()); } @@ -3226,6 +3655,53 @@ mod tests { ))); } + #[test] + fn openai_responses_provider_state_accepts_refusal_events() { + let mut state = OpenAIResponsesProviderState::default(); + let report_context = json!({}); + let mut frames = Vec::new(); + + frames.extend( + state + .push_line( + &report_context, + data_line(json!({ + "type": "response.refusal.delta", + "response_id": "resp_refusal_123", + "output_index": 0, + "item_id": "msg_refusal_123", + "content_index": 0, + "delta": "I can't", + })), + ) + .expect("refusal delta should parse"), + ); + frames.extend( + state + .push_line( + &report_context, + data_line(json!({ + "type": "response.refusal.done", + "response_id": "resp_refusal_123", + "output_index": 0, + "item_id": "msg_refusal_123", + "content_index": 0, + "refusal": "I can't help with that.", + })), + ) + .expect("refusal done should parse"), + ); + + assert!(frames.iter().any(|frame| matches!( + &frame.event, + CanonicalStreamEvent::TextDelta(text) if text == "I can't" + ))); + assert!(frames.iter().any(|frame| matches!( + &frame.event, + CanonicalStreamEvent::TextDelta(text) if text == " help with that." + ))); + } + #[test] fn openai_responses_provider_state_does_not_duplicate_text_snapshot_deltas() { let mut state = OpenAIResponsesProviderState::default(); diff --git a/crates/aether-ai-formats/src/formats/openai/responses/codex.rs b/crates/aether-ai-formats/src/formats/openai/responses/codex.rs index 1b4a9fad5..d50a165bc 100644 --- a/crates/aether-ai-formats/src/formats/openai/responses/codex.rs +++ b/crates/aether-ai-formats/src/formats/openai/responses/codex.rs @@ -738,6 +738,34 @@ fn strip_codex_hosted_tool_choice_name_for_backend( } } +fn wrap_codex_responses_string_input_for_backend( + body_object: &mut serde_json::Map, + provider_api_format: &str, +) { + if !aether_ai_formats::is_openai_responses_family_format(provider_api_format) { + return; + } + let Some(text) = body_object + .get("input") + .and_then(Value::as_str) + .map(ToOwned::to_owned) + else { + return; + }; + + body_object.insert( + "input".to_string(), + json!([{ + "type": "message", + "role": "user", + "content": [{ + "type": "input_text", + "text": text, + }], + }]), + ); +} + pub fn apply_codex_openai_responses_special_body_edits( provider_request_body: &mut Value, provider_type: &str, @@ -760,6 +788,7 @@ pub fn apply_codex_openai_responses_special_body_edits( return; }; + wrap_codex_responses_string_input_for_backend(body_object, provider_api_format); for field in CODEX_OPENAI_RESPONSES_UNSUPPORTED_BODY_FIELDS { if !body_rules_handle_path(body_rules, field) { body_object.remove(*field); @@ -985,6 +1014,34 @@ mod tests { assert_eq!(provider_request_body["parallel_tool_calls"], json!(false)); } + #[test] + fn codex_responses_body_edits_wrap_string_input_for_backend() { + let mut provider_request_body = json!({ + "input": "hello", + "model": "gpt-5.4" + }); + + apply_codex_openai_responses_special_body_edits( + &mut provider_request_body, + "codex", + "openai:responses", + None, + None, + ); + + assert_eq!( + provider_request_body["input"], + json!([{ + "type": "message", + "role": "user", + "content": [{ + "type": "input_text", + "text": "hello" + }] + }]) + ); + } + #[test] fn codex_responses_body_edits_preserve_function_tools_for_codex_backend() { let mut provider_request_body = json!({ diff --git a/crates/aether-ai-formats/src/formats/openai/responses/request.rs b/crates/aether-ai-formats/src/formats/openai/responses/request.rs index 9fed5a930..42973b601 100644 --- a/crates/aether-ai-formats/src/formats/openai/responses/request.rs +++ b/crates/aether-ai-formats/src/formats/openai/responses/request.rs @@ -1,10 +1,12 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, VecDeque}; use serde_json::{json, Map, Value}; use crate::{ formats::context::FormatContext, - formats::openai::shared::map_thinking_budget_to_openai_reasoning_effort, + formats::openai::shared::{ + map_thinking_budget_to_openai_reasoning_effort, OpenAiResponsesReasoningEffort, + }, protocol::canonical::{ canonical_response_format_to_openai, canonicalize_tool_arguments, is_claude_messages_request, is_claude_system_instruction, is_claude_thinking_block, @@ -182,6 +184,10 @@ pub fn to_raw( output.insert("reasoning".to_string(), reasoning); } + output.extend(chat_openai_extension_object_to_responses( + &canonical.extensions, + &output, + )); output.extend(namespace_extension_object( &canonical.extensions, OPENAI_RESPONSES_EXTENSION_NAMESPACE, @@ -200,6 +206,33 @@ pub fn to_raw( Some(Value::Object(output)) } +fn chat_openai_extension_object_to_responses( + extensions: &BTreeMap, + existing: &Map, +) -> Map { + const RESPONSES_COMPATIBLE_CHAT_FIELDS: &[&str] = &[ + "stream", + "store", + "service_tier", + "safety_identifier", + "prompt_cache_key", + ]; + extensions + .get("openai") + .and_then(Value::as_object) + .map(|object| { + object + .iter() + .filter(|(key, _)| { + RESPONSES_COMPATIBLE_CHAT_FIELDS.contains(&key.as_str()) + && !existing.contains_key(*key) + }) + .map(|(key, value)| (key.clone(), value.clone())) + .collect() + }) + .unwrap_or_default() +} + fn canonical_instructions_to_responses(canonical: &CanonicalRequest) -> Option { let text = canonical .instructions @@ -264,6 +297,8 @@ fn claude_system_instruction_to_responses_part( fn canonical_messages_to_responses_input(canonical: &CanonicalRequest) -> Option> { let mut input = Vec::new(); + let mut next_generated_tool_call_index = 0usize; + let mut pending_tool_call_ids = VecDeque::new(); for message in &canonical.messages { let role = match message.role { CanonicalRole::Assistant => "assistant", @@ -282,10 +317,13 @@ fn canonical_messages_to_responses_input(canonical: &CanonicalRequest) -> Option } => { flush_responses_message(&mut input, role, &mut content); saw_tool_item = true; + let call_id = responses_tool_call_id(id, &mut next_generated_tool_call_index); + let tool_name = responses_tool_name(name); + pending_tool_call_ids.push_back(call_id.clone()); input.push(json!({ "type": "function_call", - "call_id": id, - "name": name, + "call_id": call_id, + "name": tool_name, "arguments": canonicalize_tool_arguments(arguments), })); } @@ -302,10 +340,12 @@ fn canonical_messages_to_responses_input(canonical: &CanonicalRequest) -> Option output.as_ref(), content_text.as_deref(), extensions, - ); + )?; + let call_id = + responses_tool_result_call_id(tool_use_id, &mut pending_tool_call_ids)?; input.push(json!({ "type": "function_call_output", - "call_id": tool_use_id, + "call_id": call_id, "output": tool_output, })); if !extra_user_content.is_empty() { @@ -360,6 +400,42 @@ fn canonical_messages_to_responses_input(canonical: &CanonicalRequest) -> Option Some(input) } +fn responses_tool_call_id(id: &str, next_generated_tool_call_index: &mut usize) -> String { + let trimmed = id.trim(); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + let generated = format!("call_auto_{next_generated_tool_call_index}"); + *next_generated_tool_call_index += 1; + generated +} + +fn responses_tool_result_call_id( + id: &str, + pending_tool_call_ids: &mut VecDeque, +) -> Option { + let trimmed = id.trim(); + if !trimmed.is_empty() { + if let Some(position) = pending_tool_call_ids + .iter() + .position(|pending_id| pending_id == trimmed) + { + pending_tool_call_ids.remove(position); + } + return Some(trimmed.to_string()); + } + pending_tool_call_ids.pop_front() +} + +fn responses_tool_name(name: &str) -> String { + let trimmed = name.trim(); + if trimmed.is_empty() { + "unknown".to_string() + } else { + trimmed.to_string() + } +} + fn responses_max_output_tokens(canonical: &CanonicalRequest) -> Option { canonical.generation.max_tokens.map(|max_tokens| { if is_claude_messages_request(&canonical.extensions) && max_tokens < 128 { @@ -582,7 +658,7 @@ fn canonical_reasoning_config_to_responses(canonical: &CanonicalRequest) -> Opti .and_then(|value| value.get("output_config")) .and_then(|value| value.get("effort")) .and_then(Value::as_str) - .map(openai_responses_reasoning_effort) + .and_then(openai_responses_reasoning_effort) .unwrap_or("medium"); object .entry("effort".to_string()) @@ -608,10 +684,11 @@ fn reasoning_config_to_responses(thinking: &CanonicalThinkingConfig) -> Option Option &str { +fn openai_responses_reasoning_effort(effort: &str) -> Option<&'static str> { match effort.trim().to_ascii_lowercase().as_str() { - "xhigh" | "max" => "xhigh", - "low" => "low", - "medium" => "medium", - "high" => "high", - _ => effort, + "max" => Some("xhigh"), + value => { + OpenAiResponsesReasoningEffort::parse(value).map(OpenAiResponsesReasoningEffort::as_str) + } } } @@ -694,6 +770,9 @@ fn canonical_tool_to_responses(tool: &CanonicalToolDefinition) -> Value { "parameters".to_string(), responses_tool_parameters_schema(tool.parameters.as_ref()), ); + if let Some(strict) = tool.strict { + out.insert("strict".to_string(), Value::Bool(strict)); + } out.extend(namespace_extension_object( &tool.extensions, OPENAI_RESPONSES_EXTENSION_NAMESPACE, @@ -737,16 +816,16 @@ fn responses_tool_result_payload( output: Option<&Value>, content_text: Option<&str>, extensions: &BTreeMap, -) -> (Value, Vec) { +) -> Option<(Value, Vec)> { if is_claude_tool_result(extensions) { if let Some(Value::Array(parts)) = output { return claude_tool_result_parts_to_responses_payload(parts); } } - ( + Some(( responses_tool_result_output(output, content_text), Vec::new(), - ) + )) } fn responses_tool_result_output(output: Option<&Value>, content_text: Option<&str>) -> Value { @@ -759,15 +838,64 @@ fn responses_tool_result_output(output: Option<&Value>, content_text: Option<&st Value::String(non_empty_responses_tool_output(&text)) } -fn claude_tool_result_parts_to_responses_payload(parts: &[Value]) -> (Value, Vec) { +pub(crate) fn claude_tool_result_parts_are_openai_responses_representable(parts: &[Value]) -> bool { + parts + .iter() + .all(claude_tool_result_part_is_openai_responses_representable) +} + +fn claude_tool_result_part_is_openai_responses_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_responses_representable(part_object), + "document" | "file" => claude_document_block_is_openai_responses_representable(part_object), + _ => false, + } +} + +fn claude_image_block_is_openai_responses_representable(block: &Map) -> 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" => claude_source_str(source, "data").is_some(), + "url" => claude_source_str(source, "url").is_some(), + _ => false, + } +} + +fn claude_document_block_is_openai_responses_representable(block: &Map) -> 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" | "text" => claude_source_str(source, "data").is_some(), + "url" => claude_source_str(source, "url").is_some(), + _ => false, + } +} + +fn claude_tool_result_parts_to_responses_payload(parts: &[Value]) -> Option<(Value, Vec)> { let mut output_texts = Vec::new(); let mut extra_user_content = Vec::new(); for part in parts { - let Some(part_object) = part.as_object() else { - output_texts.push("[Claude tool_result non-text content omitted]".to_string()); - continue; - }; + let part_object = part.as_object()?; match part_object .get("type") .and_then(Value::as_str) @@ -784,27 +912,31 @@ fn claude_tool_result_parts_to_responses_payload(parts: &[Value]) -> (Value, Vec if let Some(part) = claude_image_block_to_responses_input_part(part_object) { extra_user_content.push(part); } else { - output_texts.push(claude_tool_result_media_summary("image", part_object)); + return None; } } "document" | "file" => { - if let Some(part) = claude_document_block_to_responses_input_part(part_object) { + if let Some(text) = claude_text_document_block_to_responses_output_text(part_object) + { + if !text.is_empty() { + output_texts.push(text.to_string()); + } + } else if let Some(part) = + claude_document_block_to_responses_input_part(part_object) + { extra_user_content.push(part); } else { - output_texts.push(claude_tool_result_media_summary("document", part_object)); + return None; } } - "" => output_texts.push("[Claude tool_result object content omitted]".to_string()), - raw_type => { - output_texts.push(format!("[Claude tool_result {raw_type} content omitted]")) - } + _ => return None, } } - ( + Some(( Value::String(non_empty_responses_tool_output(&output_texts.join("\n\n"))), extra_user_content, - ) + )) } fn claude_image_block_to_responses_input_part(block: &Map) -> Option { @@ -863,16 +995,15 @@ fn claude_document_block_to_responses_input_part(block: &Map) -> Some(Value::Object(part)) } -fn claude_tool_result_media_summary(kind: &str, block: &Map) -> String { - let media_type = block - .get("source") - .and_then(Value::as_object) - .and_then(claude_source_media_type); - match media_type { - Some(media_type) if !media_type.trim().is_empty() => { - format!("[Claude tool_result {kind} content omitted: {media_type}]") - } - _ => format!("[Claude tool_result {kind} content omitted]"), +fn claude_text_document_block_to_responses_output_text(block: &Map) -> Option<&str> { + let source = block.get("source")?.as_object()?; + match source + .get("type") + .and_then(Value::as_str) + .unwrap_or_default() + { + "text" => claude_source_str(source, "data"), + _ => None, } } @@ -913,6 +1044,16 @@ mod tests { CanonicalRole, }; use serde_json::json; + use std::collections::BTreeMap; + + fn claude_tool_result_extensions() -> BTreeMap { + let mut extensions = BTreeMap::new(); + extensions.insert( + "aether".to_string(), + json!({ "source": "claude_tool_result" }), + ); + extensions + } #[test] fn json_object_response_injects_json_hint_into_input_when_only_instructions_have_it() { @@ -938,12 +1079,17 @@ mod tests { let body = to_raw(&request, "gpt-5.5", false, false).expect("responses body"); assert_eq!(body["text"]["format"]["type"], json!("json_object")); - assert_eq!(body["input"][0]["role"], json!("system")); - assert!(body["input"][0]["content"][0]["text"] + assert_eq!(body["instructions"], json!("Please answer in JSON.")); + let input = body["input"].as_array().expect("input"); + assert_eq!(input.len(), 2); + assert_eq!(input[0]["role"], json!("system")); + assert!(input[0]["content"][0]["text"] .as_str() .expect("hint text") .to_ascii_lowercase() .contains("json")); + assert_eq!(input[1]["role"], json!("user")); + assert_eq!(input[1]["content"][0]["text"], json!("hello")); } #[test] @@ -1003,4 +1149,192 @@ mod tests { assert_eq!(body["input"][0]["call_id"], "call_empty"); assert_eq!(body["input"][0]["output"], "(empty)"); } + + #[test] + fn responses_request_replaces_empty_tool_call_identifiers() { + let request = CanonicalRequest { + model: "gpt-5.5".to_string(), + messages: vec![ + CanonicalMessage { + role: CanonicalRole::Assistant, + content: vec![CanonicalContentBlock::ToolUse { + id: " ".to_string(), + name: "".to_string(), + input: json!({"q": "rust"}), + extensions: Default::default(), + }], + extensions: Default::default(), + }, + CanonicalMessage { + role: CanonicalRole::Tool, + content: vec![CanonicalContentBlock::ToolResult { + tool_use_id: "".to_string(), + name: None, + output: Some(json!({"ok": true})), + content_text: None, + is_error: false, + extensions: Default::default(), + }], + extensions: Default::default(), + }, + ], + ..CanonicalRequest::default() + }; + + let body = to_raw(&request, "gpt-5.5", false, false).expect("responses body"); + + assert_eq!(body["input"].as_array().expect("input").len(), 2); + assert_eq!(body["input"][0]["type"], "function_call"); + assert_eq!(body["input"][0]["call_id"], "call_auto_0"); + assert_eq!(body["input"][0]["name"], "unknown"); + assert_eq!(body["input"][0]["arguments"], "{\"q\":\"rust\"}"); + assert_eq!(body["input"][1]["type"], "function_call_output"); + assert_eq!(body["input"][1]["call_id"], "call_auto_0"); + } + + #[test] + fn responses_request_assigns_empty_tool_result_identifiers_from_pending_tool_calls_in_order() { + let request = CanonicalRequest { + model: "gpt-5.5".to_string(), + messages: vec![ + CanonicalMessage { + role: CanonicalRole::Assistant, + content: vec![ + CanonicalContentBlock::ToolUse { + id: "call_a".to_string(), + name: "lookup_a".to_string(), + input: json!({"q": "a"}), + extensions: Default::default(), + }, + CanonicalContentBlock::ToolUse { + id: "call_b".to_string(), + name: "lookup_b".to_string(), + input: json!({"q": "b"}), + extensions: Default::default(), + }, + ], + extensions: Default::default(), + }, + CanonicalMessage { + role: CanonicalRole::Tool, + content: vec![ + CanonicalContentBlock::ToolResult { + tool_use_id: " ".to_string(), + name: None, + output: Some(json!("result a")), + content_text: None, + is_error: false, + extensions: Default::default(), + }, + CanonicalContentBlock::ToolResult { + tool_use_id: "".to_string(), + name: None, + output: Some(json!("result b")), + content_text: None, + is_error: false, + extensions: Default::default(), + }, + ], + extensions: Default::default(), + }, + ], + ..CanonicalRequest::default() + }; + + let body = to_raw(&request, "gpt-5.5", false, false).expect("responses body"); + + assert_eq!(body["input"][0]["call_id"], "call_a"); + assert_eq!(body["input"][1]["call_id"], "call_b"); + assert_eq!(body["input"][2]["call_id"], "call_a"); + assert_eq!(body["input"][2]["output"], "result a"); + assert_eq!(body["input"][3]["call_id"], "call_b"); + assert_eq!(body["input"][3]["output"], "result b"); + } + + #[test] + fn responses_request_rejects_orphan_empty_tool_result_identifier() { + let request = CanonicalRequest { + model: "gpt-5.5".to_string(), + messages: vec![CanonicalMessage { + role: CanonicalRole::Tool, + content: vec![CanonicalContentBlock::ToolResult { + tool_use_id: " ".to_string(), + name: None, + output: Some(json!({"ok": true})), + content_text: None, + is_error: false, + extensions: Default::default(), + }], + extensions: Default::default(), + }], + ..CanonicalRequest::default() + }; + + assert!(to_raw(&request, "gpt-5.5", false, false).is_none()); + } + + #[test] + fn responses_request_preserves_claude_text_document_tool_result_content() { + let request = CanonicalRequest { + model: "gpt-5.5".to_string(), + messages: vec![CanonicalMessage { + role: CanonicalRole::Tool, + content: vec![CanonicalContentBlock::ToolResult { + tool_use_id: "call_doc".to_string(), + name: None, + output: Some(json!([ + {"type": "text", "text": "preview"}, + { + "type": "document", + "source": { + "type": "text", + "media_type": "text/plain", + "data": "document body" + } + } + ])), + content_text: None, + is_error: false, + extensions: claude_tool_result_extensions(), + }], + extensions: Default::default(), + }], + ..CanonicalRequest::default() + }; + + let body = to_raw(&request, "gpt-5.5", false, false).expect("responses body"); + + assert_eq!(body["input"][0]["type"], "function_call_output"); + assert_eq!(body["input"][0]["output"], "preview\n\ndocument body"); + assert!(!body.to_string().contains("content omitted")); + } + + #[test] + fn responses_request_rejects_unrepresentable_claude_tool_result_blocks() { + let request = CanonicalRequest { + model: "gpt-5.5".to_string(), + messages: vec![CanonicalMessage { + role: CanonicalRole::Tool, + content: vec![CanonicalContentBlock::ToolResult { + tool_use_id: "call_img".to_string(), + name: None, + output: Some(json!([{ + "type": "image", + "source": { + "type": "unsupported", + "media_type": "image/png", + "data": "AAAA" + } + }])), + content_text: None, + is_error: false, + extensions: claude_tool_result_extensions(), + }], + extensions: Default::default(), + }], + ..CanonicalRequest::default() + }; + + assert!(to_raw(&request, "gpt-5.5", false, false).is_none()); + } } diff --git a/crates/aether-ai-formats/src/formats/openai/responses/response.rs b/crates/aether-ai-formats/src/formats/openai/responses/response.rs index be1258d4b..c26016848 100644 --- a/crates/aether-ai-formats/src/formats/openai/responses/response.rs +++ b/crates/aether-ai-formats/src/formats/openai/responses/response.rs @@ -1,13 +1,16 @@ -use std::collections::BTreeMap; +use std::{ + collections::BTreeMap, + time::{SystemTime, UNIX_EPOCH}, +}; use serde_json::{json, Map, Value}; use crate::{ formats::context::FormatContext, protocol::canonical::{ - canonical_content_block_to_openai_responses_part, + canonical_content_block_to_openai_responses_part, canonical_extension_object_mut, canonical_usage_to_openai_responses_usage, canonicalize_tool_arguments, - flush_openai_responses_message_item, namespace_extension_object, + flush_openai_responses_message_item, is_openai_thinking_block, namespace_extension_object, openai_responses_extensions, openai_responses_output_to_canonical_blocks, openai_usage_to_canonical, CanonicalContentBlock, CanonicalResponse, CanonicalResponseOutput, CanonicalRole, CanonicalStopReason, @@ -42,11 +45,19 @@ pub fn from_raw(body_json: &Value) -> Option { Some(CanonicalStopReason::ToolUse) } else { match body.get("status").and_then(Value::as_str) { - Some("incomplete") => Some(CanonicalStopReason::MaxTokens), + 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"], + ); + 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); + } Some(CanonicalResponse { id: body .get("id") @@ -68,13 +79,23 @@ pub fn from_raw(body_json: &Value) -> Option { content, stop_reason, usage: openai_usage_to_canonical(body.get("usage")), - extensions: openai_responses_extensions( - body, - &["id", "object", "model", "output", "usage", "status"], - ), + extensions, }) } +fn openai_responses_incomplete_stop_reason(body: &Map) -> 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"); @@ -82,6 +103,20 @@ pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value, _compact: b response.insert("object".to_string(), Value::String("response".to_string())); 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(); @@ -123,8 +158,16 @@ pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value, _compact: b 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, @@ -138,9 +181,7 @@ pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value, _compact: b 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.as_ref().filter(|value| !value.is_empty()) - { + if let Some(encrypted_content) = encrypted_content { item.insert( "encrypted_content".to_string(), Value::String(encrypted_content.clone()), @@ -272,19 +313,100 @@ pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value, _compact: b response.insert("service_tier".to_string(), service_tier); } } - response.extend(namespace_extension_object( + let mut extension_fields = namespace_extension_object( &canonical.extensions, OPENAI_RESPONSES_EXTENSION_NAMESPACE, &response, - )); - response.extend(namespace_extension_object( + ); + 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) } +pub(crate) fn ensure_modern_openai_responses_response_fields( + response: &mut Map, +) -> 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::() +} + +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 { + value + .as_i64() + .or_else(|| value.as_u64().and_then(|value| i64::try_from(value).ok())) +} + fn image_block_is_generation_call(extensions: &BTreeMap) -> bool { extensions .get(OPENAI_RESPONSES_EXTENSION_NAMESPACE) @@ -379,6 +501,101 @@ mod tests { 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_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] diff --git a/crates/aether-ai-formats/src/formats/openai/shared.rs b/crates/aether-ai-formats/src/formats/openai/shared.rs index 3d210e2fe..32eee7306 100644 --- a/crates/aether-ai-formats/src/formats/openai/shared.rs +++ b/crates/aether-ai-formats/src/formats/openai/shared.rs @@ -2,6 +2,51 @@ 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, + } + + impl $name { + pub fn parse(value: &str) -> Option { + 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), + _ => 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", + } + } + } + }; +} + +define_openai_reasoning_effort!(OpenAiChatReasoningEffort); +define_openai_reasoning_effort!(OpenAiResponsesReasoningEffort); + +#[deprecated(note = "use OpenAiChatReasoningEffort or OpenAiResponsesReasoningEffort")] +pub type OpenAiReasoningEffort = OpenAiChatReasoningEffort; + pub fn parse_openai_stop_sequences(stop: Option<&Value>) -> Option> { match stop { Some(Value::String(value)) if !value.trim().is_empty() => { diff --git a/crates/aether-ai-formats/src/formats/registry.rs b/crates/aether-ai-formats/src/formats/registry.rs index 1f810565d..082c6ac9e 100644 --- a/crates/aether-ai-formats/src/formats/registry.rs +++ b/crates/aether-ai-formats/src/formats/registry.rs @@ -1,4 +1,4 @@ -use serde_json::Value; +use serde_json::{Map, Value}; use crate::formats::{ aliyun, @@ -9,9 +9,14 @@ use crate::formats::{ jina, openai::{self, chat as openai_chat, responses as openai_responses}, }; -use crate::protocol::canonical::{CanonicalRequest, CanonicalResponse}; +use crate::protocol::canonical::{ + CanonicalContentBlock, CanonicalEmbeddingInput, CanonicalRequest, CanonicalResponse, + CanonicalStopReason, +}; -pub use crate::formats::context::{FormatContext, FormatError}; +pub use crate::formats::context::{ + ConversionFieldStatus, ConversionReport, Converted, FormatContext, FormatError, +}; pub fn parse_request( source_format: &str, @@ -30,9 +35,9 @@ pub fn parse_request( FormatId::JinaEmbedding => jina::embedding::request::from(body, ctx), FormatId::OpenAiRerank => openai::rerank::request::from(body, ctx), FormatId::JinaRerank => jina::rerank::request::from(body, ctx), - FormatId::GeminiEmbedding - | FormatId::DoubaoEmbedding - | FormatId::AliyunMultimodalEmbedding => None, + FormatId::GeminiEmbedding => gemini::embedding::request::from(body, ctx), + FormatId::DoubaoEmbedding => doubao::embedding::request::from(body, ctx), + FormatId::AliyunMultimodalEmbedding => aliyun::embedding::request::from(body, ctx), } .ok_or_else(|| FormatError::RequestParseFailed { format: source.as_str().to_string(), @@ -43,43 +48,131 @@ pub fn emit_request( target_format: &str, request: &CanonicalRequest, ctx: &FormatContext, +) -> Result { + emit_request_inner(target_format, request, &ctx.without_runtime_request_edits()) +} + +fn emit_request_inner( + target_format: &str, + request: &CanonicalRequest, + ctx: &FormatContext, ) -> Result { let target = parse_format(target_format)?; - let mut request = request.clone(); - if let Some(mapped_model) = ctx - .mapped_model - .as_deref() - .filter(|value| !value.trim().is_empty()) - { - request.model = mapped_model.to_string(); - } match target { - FormatId::OpenAiChat => openai_chat::request::to(&request, ctx), - FormatId::OpenAiResponses => openai_responses::request::to(&request, ctx), - FormatId::OpenAiResponsesCompact => openai_responses::request::to_compact(&request, ctx), - FormatId::ClaudeMessages => claude_messages::request::to(&request, ctx), - FormatId::GeminiGenerateContent => gemini_generate_content::request::to(&request, ctx), - FormatId::OpenAiEmbedding => openai::embedding::request::to(&request, ctx), - FormatId::JinaEmbedding => jina::embedding::request::to(&request, ctx), - FormatId::OpenAiRerank => openai::rerank::request::to(&request, ctx), - FormatId::JinaRerank => jina::rerank::request::to(&request, ctx), - FormatId::GeminiEmbedding => gemini::embedding::request::to(&request, ctx), - FormatId::DoubaoEmbedding => doubao::embedding::request::to(&request, ctx), - FormatId::AliyunMultimodalEmbedding => aliyun::embedding::request::to(&request, ctx), + FormatId::OpenAiChat => openai_chat::request::to(request, ctx), + FormatId::OpenAiResponses => openai_responses::request::to(request, ctx), + FormatId::OpenAiResponsesCompact => openai_responses::request::to_compact(request, ctx), + FormatId::ClaudeMessages => claude_messages::request::to(request, ctx), + FormatId::GeminiGenerateContent => gemini_generate_content::request::to(request, ctx), + FormatId::OpenAiEmbedding => openai::embedding::request::to(request, ctx), + FormatId::JinaEmbedding => jina::embedding::request::to(request, ctx), + FormatId::OpenAiRerank => openai::rerank::request::to(request, ctx), + FormatId::JinaRerank => jina::rerank::request::to(request, ctx), + FormatId::GeminiEmbedding => gemini::embedding::request::to(request, ctx), + FormatId::DoubaoEmbedding => doubao::embedding::request::to(request, ctx), + FormatId::AliyunMultimodalEmbedding => aliyun::embedding::request::to(request, ctx), } .ok_or_else(|| FormatError::RequestEmitFailed { format: target.as_str().to_string(), }) } +pub fn parse_request_pure( + source_format: &str, + body: &Value, +) -> Result { + parse_request(source_format, body, &FormatContext::default()) +} + +pub fn emit_request_pure( + target_format: &str, + request: &CanonicalRequest, +) -> Result { + emit_request_inner(target_format, request, &FormatContext::default()) +} + +pub fn convert_request_pure( + source_format: &str, + target_format: &str, + body: &Value, +) -> Result, FormatError> { + convert_request_pure_with_context( + source_format, + target_format, + body, + &FormatContext::default(), + ) +} + +pub fn convert_request_pure_with_context( + source_format: &str, + target_format: &str, + body: &Value, + ctx: &FormatContext, +) -> Result, FormatError> { + let pure_ctx = ctx.without_runtime_request_edits(); + let request = parse_request(source_format, body, &pure_ctx)?; + validate_request_conversion(source_format, target_format, body, &request)?; + let value = emit_request_inner(target_format, &request, &pure_ctx)?; + let report = build_request_conversion_report(source_format, target_format, body, &value); + Ok(Converted { value, report }) +} + pub fn convert_request( source_format: &str, target_format: &str, body: &Value, ctx: &FormatContext, ) -> Result { - let request = parse_request(source_format, body, ctx)?; - emit_request(target_format, &request, ctx) + let source = parse_format(source_format)?; + let target = parse_format(target_format)?; + validate_runtime_request_conversion(source, target, body)?; + let mut request = parse_request(source_format, body, ctx)?; + if let Some(mapped_model) = ctx + .mapped_model + .as_deref() + .filter(|value| !value.trim().is_empty()) + { + request.model = mapped_model.to_string(); + } + emit_request_inner(target_format, &request, ctx) +} + +fn validate_runtime_request_conversion( + source: FormatId, + target: FormatId, + body: &Value, +) -> Result<(), FormatError> { + if source == FormatId::ClaudeMessages { + match target { + FormatId::OpenAiChat + if claude_request_contains_unrepresentable_tool_result_content_for_openai_chat( + body, + ) => + { + return Err(FormatError::LossyConversionBlocked { + source_format: source.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "messages[].content[].tool_result.content".to_string(), + reason: "OpenAI Chat tool messages cannot represent one or more Claude tool_result content blocks".to_string(), + }); + } + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact + if claude_request_contains_unrepresentable_tool_result_content_for_openai_responses( + body, + ) => + { + return Err(FormatError::LossyConversionBlocked { + source_format: source.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "messages[].content[].tool_result.content".to_string(), + reason: "OpenAI Responses function_call_output cannot represent one or more Claude tool_result content blocks".to_string(), + }); + } + _ => {} + } + } + Ok(()) } pub fn parse_response( @@ -112,6 +205,18 @@ pub fn emit_response( target_format: &str, response: &CanonicalResponse, ctx: &FormatContext, +) -> Result { + emit_response_inner( + target_format, + response, + &ctx.without_runtime_request_edits(), + ) +} + +fn emit_response_inner( + target_format: &str, + response: &CanonicalResponse, + ctx: &FormatContext, ) -> Result { let target = parse_format(target_format)?; match target { @@ -133,6 +238,32 @@ pub fn emit_response( }) } +pub fn parse_response_pure( + source_format: &str, + body: &Value, +) -> Result { + parse_response(source_format, body, &FormatContext::default()) +} + +pub fn emit_response_pure( + target_format: &str, + response: &CanonicalResponse, +) -> Result { + emit_response_inner(target_format, response, &FormatContext::default()) +} + +pub fn convert_response_pure( + source_format: &str, + target_format: &str, + body: &Value, +) -> Result, FormatError> { + let response = parse_response_pure(source_format, body)?; + validate_response_conversion(source_format, target_format, body, &response)?; + let value = emit_response_pure(target_format, &response)?; + let report = build_response_conversion_report(source_format, target_format, body, &value); + Ok(Converted { value, report }) +} + pub fn convert_response( source_format: &str, target_format: &str, @@ -140,6 +271,7 @@ pub fn convert_response( ctx: &FormatContext, ) -> Result { let mut response = parse_response(source_format, body, ctx)?; + validate_response_conversion(source_format, target_format, body, &response)?; if response.model.trim().is_empty() || response.model == "unknown" { if let Some(mapped_model) = ctx .mapped_model @@ -149,7 +281,7 @@ pub fn convert_response( response.model = mapped_model.to_string(); } } - emit_response(target_format, &response, ctx) + emit_response_inner(target_format, &response, ctx) } #[derive(Debug, Clone, PartialEq, Eq)] @@ -173,11 +305,2166 @@ fn parse_format(format: &str) -> Result { FormatId::parse(format).ok_or_else(|| FormatError::UnsupportedFormat(format.to_string())) } +fn validate_request_conversion( + source_format: &str, + target_format: &str, + body: &Value, + request: &CanonicalRequest, +) -> Result<(), FormatError> { + let source = parse_format(source_format)?; + let target = parse_format(target_format)?; + if source == target { + return Ok(()); + } + if is_embedding_format(source) || is_embedding_format(target) { + return validate_embedding_request_conversion(source, target, request); + } + if is_rerank_format(source) || is_rerank_format(target) { + return validate_rerank_request_conversion(source, target, request); + } + validate_known_standard_request_root_fields(source, target, body)?; + validate_cross_format_generation_target(source, target, request)?; + validate_openai_reasoning_effort(source, target, body)?; + match (source, target) { + (FormatId::OpenAiChat, FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact) => { + validate_openai_chat_to_responses(body)?; + } + (FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact, FormatId::OpenAiChat) => { + validate_openai_responses_to_chat(body, request)?; + } + (FormatId::ClaudeMessages, target) if target != FormatId::ClaudeMessages => { + validate_claude_cross_format_request(body, target)?; + } + (FormatId::GeminiGenerateContent, target) if target != FormatId::GeminiGenerateContent => { + validate_gemini_cross_format_request(body, target)?; + } + _ => {} + } + validate_cross_format_request_extensions(source, target, request) +} + +fn validate_response_conversion( + source_format: &str, + target_format: &str, + body: &Value, + response: &CanonicalResponse, +) -> Result<(), FormatError> { + let source = parse_format(source_format)?; + let target = parse_format(target_format)?; + if source == target { + return Ok(()); + } + + validate_source_response_stop_enums(source, target, body)?; + validate_canonical_response_stop_reasons(source, target, response) +} + +fn validate_known_standard_request_root_fields( + source: FormatId, + target: FormatId, + body: &Value, +) -> Result<(), FormatError> { + let Some(object) = body.as_object() else { + return Ok(()); + }; + for key in object.keys() { + if standard_request_root_field_is_audited(source, key) { + continue; + } + return Err(FormatError::UnauditedField { + source_format: source.as_str().to_string(), + target_format: target.as_str().to_string(), + field: key.clone(), + reason: "source request root field is not in the audited provider schema for cross-format conversion".to_string(), + }); + } + Ok(()) +} + +fn standard_request_root_field_is_audited(source: FormatId, key: &str) -> bool { + match source { + FormatId::OpenAiChat => matches!( + key, + "audio" + | "frequency_penalty" + | "function_call" + | "functions" + | "logit_bias" + | "logprobs" + | "max_completion_tokens" + | "max_tokens" + | "messages" + | "metadata" + | "modalities" + | "model" + | "n" + | "parallel_tool_calls" + | "prediction" + | "presence_penalty" + | "prompt_cache_key" + | "prompt_cache_retention" + | "reasoning_effort" + | "response_format" + | "safety_identifier" + | "seed" + | "service_tier" + | "stop" + | "store" + | "stream" + | "stream_options" + | "temperature" + | "tool_choice" + | "tools" + | "top_logprobs" + | "top_p" + | "user" + | "verbosity" + | "web_search_options" + ), + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact => matches!( + key, + "background" + | "context_management" + | "conversation" + | "include" + | "input" + | "instructions" + | "max_output_tokens" + | "max_tool_calls" + | "metadata" + | "model" + | "parallel_tool_calls" + | "previous_response_id" + | "prompt" + | "prompt_cache_key" + | "prompt_cache_retention" + | "reasoning" + | "safety_identifier" + | "service_tier" + | "store" + | "stream" + | "stream_options" + | "temperature" + | "text" + | "tool_choice" + | "tools" + | "top_logprobs" + | "top_p" + | "truncation" + | "user" + ), + FormatId::ClaudeMessages => matches!( + key, + "cache_control" + | "container" + | "inference_geo" + | "max_tokens" + | "messages" + | "metadata" + | "model" + | "output_config" + | "service_tier" + | "stop_sequences" + | "stream" + | "system" + | "temperature" + | "thinking" + | "tool_choice" + | "tools" + | "top_k" + | "top_p" + ), + FormatId::GeminiGenerateContent => matches!( + key, + "cachedContent" + | "cached_content" + | "contents" + | "generationConfig" + | "generation_config" + | "model" + | "safetySettings" + | "safety_settings" + | "serviceTier" + | "service_tier" + | "store" + | "systemInstruction" + | "system_instruction" + | "toolConfig" + | "tool_config" + | "tools" + ), + FormatId::OpenAiEmbedding + | FormatId::OpenAiRerank + | FormatId::GeminiEmbedding + | FormatId::JinaEmbedding + | FormatId::JinaRerank + | FormatId::DoubaoEmbedding + | FormatId::AliyunMultimodalEmbedding => true, + } +} + +fn validate_cross_format_generation_target( + source: FormatId, + target: FormatId, + request: &CanonicalRequest, +) -> Result<(), FormatError> { + let generation = &request.generation; + match target { + FormatId::OpenAiChat if generation.top_k.is_some() => { + return lossy_generation_field( + source, + target, + "top_k", + "OpenAI Chat Completions has no official top_k request field", + ); + } + FormatId::OpenAiChat => {} + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact => { + for (field, present, reason) in [ + ( + "top_k", + generation.top_k.is_some(), + "OpenAI Responses has no top_k request field", + ), + ( + "stop_sequences", + generation.stop_sequences.is_some(), + "OpenAI Responses has no stop sequence request field", + ), + ( + "n", + generation.n.is_some(), + "OpenAI Responses has no multi-candidate n request field", + ), + ( + "presence_penalty", + generation.presence_penalty.is_some(), + "OpenAI Responses has no presence_penalty request field", + ), + ( + "frequency_penalty", + generation.frequency_penalty.is_some(), + "OpenAI Responses has no frequency_penalty request field", + ), + ( + "seed", + generation.seed.is_some(), + "OpenAI Responses has no seed request field", + ), + ( + "logprobs", + generation.logprobs.is_some(), + "OpenAI Responses has no logprobs boolean request field", + ), + ] { + if present { + return lossy_generation_field(source, target, field, reason); + } + } + } + FormatId::ClaudeMessages => { + for (field, present, reason) in [ + ( + "n", + generation.n.is_some(), + "Claude Messages has no multi-candidate n request field", + ), + ( + "presence_penalty", + generation.presence_penalty.is_some(), + "Claude Messages has no presence_penalty request field", + ), + ( + "frequency_penalty", + generation.frequency_penalty.is_some(), + "Claude Messages has no frequency_penalty request field", + ), + ( + "seed", + generation.seed.is_some(), + "Claude Messages has no seed request field", + ), + ( + "logprobs", + generation.logprobs.is_some(), + "Claude Messages has no logprobs request field", + ), + ( + "top_logprobs", + generation.top_logprobs.is_some(), + "Claude Messages has no top_logprobs request field", + ), + ] { + if present { + return lossy_generation_field(source, target, field, reason); + } + } + } + FormatId::GeminiGenerateContent => { + for (field, present, reason) in [ + ( + "presence_penalty", + generation.presence_penalty.is_some(), + "Gemini GenerateContent has no presence_penalty request field", + ), + ( + "frequency_penalty", + generation.frequency_penalty.is_some(), + "Gemini GenerateContent has no frequency_penalty request field", + ), + ( + "logprobs", + generation.logprobs.is_some(), + "Gemini GenerateContent has no logprobs request field", + ), + ( + "top_logprobs", + generation.top_logprobs.is_some(), + "Gemini GenerateContent has no top_logprobs request field", + ), + ] { + if present { + return lossy_generation_field(source, target, field, reason); + } + } + } + _ => {} + } + Ok(()) +} + +fn lossy_generation_field( + source: FormatId, + target: FormatId, + canonical_field: &str, + reason: &str, +) -> Result<(), FormatError> { + Err(FormatError::LossyConversionBlocked { + source_format: source.as_str().to_string(), + target_format: target.as_str().to_string(), + field: source_generation_field_path(source, canonical_field), + reason: reason.to_string(), + }) +} + +fn source_generation_field_path(source: FormatId, canonical_field: &str) -> String { + let field = match (source, canonical_field) { + (FormatId::OpenAiChat, "max_tokens") => "max_tokens/max_completion_tokens", + (FormatId::OpenAiChat, "stop_sequences") => "stop", + (FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact, "max_tokens") => { + "max_output_tokens" + } + (FormatId::ClaudeMessages, "stop_sequences") => "stop_sequences", + (FormatId::GeminiGenerateContent, "max_tokens") => "generationConfig.maxOutputTokens", + (FormatId::GeminiGenerateContent, "top_p") => "generationConfig.topP", + (FormatId::GeminiGenerateContent, "top_k") => "generationConfig.topK", + (FormatId::GeminiGenerateContent, "stop_sequences") => "generationConfig.stopSequences", + (FormatId::GeminiGenerateContent, "n") => "generationConfig.candidateCount", + (FormatId::GeminiGenerateContent, "seed") => "generationConfig.seed", + (FormatId::GeminiGenerateContent, other) => return format!("generationConfig.{other}"), + (_, other) => other, + }; + field.to_string() +} + +fn validate_cross_format_request_extensions( + source: FormatId, + target: FormatId, + request: &CanonicalRequest, +) -> Result<(), FormatError> { + validate_request_content_has_no_unknown_blocks(source, target, request)?; + validate_request_extension_namespace(source, target, "request", &request.extensions)?; + for instruction in &request.instructions { + validate_request_extension_namespace( + source, + target, + "instructions[]", + &instruction.extensions, + )?; + } + for message in &request.messages { + validate_request_extension_namespace(source, target, "messages[]", &message.extensions)?; + for block in &message.content { + match block { + CanonicalContentBlock::Text { extensions, .. } + | CanonicalContentBlock::Thinking { extensions, .. } + | CanonicalContentBlock::Image { extensions, .. } + | CanonicalContentBlock::File { extensions, .. } + | CanonicalContentBlock::Audio { extensions, .. } + | CanonicalContentBlock::ToolUse { extensions, .. } + | CanonicalContentBlock::ToolResult { extensions, .. } + | CanonicalContentBlock::Unknown { extensions, .. } => { + validate_request_extension_namespace( + source, + target, + "messages[].content[]", + extensions, + )?; + } + } + } + } + for tool in &request.tools { + validate_request_extension_namespace(source, target, "tools[]", &tool.extensions)?; + } + if let Some(thinking) = &request.thinking { + validate_request_extension_namespace(source, target, "thinking", &thinking.extensions)?; + } + if let Some(response_format) = &request.response_format { + validate_request_extension_namespace( + source, + target, + "response_format", + &response_format.extensions, + )?; + } + Ok(()) +} + +fn validate_request_content_has_no_unknown_blocks( + source: FormatId, + target: FormatId, + request: &CanonicalRequest, +) -> Result<(), FormatError> { + for message in &request.messages { + for block in &message.content { + if let CanonicalContentBlock::Unknown { raw_type, .. } = block { + return Err(FormatError::LossyConversionBlocked { + source_format: source.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "messages[].content[].type".to_string(), + reason: format!( + "target format has no lossless mapping for unknown source content block type {raw_type:?}" + ), + }); + } + } + } + Ok(()) +} + +fn validate_request_extension_namespace( + source: FormatId, + target: FormatId, + location: &str, + extensions: &std::collections::BTreeMap, +) -> Result<(), FormatError> { + for (namespace, value) in extensions { + if namespace == "aether" { + continue; + } + let Some(object) = value.as_object() else { + return Err(FormatError::UnsupportedField { + format: source.as_str().to_string(), + field: format!("{location}.{namespace}"), + reason: + "provider extension namespace must be an object for cross-format conversion" + .to_string(), + }); + }; + for key in object.keys() { + if request_extension_key_is_cross_format_safe(source, target, location, namespace, key) + { + continue; + } + return Err(FormatError::LossyConversionBlocked { + source_format: source.as_str().to_string(), + target_format: target.as_str().to_string(), + field: extension_field_path(location, namespace, key), + reason: "provider-specific extension field has no audited lossless target mapping" + .to_string(), + }); + } + } + Ok(()) +} + +fn request_extension_key_is_cross_format_safe( + source: FormatId, + target: FormatId, + location: &str, + namespace: &str, + key: &str, +) -> bool { + if location == "tools[]" { + return tool_extension_key_is_cross_format_safe(source, target, namespace, key); + } + if location == "thinking" { + return thinking_extension_key_is_cross_format_safe(source, target, namespace, key); + } + if location == "response_format" { + return response_format_extension_key_is_cross_format_safe(namespace, key); + } + matches!( + (source, target, namespace, key), + ( + FormatId::OpenAiChat, + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact, + "openai", + "stream" + | "store" + | "service_tier" + | "safety_identifier" + | "prompt_cache_key" + | "prompt_cache_retention" + | "verbosity", + ) | ( + FormatId::OpenAiChat, + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact, + "openai_responses", + "verbosity", + ) | ( + FormatId::OpenAiChat, + FormatId::GeminiGenerateContent, + "openai", + "web_search_options", + ) | ( + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact, + FormatId::OpenAiChat, + "openai_responses" | "openai_cli", + "stream" + | "store" + | "service_tier" + | "safety_identifier" + | "prompt_cache_key" + | "prompt_cache_retention" + | "verbosity", + ) | (FormatId::ClaudeMessages, _, "claude", "output_config") + | ( + FormatId::ClaudeMessages, + FormatId::OpenAiChat | FormatId::GeminiGenerateContent, + "openai", + "web_search_options", + ) + | ( + FormatId::GeminiGenerateContent, + _, + "gemini", + "thinking_config" | "raw_tools" | "raw_tool_config", + ) + | ( + FormatId::GeminiGenerateContent, + FormatId::OpenAiChat, + "gemini", + "builtin_tools" | "grounding", + ) + | ( + FormatId::GeminiGenerateContent, + FormatId::GeminiGenerateContent, + "gemini", + _ + ) + | ( + FormatId::GeminiGenerateContent, + FormatId::OpenAiChat, + "openai", + "web_search_options", + ) + ) +} + +fn tool_extension_key_is_cross_format_safe( + source: FormatId, + target: FormatId, + namespace: &str, + key: &str, +) -> bool { + matches!( + (source, target, namespace, key), + (_, _, "claude", "raw_input_schema") + | (_, _, "gemini", "raw_parameters") + | ( + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact, + FormatId::GeminiGenerateContent, + "openai_responses" | "openai_cli", + "type", + ) + ) +} + +fn thinking_extension_key_is_cross_format_safe( + source: FormatId, + target: FormatId, + namespace: &str, + key: &str, +) -> bool { + matches!( + (source, target, namespace, key), + ( + FormatId::OpenAiChat, + FormatId::OpenAiResponses + | FormatId::OpenAiResponsesCompact + | FormatId::ClaudeMessages + | FormatId::GeminiGenerateContent, + "openai", + "reasoning_effort", + ) | ( + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact, + FormatId::OpenAiChat | FormatId::ClaudeMessages | FormatId::GeminiGenerateContent, + "openai_responses" | "openai_cli", + "effort", + ) | ( + FormatId::ClaudeMessages, + _, + "claude", + "type" | "budget_tokens" | "output_config", + ) | ( + FormatId::ClaudeMessages, + FormatId::OpenAiChat | FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact, + "openai", + "reasoning_effort", + ) | ( + FormatId::GeminiGenerateContent, + _, + "gemini", + "thinking_config" | "includeThoughts" | "thinkingBudget" | "thinkingLevel", + ) | ( + FormatId::GeminiGenerateContent, + FormatId::OpenAiChat | FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact, + "openai", + "reasoning_effort", + ) + ) +} + +fn response_format_extension_key_is_cross_format_safe(namespace: &str, key: &str) -> bool { + matches!((namespace, key), ("openai", _) | ("gemini", "raw_schema")) +} + +fn extension_field_path(location: &str, namespace: &str, key: &str) -> String { + if location == "request" { + format!("{namespace}.{key}") + } else { + format!("{location}.{namespace}.{key}") + } +} + +fn validate_source_response_stop_enums( + source: FormatId, + target: FormatId, + body: &Value, +) -> Result<(), FormatError> { + match source { + FormatId::OpenAiChat => validate_openai_chat_response_finish_reasons(body), + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact => { + validate_openai_responses_response_status(body, target) + } + FormatId::ClaudeMessages => validate_claude_response_stop_reason(body), + FormatId::GeminiGenerateContent => validate_gemini_response_finish_reasons(body, target), + FormatId::OpenAiEmbedding + | FormatId::OpenAiRerank + | FormatId::GeminiEmbedding + | FormatId::JinaEmbedding + | FormatId::JinaRerank + | FormatId::DoubaoEmbedding + | FormatId::AliyunMultimodalEmbedding => Ok(()), + } +} + +fn validate_openai_chat_response_finish_reasons(body: &Value) -> Result<(), FormatError> { + let Some(choices) = body.get("choices").and_then(Value::as_array) else { + return Ok(()); + }; + for choice in choices { + let Some(value) = choice.get("finish_reason") else { + continue; + }; + validate_nullable_string_field( + value, + FormatId::OpenAiChat.as_str(), + "choices[].finish_reason", + )?; + let Some(raw) = value + .as_str() + .map(str::trim) + .filter(|value| !value.is_empty()) + else { + continue; + }; + if !matches!( + raw, + "stop" | "length" | "tool_calls" | "function_call" | "content_filter" + ) { + return Err(FormatError::InvalidEnumValue { + format: FormatId::OpenAiChat.as_str().to_string(), + field: "choices[].finish_reason".to_string(), + value: raw.to_string(), + }); + } + } + Ok(()) +} + +fn validate_openai_responses_response_status( + body: &Value, + target: FormatId, +) -> Result<(), FormatError> { + let Some(value) = body.get("status") else { + return Ok(()); + }; + validate_nullable_string_field(value, FormatId::OpenAiResponses.as_str(), "status")?; + let Some(raw) = value + .as_str() + .map(str::trim) + .filter(|value| !value.is_empty()) + else { + return Ok(()); + }; + match raw { + "completed" | "incomplete" => Ok(()), + "queued" | "in_progress" | "cancelled" => Err(FormatError::LossyConversionBlocked { + source_format: FormatId::OpenAiResponses.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "status".to_string(), + reason: "target sync response format cannot represent non-terminal Responses status" + .to_string(), + }), + "failed" => Err(FormatError::LossyConversionBlocked { + source_format: FormatId::OpenAiResponses.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "status".to_string(), + reason: + "failed Responses objects must be handled as provider errors, not success responses" + .to_string(), + }), + _ => Err(FormatError::InvalidEnumValue { + format: FormatId::OpenAiResponses.as_str().to_string(), + field: "status".to_string(), + value: raw.to_string(), + }), + } +} + +fn validate_claude_response_stop_reason(body: &Value) -> Result<(), FormatError> { + let Some(value) = body.get("stop_reason") else { + return Ok(()); + }; + validate_nullable_string_field(value, FormatId::ClaudeMessages.as_str(), "stop_reason")?; + let Some(raw) = value + .as_str() + .map(str::trim) + .filter(|value| !value.is_empty()) + else { + return Ok(()); + }; + if !matches!( + raw, + "end_turn" + | "max_tokens" + | "stop_sequence" + | "tool_use" + | "pause_turn" + | "refusal" + | "content_filtered" + ) { + return Err(FormatError::InvalidEnumValue { + format: FormatId::ClaudeMessages.as_str().to_string(), + field: "stop_reason".to_string(), + value: raw.to_string(), + }); + } + Ok(()) +} + +fn validate_gemini_response_finish_reasons( + body: &Value, + target: FormatId, +) -> Result<(), FormatError> { + let Some(candidates) = body.get("candidates").and_then(Value::as_array) else { + return Ok(()); + }; + for candidate in candidates { + let Some(value) = candidate + .get("finishReason") + .or_else(|| candidate.get("finish_reason")) + else { + continue; + }; + validate_nullable_string_field( + value, + FormatId::GeminiGenerateContent.as_str(), + "candidates[].finishReason", + )?; + let Some(raw) = value + .as_str() + .map(str::trim) + .filter(|value| !value.is_empty()) + else { + continue; + }; + let normalized = raw.to_ascii_uppercase(); + if gemini_finish_reason_is_cross_format_mappable(normalized.as_str()) { + continue; + } + if gemini_finish_reason_is_known(normalized.as_str()) { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::GeminiGenerateContent.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "candidates[].finishReason".to_string(), + reason: format!("Gemini finishReason {raw:?} has no lossless target finish reason"), + }); + } + return Err(FormatError::InvalidEnumValue { + format: FormatId::GeminiGenerateContent.as_str().to_string(), + field: "candidates[].finishReason".to_string(), + value: raw.to_string(), + }); + } + Ok(()) +} + +fn validate_nullable_string_field( + value: &Value, + format: &str, + field: &str, +) -> Result<(), FormatError> { + if value.is_null() || value.is_string() { + Ok(()) + } else { + Err(FormatError::InvalidTargetField { + format: format.to_string(), + field: field.to_string(), + reason: "enum field must be a string or null".to_string(), + }) + } +} + +fn gemini_finish_reason_is_cross_format_mappable(value: &str) -> bool { + matches!( + value, + "STOP" + | "MAX_TOKENS" + | "SAFETY" + | "RECITATION" + | "LANGUAGE" + | "BLOCKLIST" + | "PROHIBITED_CONTENT" + | "SPII" + | "IMAGE_SAFETY" + | "IMAGE_PROHIBITED_CONTENT" + | "IMAGE_RECITATION" + ) +} + +fn gemini_finish_reason_is_known(value: &str) -> bool { + gemini_finish_reason_is_cross_format_mappable(value) + || matches!( + value, + "FINISH_REASON_UNSPECIFIED" + | "OTHER" + | "MALFORMED_FUNCTION_CALL" + | "IMAGE_OTHER" + | "NO_IMAGE" + | "UNEXPECTED_TOOL_CALL" + | "TOO_MANY_TOOL_CALLS" + | "MISSING_THOUGHT_SIGNATURE" + | "MALFORMED_RESPONSE" + | "ESCALATION" + ) +} + +fn validate_canonical_response_stop_reasons( + source: FormatId, + target: FormatId, + response: &CanonicalResponse, +) -> Result<(), FormatError> { + if response.stop_reason == Some(CanonicalStopReason::Unknown) + || response + .outputs + .iter() + .any(|output| output.stop_reason == Some(CanonicalStopReason::Unknown)) + { + return Err(FormatError::LossyConversionBlocked { + source_format: source.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "stop_reason".to_string(), + reason: "source response stop reason cannot be represented losslessly in target format" + .to_string(), + }); + } + Ok(()) +} + +fn is_embedding_format(format: FormatId) -> bool { + matches!( + format, + FormatId::OpenAiEmbedding + | FormatId::GeminiEmbedding + | FormatId::JinaEmbedding + | FormatId::DoubaoEmbedding + | FormatId::AliyunMultimodalEmbedding + ) +} + +fn is_rerank_format(format: FormatId) -> bool { + matches!(format, FormatId::OpenAiRerank | FormatId::JinaRerank) +} + +fn validate_embedding_request_conversion( + source: FormatId, + target: FormatId, + request: &CanonicalRequest, +) -> Result<(), FormatError> { + if !is_embedding_format(source) || !is_embedding_format(target) { + return Err(FormatError::UnsupportedField { + format: target.as_str().to_string(), + field: "embedding".to_string(), + reason: "embedding requests can only convert to embedding target formats".to_string(), + }); + } + let Some(embedding) = request.embedding.as_ref() else { + return Err(FormatError::RequestParseFailed { + format: source.as_str().to_string(), + }); + }; + if embedding.input.is_empty() { + return Err(FormatError::InvalidTargetField { + format: source.as_str().to_string(), + field: "input".to_string(), + reason: "embedding input must not be empty".to_string(), + }); + } + validate_no_cross_format_embedding_extensions(source, target, embedding)?; + match target { + FormatId::OpenAiEmbedding => validate_openai_embedding_target(source, target, embedding), + FormatId::JinaEmbedding => validate_text_embedding_target(source, target, embedding, true), + FormatId::GeminiEmbedding => validate_gemini_embedding_target(source, target, embedding), + FormatId::DoubaoEmbedding => validate_doubao_embedding_target(source, target, embedding), + FormatId::AliyunMultimodalEmbedding => { + validate_aliyun_embedding_target(source, target, embedding) + } + _ => Ok(()), + } +} + +fn validate_no_cross_format_embedding_extensions( + source: FormatId, + target: FormatId, + embedding: &crate::protocol::canonical::CanonicalEmbeddingRequest, +) -> Result<(), FormatError> { + if embedding.extensions.is_empty() { + return Ok(()); + } + let target_namespace = embedding_namespace(target); + if embedding + .extensions + .keys() + .all(|key| key == target_namespace) + { + return Ok(()); + } + Err(FormatError::UnsupportedField { + format: target.as_str().to_string(), + field: "embedding.extensions".to_string(), + reason: format!( + "{} provider-specific embedding fields cannot be losslessly emitted as {}", + source.as_str(), + target.as_str() + ), + }) +} + +fn embedding_namespace(format: FormatId) -> &'static str { + match format { + FormatId::OpenAiEmbedding => "openai", + FormatId::GeminiEmbedding => "gemini", + FormatId::JinaEmbedding => "jina", + FormatId::DoubaoEmbedding => "doubao", + FormatId::AliyunMultimodalEmbedding => "aliyun", + _ => "", + } +} + +fn validate_openai_embedding_target( + source: FormatId, + target: FormatId, + embedding: &crate::protocol::canonical::CanonicalEmbeddingRequest, +) -> Result<(), FormatError> { + if matches!(embedding.input, CanonicalEmbeddingInput::Multimodal(_)) { + return lossy_embedding_field( + source, + target, + "input", + "OpenAI embeddings do not support multimodal embedding input", + ); + } + if embedding.task.is_some() { + return lossy_embedding_field( + source, + target, + "task", + "OpenAI embeddings have no task field", + ); + } + if embedding.parameters.is_some() { + return lossy_embedding_field( + source, + target, + "parameters", + "OpenAI embeddings have no generic parameters field", + ); + } + Ok(()) +} + +fn validate_text_embedding_target( + source: FormatId, + target: FormatId, + embedding: &crate::protocol::canonical::CanonicalEmbeddingRequest, + allow_task: bool, +) -> Result<(), FormatError> { + if !matches!( + embedding.input, + CanonicalEmbeddingInput::String(_) | CanonicalEmbeddingInput::StringArray(_) + ) { + return lossy_embedding_field( + source, + target, + "input", + "target embedding format only supports text string inputs", + ); + } + if embedding.encoding_format.is_some() { + return lossy_embedding_field( + source, + target, + "encoding_format", + "target embedding format has no encoding_format field", + ); + } + if embedding.user.is_some() { + return lossy_embedding_field( + source, + target, + "user", + "target embedding format has no user field", + ); + } + if !allow_task && embedding.task.is_some() { + return lossy_embedding_field( + source, + target, + "task", + "target embedding format has no task field", + ); + } + Ok(()) +} + +fn validate_gemini_embedding_target( + source: FormatId, + target: FormatId, + embedding: &crate::protocol::canonical::CanonicalEmbeddingRequest, +) -> Result<(), FormatError> { + validate_text_embedding_target(source, target, embedding, true)?; + if embedding.parameters.is_some() { + return lossy_embedding_field( + source, + target, + "parameters", + "Gemini embeddings require named embedContent config fields, not generic parameters", + ); + } + if let Some(task) = embedding.task.as_deref() { + validate_gemini_embedding_task_type(task)?; + } + Ok(()) +} + +fn validate_gemini_embedding_task_type(task: &str) -> Result<(), FormatError> { + let normalized = task.trim().replace(['-', ' '], "_").to_ascii_uppercase(); + if matches!( + normalized.as_str(), + "QUERY" + | "DOCUMENT" + | "TASK_TYPE_UNSPECIFIED" + | "RETRIEVAL_QUERY" + | "RETRIEVAL_DOCUMENT" + | "TEXT_MATCHING" + | "SEMANTIC_SIMILARITY" + | "CLASSIFICATION" + | "CLUSTERING" + | "QUESTION_ANSWERING" + | "FACT_VERIFICATION" + | "CODE_RETRIEVAL_QUERY" + ) { + Ok(()) + } else { + Err(FormatError::InvalidEnumValue { + format: FormatId::GeminiEmbedding.as_str().to_string(), + field: "taskType".to_string(), + value: task.to_string(), + }) + } +} + +fn validate_doubao_embedding_target( + source: FormatId, + target: FormatId, + embedding: &crate::protocol::canonical::CanonicalEmbeddingRequest, +) -> Result<(), FormatError> { + validate_text_embedding_target(source, target, embedding, false)?; + if embedding.parameters.is_some() { + return lossy_embedding_field( + source, + target, + "parameters", + "Doubao embeddings have no generic parameters field", + ); + } + Ok(()) +} + +fn validate_aliyun_embedding_target( + source: FormatId, + target: FormatId, + embedding: &crate::protocol::canonical::CanonicalEmbeddingRequest, +) -> Result<(), FormatError> { + if matches!( + embedding.input, + CanonicalEmbeddingInput::TokenArray(_) | CanonicalEmbeddingInput::TokenArrayArray(_) + ) { + return lossy_embedding_field( + source, + target, + "input", + "Aliyun multimodal embeddings do not support token-array input", + ); + } + if embedding.encoding_format.is_some() { + return lossy_embedding_field( + source, + target, + "encoding_format", + "Aliyun multimodal embeddings have no encoding_format field", + ); + } + if embedding.user.is_some() { + return lossy_embedding_field( + source, + target, + "user", + "Aliyun multimodal embeddings have no user field", + ); + } + if embedding.task.is_some() { + return lossy_embedding_field( + source, + target, + "task", + "Aliyun multimodal embeddings have no task field", + ); + } + Ok(()) +} + +fn lossy_embedding_field( + source: FormatId, + target: FormatId, + field: &str, + reason: &str, +) -> Result<(), FormatError> { + Err(FormatError::LossyConversionBlocked { + source_format: source.as_str().to_string(), + target_format: target.as_str().to_string(), + field: field.to_string(), + reason: reason.to_string(), + }) +} + +fn validate_rerank_request_conversion( + source: FormatId, + target: FormatId, + request: &CanonicalRequest, +) -> Result<(), FormatError> { + if !is_rerank_format(source) || !is_rerank_format(target) { + return Err(FormatError::UnsupportedField { + format: target.as_str().to_string(), + field: "rerank".to_string(), + reason: "rerank requests can only convert to rerank target formats".to_string(), + }); + } + let Some(rerank) = request.rerank.as_ref() else { + return Err(FormatError::RequestParseFailed { + format: source.as_str().to_string(), + }); + }; + if rerank.is_empty() { + return Err(FormatError::InvalidTargetField { + format: source.as_str().to_string(), + field: "query/documents".to_string(), + reason: "rerank query and documents must not be empty".to_string(), + }); + } + if rerank.top_n == Some(0) { + return Err(FormatError::InvalidTargetField { + format: source.as_str().to_string(), + field: "top_n".to_string(), + reason: "rerank top_n must be greater than zero".to_string(), + }); + } + let target_namespace = rerank_namespace(target); + if !rerank.extensions.is_empty() && !rerank.extensions.keys().all(|key| key == target_namespace) + { + return Err(FormatError::UnsupportedField { + format: target.as_str().to_string(), + field: "rerank.extensions".to_string(), + reason: format!( + "{} provider-specific rerank fields cannot be losslessly emitted as {}", + source.as_str(), + target.as_str() + ), + }); + } + Ok(()) +} + +fn rerank_namespace(format: FormatId) -> &'static str { + match format { + FormatId::OpenAiRerank => "openai", + FormatId::JinaRerank => "jina", + _ => "", + } +} + +fn validate_openai_reasoning_effort( + source: FormatId, + target: FormatId, + body: &Value, +) -> Result<(), FormatError> { + if !matches!( + (source, target), + ( + FormatId::OpenAiChat, + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact + ) | ( + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact, + FormatId::OpenAiChat + ) + ) { + return Ok(()); + } + let Some(object) = body.as_object() else { + return Ok(()); + }; + match source { + FormatId::OpenAiChat => validate_openai_reasoning_effort_value( + source.as_str(), + "reasoning_effort", + object.get("reasoning_effort"), + openai_chat_reasoning_effort_is_valid, + ), + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact => { + let effort = object + .get("reasoning") + .and_then(Value::as_object) + .and_then(|reasoning| reasoning.get("effort")); + validate_openai_reasoning_effort_value( + source.as_str(), + "reasoning.effort", + effort, + openai_responses_reasoning_effort_is_valid, + ) + } + _ => Ok(()), + } +} + +fn validate_openai_reasoning_effort_value( + format: &str, + field: &str, + value: Option<&Value>, + is_valid: fn(&str) -> bool, +) -> Result<(), FormatError> { + let Some(value) = value else { + return Ok(()); + }; + let Some(raw) = value.as_str() else { + return Err(FormatError::InvalidTargetField { + format: format.to_string(), + field: field.to_string(), + reason: "reasoning effort must be a string".to_string(), + }); + }; + if is_valid(raw) { + Ok(()) + } else { + Err(FormatError::InvalidEnumValue { + format: format.to_string(), + field: field.to_string(), + value: raw.to_string(), + }) + } +} + +fn openai_chat_reasoning_effort_is_valid(value: &str) -> bool { + crate::formats::openai::shared::OpenAiChatReasoningEffort::parse(value).is_some() +} + +fn openai_responses_reasoning_effort_is_valid(value: &str) -> bool { + crate::formats::openai::shared::OpenAiResponsesReasoningEffort::parse(value).is_some() +} + +fn validate_openai_chat_to_responses(body: &Value) -> Result<(), FormatError> { + let Some(object) = body.as_object() else { + return Ok(()); + }; + for field in [ + "n", + "stop", + "presence_penalty", + "frequency_penalty", + "seed", + "logprobs", + "stream_options", + "user", + ] { + if object.contains_key(field) { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::OpenAiChat.as_str().to_string(), + target_format: FormatId::OpenAiResponses.as_str().to_string(), + field: field.to_string(), + reason: "OpenAI Responses request has no canonical equivalent for this Chat field" + .to_string(), + }); + } + } + Ok(()) +} + +fn validate_openai_responses_to_chat( + body: &Value, + request: &CanonicalRequest, +) -> Result<(), FormatError> { + let Some(object) = body.as_object() else { + return Ok(()); + }; + for field in [ + "include", + "previous_response_id", + "truncation", + "prompt", + "conversation", + "background", + "max_tool_calls", + ] { + if object.contains_key(field) { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::OpenAiResponses.as_str().to_string(), + target_format: FormatId::OpenAiChat.as_str().to_string(), + field: field.to_string(), + reason: "OpenAI Chat request has no canonical equivalent for this Responses field" + .to_string(), + }); + } + } + if let Some(reasoning) = object.get("reasoning").and_then(Value::as_object) { + for field in ["summary", "budget_tokens"] { + if reasoning.contains_key(field) { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::OpenAiResponses.as_str().to_string(), + target_format: FormatId::OpenAiChat.as_str().to_string(), + field: format!("reasoning.{field}"), + reason: + "OpenAI Chat reasoning_effort cannot carry this Responses reasoning field" + .to_string(), + }); + } + } + } + if let Some(tools) = object.get("tools").and_then(Value::as_array) { + for tool in tools { + let tool_type = tool + .get("type") + .and_then(Value::as_str) + .unwrap_or("function") + .trim() + .to_ascii_lowercase(); + if tool_type != "function" { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::OpenAiResponses.as_str().to_string(), + target_format: FormatId::OpenAiChat.as_str().to_string(), + field: "tools".to_string(), + reason: format!("OpenAI Chat only supports function tools, got {tool_type}"), + }); + } + } + } + if request.tools.iter().any(|tool| tool.name.trim().is_empty()) { + return Err(FormatError::InvalidTargetField { + format: FormatId::OpenAiChat.as_str().to_string(), + field: "tools[].function.name".to_string(), + reason: "OpenAI Chat function tools require a non-empty name".to_string(), + }); + } + Ok(()) +} + +fn validate_claude_cross_format_request(body: &Value, target: FormatId) -> Result<(), FormatError> { + if claude_request_contains_provider_cache_control(body) { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::ClaudeMessages.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "cache_control".to_string(), + reason: "target format has no lossless equivalent for Claude cache_control".to_string(), + }); + } + + if let Some(output_effort) = body + .as_object() + .and_then(|object| object.get("output_config")) + .and_then(Value::as_object) + .and_then(|output_config| output_config.get("effort")) + { + validate_claude_output_effort_value(output_effort)?; + } + + match target { + FormatId::OpenAiChat if claude_request_contains_tool_result_content_array(body) => { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::ClaudeMessages.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "messages[].content[].tool_result.content".to_string(), + reason: "OpenAI Chat tool messages cannot losslessly preserve Claude multi-block tool_result content".to_string(), + }); + } + FormatId::OpenAiChat => {} + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact => { + if claude_request_contains_message_thinking_blocks(body) { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::ClaudeMessages.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "messages[].content[].type".to_string(), + reason: "OpenAI Responses request input cannot losslessly preserve Claude thinking blocks".to_string(), + }); + } + if claude_request_contains_tool_result_content_array(body) { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::ClaudeMessages.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "messages[].content[].tool_result.content".to_string(), + reason: "OpenAI Responses function_call_output cannot losslessly preserve Claude multi-block tool_result content".to_string(), + }); + } + } + FormatId::GeminiGenerateContent + if claude_request_contains_redacted_thinking_blocks(body) => + { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::ClaudeMessages.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "messages[].content[].type".to_string(), + reason: "Gemini request parts cannot losslessly preserve Claude redacted thinking blocks".to_string(), + }); + } + FormatId::GeminiGenerateContent => {} + _ => {} + } + Ok(()) +} + +fn validate_claude_output_effort_value(value: &Value) -> Result<(), FormatError> { + let Some(raw) = value.as_str() else { + return Err(FormatError::InvalidTargetField { + format: FormatId::ClaudeMessages.as_str().to_string(), + field: "output_config.effort".to_string(), + reason: "Claude output effort must be a string".to_string(), + }); + }; + if crate::protocol::canonical::claude_output_effort_to_openai_reasoning_effort(raw).is_some() { + Ok(()) + } else { + Err(FormatError::InvalidEnumValue { + format: FormatId::ClaudeMessages.as_str().to_string(), + field: "output_config.effort".to_string(), + value: raw.to_string(), + }) + } +} + +fn validate_gemini_cross_format_request(body: &Value, target: FormatId) -> Result<(), FormatError> { + let Some(object) = body.as_object() else { + return Ok(()); + }; + + if object.contains_key("safetySettings") || object.contains_key("safety_settings") { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::GeminiGenerateContent.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "safetySettings".to_string(), + reason: "target format has no lossless equivalent for Gemini safetySettings" + .to_string(), + }); + } + if object.contains_key("cachedContent") || object.contains_key("cached_content") { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::GeminiGenerateContent.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "cachedContent".to_string(), + reason: "target format has no lossless equivalent for Gemini cachedContent".to_string(), + }); + } + if let Some(generation_config) = object_by_case(object, "generationConfig", "generation_config") + { + if generation_config.contains_key("responseModalities") + || generation_config.contains_key("response_modalities") + { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::GeminiGenerateContent.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "generationConfig.responseModalities".to_string(), + reason: "target format has no lossless equivalent for Gemini responseModalities" + .to_string(), + }); + } + if let Some(thinking_config) = + object_by_case(generation_config, "thinkingConfig", "thinking_config") + { + validate_gemini_cross_format_thinking_config(thinking_config)?; + } + } + if let Some(tool_config) = object_by_case(object, "toolConfig", "tool_config") { + validate_gemini_cross_format_tool_config(tool_config, target)?; + } + if gemini_request_contains_builtin_tool(body, "codeExecution", "code_execution") { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::GeminiGenerateContent.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "tools[].codeExecution".to_string(), + reason: "target format has no lossless equivalent for Gemini codeExecution".to_string(), + }); + } + if gemini_request_contains_builtin_tool(body, "urlContext", "url_context") { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::GeminiGenerateContent.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "tools[].urlContext".to_string(), + reason: "target format has no lossless equivalent for Gemini urlContext".to_string(), + }); + } + if matches!( + target, + FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact + ) && gemini_request_contains_thought_parts(body) + { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::GeminiGenerateContent.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "contents[].parts[].thoughtSignature".to_string(), + reason: + "OpenAI Responses request input cannot losslessly preserve Gemini thought parts" + .to_string(), + }); + } + Ok(()) +} + +fn validate_gemini_cross_format_thinking_config( + thinking_config: &Map, +) -> Result<(), FormatError> { + if let Some(value) = thinking_config + .get("thinkingLevel") + .or_else(|| thinking_config.get("thinking_level")) + { + let Some(raw) = value.as_str() else { + return Err(FormatError::InvalidTargetField { + format: FormatId::GeminiGenerateContent.as_str().to_string(), + field: "generationConfig.thinkingConfig.thinkingLevel".to_string(), + reason: "Gemini thinkingLevel must be a string".to_string(), + }); + }; + if !matches!( + raw.trim().to_ascii_lowercase().as_str(), + "low" | "medium" | "high" + ) { + return Err(FormatError::InvalidEnumValue { + format: FormatId::GeminiGenerateContent.as_str().to_string(), + field: "generationConfig.thinkingConfig.thinkingLevel".to_string(), + value: raw.to_string(), + }); + } + } + for key in thinking_config.keys() { + if !matches!( + key.as_str(), + "includeThoughts" + | "include_thoughts" + | "thinkingBudget" + | "thinking_budget" + | "thinkingLevel" + | "thinking_level" + ) { + return Err(FormatError::UnsupportedField { + format: FormatId::GeminiGenerateContent.as_str().to_string(), + field: format!("generationConfig.thinkingConfig.{key}"), + reason: "Gemini thinkingConfig field has no canonical cross-format mapping" + .to_string(), + }); + } + } + Ok(()) +} + +fn validate_gemini_cross_format_tool_config( + tool_config: &Map, + target: FormatId, +) -> Result<(), FormatError> { + for key in tool_config.keys() { + if !matches!( + key.as_str(), + "functionCallingConfig" | "function_calling_config" + ) { + return Err(FormatError::UnsupportedField { + format: FormatId::GeminiGenerateContent.as_str().to_string(), + field: format!("toolConfig.{key}"), + reason: "Gemini toolConfig field has no canonical cross-format mapping".to_string(), + }); + } + } + let Some(function_calling_config) = object_by_case( + tool_config, + "functionCallingConfig", + "function_calling_config", + ) else { + return Err(FormatError::UnsupportedField { + format: FormatId::GeminiGenerateContent.as_str().to_string(), + field: "toolConfig".to_string(), + reason: "Gemini toolConfig must use functionCallingConfig for cross-format conversion" + .to_string(), + }); + }; + for key in function_calling_config.keys() { + if !matches!( + key.as_str(), + "mode" | "allowedFunctionNames" | "allowed_function_names" + ) { + return Err(FormatError::UnsupportedField { + format: FormatId::GeminiGenerateContent.as_str().to_string(), + field: format!("toolConfig.functionCallingConfig.{key}"), + reason: "Gemini functionCallingConfig field has no canonical cross-format mapping" + .to_string(), + }); + } + } + if let Some(value) = function_calling_config.get("mode") { + let Some(raw) = value.as_str() else { + return Err(FormatError::InvalidTargetField { + format: FormatId::GeminiGenerateContent.as_str().to_string(), + field: "toolConfig.functionCallingConfig.mode".to_string(), + reason: "Gemini function calling mode must be a string".to_string(), + }); + }; + if !matches!( + raw.trim().to_ascii_uppercase().as_str(), + "NONE" | "AUTO" | "ANY" | "REQUIRED" + ) { + return Err(FormatError::InvalidEnumValue { + format: FormatId::GeminiGenerateContent.as_str().to_string(), + field: "toolConfig.functionCallingConfig.mode".to_string(), + value: raw.to_string(), + }); + } + } + if let Some(value) = function_calling_config + .get("allowedFunctionNames") + .or_else(|| function_calling_config.get("allowed_function_names")) + { + let Some(names) = value.as_array() else { + return Err(FormatError::InvalidTargetField { + format: FormatId::GeminiGenerateContent.as_str().to_string(), + field: "toolConfig.functionCallingConfig.allowedFunctionNames".to_string(), + reason: "Gemini allowedFunctionNames must be an array of strings".to_string(), + }); + }; + if !names.iter().all(|value| value.as_str().is_some()) { + return Err(FormatError::InvalidTargetField { + format: FormatId::GeminiGenerateContent.as_str().to_string(), + field: "toolConfig.functionCallingConfig.allowedFunctionNames".to_string(), + reason: "Gemini allowedFunctionNames must be an array of strings".to_string(), + }); + } + if names.len() > 1 { + return Err(FormatError::LossyConversionBlocked { + source_format: FormatId::GeminiGenerateContent.as_str().to_string(), + target_format: target.as_str().to_string(), + field: "toolConfig.functionCallingConfig.allowedFunctionNames".to_string(), + reason: "target format can represent at most one named tool choice".to_string(), + }); + } + } + Ok(()) +} + +fn object_by_case<'a>( + object: &'a Map, + camel: &str, + snake: &str, +) -> Option<&'a Map> { + object + .get(camel) + .or_else(|| object.get(snake)) + .and_then(Value::as_object) +} + +fn claude_request_contains_message_thinking_blocks(body: &Value) -> bool { + claude_request_contains_block_type(body, "thinking") + || claude_request_contains_block_type(body, "redacted_thinking") +} + +fn claude_request_contains_redacted_thinking_blocks(body: &Value) -> bool { + claude_request_contains_block_type(body, "redacted_thinking") +} + +fn claude_request_contains_block_type(body: &Value, expected_type: &str) -> bool { + let Some(messages) = body + .as_object() + .and_then(|object| object.get("messages")) + .and_then(Value::as_array) + else { + return false; + }; + messages.iter().any(|message| { + message + .get("content") + .and_then(Value::as_array) + .is_some_and(|blocks| { + blocks.iter().any(|block| { + block + .get("type") + .and_then(Value::as_str) + .is_some_and(|block_type| block_type.eq_ignore_ascii_case(expected_type)) + }) + }) + }) +} + +fn claude_request_contains_tool_result_content_array(body: &Value) -> bool { + let Some(messages) = body + .as_object() + .and_then(|object| object.get("messages")) + .and_then(Value::as_array) + else { + return false; + }; + messages.iter().any(|message| { + message + .get("content") + .and_then(Value::as_array) + .is_some_and(|blocks| { + blocks.iter().any(|block| { + block + .get("type") + .and_then(Value::as_str) + .is_some_and(|block_type| block_type.eq_ignore_ascii_case("tool_result")) + && block.get("content").is_some_and(Value::is_array) + }) + }) + }) +} + +fn claude_request_contains_unrepresentable_tool_result_content_for_openai_chat( + body: &Value, +) -> bool { + claude_request_contains_unrepresentable_tool_result_content(body, |parts| { + !openai_chat::request::claude_tool_result_parts_are_openai_chat_representable(parts) + }) +} + +fn claude_request_contains_unrepresentable_tool_result_content_for_openai_responses( + body: &Value, +) -> bool { + claude_request_contains_unrepresentable_tool_result_content(body, |parts| { + !openai_responses::request::claude_tool_result_parts_are_openai_responses_representable( + parts, + ) + }) +} + +fn claude_request_contains_unrepresentable_tool_result_content( + body: &Value, + is_unrepresentable: impl Fn(&[Value]) -> bool, +) -> bool { + let Some(messages) = body + .as_object() + .and_then(|object| object.get("messages")) + .and_then(Value::as_array) + else { + return false; + }; + messages.iter().any(|message| { + message + .get("content") + .and_then(Value::as_array) + .is_some_and(|blocks| { + blocks.iter().any(|block| { + block + .get("type") + .and_then(Value::as_str) + .is_some_and(|block_type| block_type.eq_ignore_ascii_case("tool_result")) + && block + .get("content") + .and_then(Value::as_array) + .is_some_and(|parts| is_unrepresentable(parts.as_slice())) + }) + }) + }) +} + +fn gemini_request_contains_builtin_tool(body: &Value, camel: &str, snake: &str) -> bool { + let Some(tools) = body + .as_object() + .and_then(|object| object.get("tools")) + .and_then(Value::as_array) + else { + return false; + }; + tools.iter().any(|tool| { + tool.as_object() + .is_some_and(|object| object.contains_key(camel) || object.contains_key(snake)) + }) +} + +fn gemini_request_contains_thought_parts(body: &Value) -> bool { + let Some(contents) = body + .as_object() + .and_then(|object| object.get("contents")) + .and_then(Value::as_array) + else { + return false; + }; + contents.iter().any(|content| { + content + .get("parts") + .and_then(Value::as_array) + .is_some_and(|parts| { + parts.iter().any(|part| { + part.get("thought") + .and_then(Value::as_bool) + .unwrap_or(false) + }) + }) + }) +} + +fn claude_request_contains_provider_cache_control(body: &Value) -> bool { + let Some(object) = body.as_object() else { + return false; + }; + object.contains_key("cache_control") + || claude_system_contains_cache_control(object.get("system")) + || claude_messages_contain_cache_control(object.get("messages")) + || claude_tools_contain_cache_control(object.get("tools")) +} + +fn claude_system_contains_cache_control(system: Option<&Value>) -> bool { + match system { + Some(Value::Array(blocks)) => blocks.iter().any(|block| { + block + .as_object() + .is_some_and(|object| object.contains_key("cache_control")) + }), + _ => false, + } +} + +fn claude_messages_contain_cache_control(messages: Option<&Value>) -> bool { + let Some(messages) = messages.and_then(Value::as_array) else { + return false; + }; + messages.iter().any(|message| { + message + .get("content") + .is_some_and(claude_content_value_contains_cache_control) + }) +} + +fn claude_content_value_contains_cache_control(content: &Value) -> bool { + match content { + Value::Array(blocks) => blocks + .iter() + .any(claude_content_block_contains_cache_control), + _ => false, + } +} + +fn claude_content_block_contains_cache_control(block: &Value) -> bool { + let Some(object) = block.as_object() else { + return false; + }; + if object.contains_key("cache_control") { + return true; + } + object + .get("type") + .and_then(Value::as_str) + .is_some_and(|block_type| block_type.eq_ignore_ascii_case("tool_result")) + && object + .get("content") + .is_some_and(claude_content_value_contains_cache_control) +} + +fn claude_tools_contain_cache_control(tools: Option<&Value>) -> bool { + let Some(tools) = tools.and_then(Value::as_array) else { + return false; + }; + tools.iter().any(|tool| { + tool.as_object() + .is_some_and(|object| object.contains_key("cache_control")) + }) +} + +fn build_request_conversion_report( + source_format: &str, + target_format: &str, + body: &Value, + output: &Value, +) -> ConversionReport { + let mut report = ConversionReport::new(source_format, target_format); + let source = body.as_object(); + let target = output.as_object(); + if let Some(source) = source { + for key in source.keys() { + let status = if target.is_some_and(|target| target.contains_key(key)) { + ConversionFieldStatus::Native + } else if request_field_has_known_mapping(source_format, target_format, key.as_str()) { + ConversionFieldStatus::Mapped + } else { + ConversionFieldStatus::ExtensionPreserved + }; + report.record(key.clone(), status, None); + } + } + if let Some(target) = target { + for key in target.keys() { + if source.is_some_and(|source| source.contains_key(key)) { + continue; + } + if request_field_is_target_native(source_format, target_format, key.as_str()) { + report.record( + key.clone(), + ConversionFieldStatus::Mapped, + Some("emitted from canonical field".to_string()), + ); + } + } + } + report +} + +fn build_response_conversion_report( + source_format: &str, + target_format: &str, + body: &Value, + output: &Value, +) -> ConversionReport { + let mut report = ConversionReport::new(source_format, target_format); + let source = body.as_object(); + let target = output.as_object(); + if let Some(source) = source { + for key in source.keys() { + let status = if target.is_some_and(|target| target.contains_key(key)) { + ConversionFieldStatus::Native + } else if response_field_has_known_mapping(source_format, target_format, key.as_str()) { + ConversionFieldStatus::Mapped + } else { + ConversionFieldStatus::ExtensionPreserved + }; + report.record(key.clone(), status, None); + } + } + if let Some(target) = target { + for key in target.keys() { + if source.is_some_and(|source| source.contains_key(key)) { + continue; + } + if response_field_is_target_native(source_format, target_format, key.as_str()) { + report.record( + key.clone(), + ConversionFieldStatus::Mapped, + Some("emitted from canonical response field".to_string()), + ); + } + } + } + report +} + +fn request_field_has_known_mapping(source_format: &str, target_format: &str, field: &str) -> bool { + matches!( + ( + normalize_known_format(source_format), + normalize_known_format(target_format), + field, + ), + ( + Some(FormatId::OpenAiChat), + Some(FormatId::OpenAiResponses), + "messages" + ) | ( + Some(FormatId::OpenAiChat), + Some(FormatId::OpenAiResponses), + "max_tokens" + ) | ( + Some(FormatId::OpenAiChat), + Some(FormatId::OpenAiResponses), + "max_completion_tokens" + ) | ( + Some(FormatId::OpenAiChat), + Some(FormatId::OpenAiResponses), + "response_format" + ) | ( + Some(FormatId::OpenAiChat), + Some(FormatId::OpenAiResponses), + "reasoning_effort" + ) | ( + Some(FormatId::OpenAiResponses), + Some(FormatId::OpenAiChat), + "input" + ) | ( + Some(FormatId::OpenAiResponses), + Some(FormatId::OpenAiChat), + "instructions" + ) | ( + Some(FormatId::OpenAiResponses), + Some(FormatId::OpenAiChat), + "max_output_tokens" + ) | ( + Some(FormatId::OpenAiResponses), + Some(FormatId::OpenAiChat), + "text" + ) | ( + Some(FormatId::OpenAiResponses), + Some(FormatId::OpenAiChat), + "reasoning" + ) + ) +} + +fn request_field_is_target_native(source_format: &str, target_format: &str, field: &str) -> bool { + matches!( + ( + normalize_known_format(source_format), + normalize_known_format(target_format), + field, + ), + ( + Some(FormatId::OpenAiChat), + Some(FormatId::OpenAiResponses), + "input" + ) | ( + Some(FormatId::OpenAiChat), + Some(FormatId::OpenAiResponses), + "max_output_tokens" + ) | ( + Some(FormatId::OpenAiChat), + Some(FormatId::OpenAiResponses), + "text" + ) | ( + Some(FormatId::OpenAiChat), + Some(FormatId::OpenAiResponses), + "reasoning" + ) | ( + Some(FormatId::OpenAiResponses), + Some(FormatId::OpenAiChat), + "messages" + ) | ( + Some(FormatId::OpenAiResponses), + Some(FormatId::OpenAiChat), + "max_completion_tokens" + ) | ( + Some(FormatId::OpenAiResponses), + Some(FormatId::OpenAiChat), + "response_format" + ) | ( + Some(FormatId::OpenAiResponses), + Some(FormatId::OpenAiChat), + "reasoning_effort" + ) + ) +} + +fn response_field_has_known_mapping(source_format: &str, target_format: &str, field: &str) -> bool { + match ( + normalize_known_format(source_format), + normalize_known_format(target_format), + ) { + (Some(FormatId::OpenAiChat), Some(FormatId::OpenAiResponses)) => { + matches!(field, "choices") + } + (Some(FormatId::OpenAiResponses), Some(FormatId::OpenAiChat)) => { + matches!( + field, + "output" | "status" | "incomplete_details" | "output_text" + ) + } + ( + Some(FormatId::ClaudeMessages), + Some(FormatId::OpenAiChat | FormatId::OpenAiResponses), + ) => { + matches!(field, "content" | "stop_reason" | "stop_sequence") + } + ( + Some(FormatId::GeminiGenerateContent), + Some(FormatId::OpenAiChat | FormatId::OpenAiResponses), + ) => { + matches!(field, "candidates" | "usageMetadata" | "usage_metadata") + } + ( + Some(FormatId::OpenAiChat | FormatId::OpenAiResponses), + Some(FormatId::ClaudeMessages), + ) => { + matches!( + field, + "choices" | "output" | "status" | "incomplete_details" + ) + } + ( + Some(FormatId::OpenAiChat | FormatId::OpenAiResponses), + Some(FormatId::GeminiGenerateContent), + ) => { + matches!( + field, + "choices" | "output" | "status" | "incomplete_details" + ) + } + _ => false, + } +} + +fn response_field_is_target_native(source_format: &str, target_format: &str, field: &str) -> bool { + match ( + normalize_known_format(source_format), + normalize_known_format(target_format), + ) { + (Some(FormatId::OpenAiChat), Some(FormatId::OpenAiResponses)) => { + matches!(field, "output" | "status" | "output_text") + } + (Some(FormatId::OpenAiResponses), Some(FormatId::OpenAiChat)) => matches!(field, "choices"), + (_, Some(FormatId::OpenAiChat)) => matches!(field, "choices"), + (_, Some(FormatId::OpenAiResponses)) => { + matches!(field, "output" | "status" | "output_text") + } + (_, Some(FormatId::ClaudeMessages)) => matches!(field, "content" | "stop_reason"), + (_, Some(FormatId::GeminiGenerateContent)) => matches!(field, "candidates"), + _ => false, + } +} + +fn normalize_known_format(format: &str) -> Option { + match FormatId::parse(format)? { + FormatId::OpenAiResponsesCompact => Some(FormatId::OpenAiResponses), + value => Some(value), + } +} + #[cfg(test)] mod tests { use serde_json::json; - use super::{convert_request, FormatContext}; + use super::{ + convert_request, convert_request_pure, convert_request_pure_with_context, + convert_response_pure, FormatContext, + }; use crate::formats::id::FormatId; #[test] @@ -201,6 +2488,1090 @@ mod tests { assert_eq!(converted["input"][0]["content"][0]["type"], "input_text"); } + #[test] + fn pure_request_conversion_does_not_apply_model_or_stream_edits() { + let body = json!({ + "model": "gpt-source", + "messages": [{"role": "user", "content": "hello"}] + }); + let ctx = FormatContext::default() + .with_mapped_model("gpt-target") + .with_upstream_stream(true); + + let converted = + convert_request_pure_with_context("openai:chat", "openai:responses", &body, &ctx) + .expect("pure conversion should succeed"); + + assert_eq!(converted.value["model"], "gpt-source"); + assert!(converted.value.get("stream").is_none()); + assert!(converted + .report + .fields + .iter() + .any(|field| field.field == "messages")); + } + + #[test] + fn pure_openai_chat_to_responses_preserves_explicit_tool_strict() { + let body = json!({ + "model": "gpt-source", + "messages": [{"role": "user", "content": "hello"}], + "tools": [{ + "type": "function", + "function": { + "name": "lookup", + "parameters": {"type": "object"}, + "strict": true + } + }] + }); + + let converted = convert_request_pure("openai:chat", "openai:responses", &body) + .expect("pure conversion should succeed") + .value; + + assert_eq!(converted["tools"][0]["strict"], true); + } + + #[test] + fn pure_openai_responses_to_chat_preserves_explicit_tool_strict() { + let body = json!({ + "model": "gpt-source", + "input": [{"role": "user", "content": "hello"}], + "tools": [{ + "type": "function", + "name": "lookup", + "parameters": {"type": "object"}, + "strict": false + }] + }); + + let converted = convert_request_pure("openai:responses", "openai:chat", &body) + .expect("pure conversion should succeed") + .value; + + assert_eq!(converted["tools"][0]["function"]["strict"], false); + } + + #[test] + fn pure_openai_chat_to_responses_maps_tool_call_ids_to_call_ids() { + let body = json!({ + "model": "gpt-source", + "messages": [{ + "role": "assistant", + "content": null, + "tool_calls": [{ + "id": "call_lookup_1", + "type": "function", + "function": { + "name": "lookup", + "arguments": "{\"q\":\"rust\"}" + } + }] + }, { + "role": "tool", + "tool_call_id": "call_lookup_1", + "content": "{\"ok\":true}" + }] + }); + + let converted = convert_request_pure("openai:chat", "openai:responses", &body) + .expect("pure conversion should succeed") + .value; + + assert_eq!(converted["input"][0]["type"], "function_call"); + assert_eq!(converted["input"][0]["call_id"], "call_lookup_1"); + assert_eq!(converted["input"][1]["type"], "function_call_output"); + assert_eq!(converted["input"][1]["call_id"], "call_lookup_1"); + } + + #[test] + fn pure_openai_responses_to_chat_maps_call_ids_to_tool_call_ids() { + let body = json!({ + "model": "gpt-source", + "input": [{ + "type": "function_call", + "call_id": "call_lookup_1", + "name": "lookup", + "arguments": "{\"q\":\"rust\"}" + }, { + "type": "function_call_output", + "call_id": "call_lookup_1", + "output": "{\"ok\":true}" + }] + }); + + let converted = convert_request_pure("openai:responses", "openai:chat", &body) + .expect("pure conversion should succeed") + .value; + + assert_eq!( + converted["messages"][0]["tool_calls"][0]["id"], + "call_lookup_1" + ); + assert_eq!(converted["messages"][1]["tool_call_id"], "call_lookup_1"); + } + + #[test] + fn pure_gemini_to_openai_chat_maps_function_response_id_to_tool_call_id() { + let body = json!({ + "contents": [{ + "role": "model", + "parts": [{ + "functionCall": { + "id": "call_lookup_1", + "name": "lookup", + "args": {"q": "rust"} + } + }] + }, { + "role": "user", + "parts": [{ + "functionResponse": { + "id": "call_lookup_1", + "name": "lookup", + "response": {"result": {"ok": true}} + } + }] + }] + }); + + let converted = convert_request_pure("gemini:generate_content", "openai:chat", &body) + .expect("pure conversion should succeed") + .value; + + assert_eq!( + converted["messages"][0]["tool_calls"][0]["id"], + "call_lookup_1" + ); + assert_eq!(converted["messages"][1]["tool_call_id"], "call_lookup_1"); + } + + #[test] + fn pure_claude_to_openai_chat_maps_disable_parallel_tool_use() { + let body = json!({ + "model": "claude-sonnet", + "messages": [{"role": "user", "content": "hello"}], + "max_tokens": 64, + "tool_choice": { + "type": "auto", + "disable_parallel_tool_use": true + } + }); + + let converted = convert_request_pure("claude:messages", "openai:chat", &body) + .expect("pure conversion should succeed") + .value; + + assert_eq!(converted["parallel_tool_calls"], false); + } + + #[test] + fn pure_claude_to_openai_chat_clamps_max_output_effort_to_high() { + let body = json!({ + "model": "claude-sonnet", + "messages": [{"role": "user", "content": "hello"}], + "max_tokens": 64, + "output_config": { + "effort": "max" + } + }); + + let converted = convert_request_pure("claude:messages", "openai:chat", &body) + .expect("pure conversion should succeed") + .value; + + assert_eq!(converted["reasoning_effort"], "high"); + } + + #[test] + fn pure_openai_chat_to_claude_maps_parallel_tool_calls() { + let body = json!({ + "model": "gpt-source", + "messages": [{"role": "user", "content": "hello"}], + "parallel_tool_calls": false + }); + + let converted = convert_request_pure("openai:chat", "claude:messages", &body) + .expect("pure conversion should succeed") + .value; + + assert_eq!(converted["tool_choice"]["type"], "auto"); + assert_eq!(converted["tool_choice"]["disable_parallel_tool_use"], true); + } + + #[test] + fn pure_claude_to_openai_responses_blocks_message_thinking_loss() { + let body = json!({ + "model": "claude-sonnet", + "messages": [{ + "role": "assistant", + "content": [{ + "type": "thinking", + "thinking": "plan", + "signature": "sig_123" + }] + }], + "max_tokens": 64 + }); + + let error = convert_request_pure("claude:messages", "openai:responses", &body) + .expect_err("Claude thinking blocks should fail closed for Responses"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "messages[].content[].type" + )); + } + + #[test] + fn pure_claude_to_openai_chat_blocks_structured_tool_result_loss() { + let body = json!({ + "model": "claude-sonnet", + "messages": [{ + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": "toolu_123", + "content": [{ + "type": "text", + "text": "first" + }, { + "type": "text", + "text": "second" + }] + }] + }], + "max_tokens": 64 + }); + + let error = convert_request_pure("claude:messages", "openai:chat", &body) + .expect_err("Claude tool_result block arrays should fail closed for Chat"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "messages[].content[].tool_result.content" + )); + } + + #[test] + fn pure_claude_to_gemini_blocks_redacted_thinking_loss() { + let body = json!({ + "model": "claude-sonnet", + "messages": [{ + "role": "assistant", + "content": [{ + "type": "redacted_thinking", + "data": "enc_123" + }] + }], + "max_tokens": 64 + }); + + let error = convert_request_pure("claude:messages", "gemini:generate_content", &body) + .expect_err("Claude redacted thinking should fail closed for Gemini"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "messages[].content[].type" + )); + } + + #[test] + fn pure_gemini_to_openai_chat_maps_allowed_function_names_to_named_tool_choice() { + let body = json!({ + "contents": [{"role": "user", "parts": [{"text": "hello"}]}], + "toolConfig": { + "functionCallingConfig": { + "mode": "ANY", + "allowedFunctionNames": ["lookup"] + } + } + }); + + let converted = convert_request_pure("gemini:generate_content", "openai:chat", &body) + .expect("pure conversion should succeed") + .value; + + assert_eq!(converted["tool_choice"]["type"], "function"); + assert_eq!(converted["tool_choice"]["function"]["name"], "lookup"); + } + + #[test] + fn pure_gemini_to_openai_chat_maps_thinking_level_to_reasoning_effort() { + let body = json!({ + "contents": [{"role": "user", "parts": [{"text": "hello"}]}], + "generationConfig": { + "thinkingConfig": { + "includeThoughts": true, + "thinkingLevel": "high" + } + } + }); + + let converted = convert_request_pure("gemini:generate_content", "openai:chat", &body) + .expect("pure conversion should succeed") + .value; + + assert_eq!(converted["reasoning_effort"], "high"); + } + + #[test] + fn pure_openai_chat_to_gemini_maps_named_tool_choice_to_allowed_function_names() { + let body = json!({ + "model": "gpt-source", + "messages": [{"role": "user", "content": "hello"}], + "tool_choice": { + "type": "function", + "function": {"name": "lookup"} + } + }); + + let converted = convert_request_pure("openai:chat", "gemini:generate_content", &body) + .expect("pure conversion should succeed") + .value; + + assert_eq!( + converted["toolConfig"]["functionCallingConfig"]["allowedFunctionNames"][0], + "lookup" + ); + } + + #[test] + fn pure_gemini_to_openai_responses_blocks_thought_part_loss() { + let body = json!({ + "contents": [{ + "role": "model", + "parts": [{ + "text": "plan", + "thought": true, + "thoughtSignature": "sig_123" + }] + }] + }); + + let error = convert_request_pure("gemini:generate_content", "openai:responses", &body) + .expect_err("Gemini thought parts should fail closed for Responses"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "contents[].parts[].thoughtSignature" + )); + } + + #[test] + fn pure_gemini_to_openai_chat_blocks_safety_settings_loss() { + let body = json!({ + "contents": [{"role": "user", "parts": [{"text": "hello"}]}], + "safetySettings": [{ + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "threshold": "BLOCK_NONE" + }] + }); + + let error = convert_request_pure("gemini:generate_content", "openai:chat", &body) + .expect_err("Gemini safetySettings should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "safetySettings" + )); + } + + #[test] + fn pure_gemini_to_openai_chat_blocks_response_modalities_loss() { + let body = json!({ + "contents": [{"role": "user", "parts": [{"text": "hello"}]}], + "generationConfig": { + "responseModalities": ["TEXT", "IMAGE"] + } + }); + + let error = convert_request_pure("gemini:generate_content", "openai:chat", &body) + .expect_err("Gemini responseModalities should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "generationConfig.responseModalities" + )); + } + + #[test] + fn pure_gemini_to_openai_chat_blocks_multiple_allowed_function_names() { + let body = json!({ + "contents": [{"role": "user", "parts": [{"text": "hello"}]}], + "toolConfig": { + "functionCallingConfig": { + "mode": "ANY", + "allowedFunctionNames": ["lookup", "search"] + } + } + }); + + let error = convert_request_pure("gemini:generate_content", "openai:chat", &body) + .expect_err("multiple Gemini allowed function names should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "toolConfig.functionCallingConfig.allowedFunctionNames" + )); + } + + #[test] + fn pure_gemini_to_openai_chat_rejects_invalid_tool_mode_enum() { + let body = json!({ + "contents": [{"role": "user", "parts": [{"text": "hello"}]}], + "toolConfig": { + "functionCallingConfig": { + "mode": "SOMETIME" + } + } + }); + + let error = convert_request_pure("gemini:generate_content", "openai:chat", &body) + .expect_err("invalid Gemini tool mode should fail closed"); + + assert!(matches!( + error, + super::FormatError::InvalidEnumValue { ref field, ref value, .. } + if field == "toolConfig.functionCallingConfig.mode" && value == "SOMETIME" + )); + } + + #[test] + fn pure_gemini_to_claude_blocks_code_execution_tool_loss() { + let body = json!({ + "contents": [{"role": "user", "parts": [{"text": "hello"}]}], + "tools": [{ + "codeExecution": {} + }] + }); + + let error = convert_request_pure("gemini:generate_content", "claude:messages", &body) + .expect_err("Gemini codeExecution should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "tools[].codeExecution" + )); + } + + #[test] + fn pure_claude_to_openai_chat_blocks_cache_control_loss() { + let body = json!({ + "model": "claude-sonnet", + "system": [{ + "type": "text", + "text": "Cache this.", + "cache_control": {"type": "ephemeral"} + }], + "messages": [{"role": "user", "content": "hello"}], + "max_tokens": 64 + }); + + let error = convert_request_pure("claude:messages", "openai:chat", &body) + .expect_err("Claude cache_control should fail closed cross-format"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "cache_control" + )); + } + + #[test] + fn pure_claude_to_openai_chat_allows_tool_schema_property_named_cache_control() { + let body = json!({ + "model": "claude-sonnet", + "messages": [{"role": "user", "content": "hello"}], + "max_tokens": 64, + "tools": [{ + "name": "configure_cache", + "description": "Configure application cache behavior", + "input_schema": { + "type": "object", + "properties": { + "cache_control": { + "type": "string", + "description": "Application-level cache policy" + } + } + } + }] + }); + + let converted = convert_request_pure("claude:messages", "openai:chat", &body) + .expect("tool schema property names should not be treated as Claude cache_control") + .value; + + assert_eq!( + converted["tools"][0]["function"]["parameters"]["properties"]["cache_control"]["type"], + "string" + ); + } + + #[test] + fn pure_openai_chat_to_responses_blocks_lossy_chat_only_fields() { + let body = json!({ + "model": "gpt-source", + "messages": [{"role": "user", "content": "hello"}], + "n": 2 + }); + + let error = convert_request_pure("openai:chat", "openai:responses", &body) + .expect_err("lossy field should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } if field == "n" + )); + } + + #[test] + fn pure_openai_responses_to_chat_blocks_responses_only_fields() { + let body = json!({ + "model": "gpt-source", + "input": [{"role": "user", "content": "hello"}], + "include": ["reasoning.encrypted_content"] + }); + + let error = convert_request_pure("openai:responses", "openai:chat", &body) + .expect_err("lossy field should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } if field == "include" + )); + } + + #[test] + fn pure_cross_format_rejects_unknown_source_root_field() { + let body = json!({ + "model": "gpt-source", + "messages": [{"role": "user", "content": "hello"}], + "future_field": true + }); + + let error = convert_request_pure("openai:chat", "gemini:generate_content", &body) + .expect_err("unknown source root field should fail closed"); + + assert!(matches!( + error, + super::FormatError::UnauditedField { + ref source_format, + ref target_format, + ref field, + .. + } if source_format == "openai:chat" + && target_format == "gemini:generate_content" + && field == "future_field" + )); + } + + #[test] + fn same_format_canonical_roundtrip_preserves_future_root_fields() { + let cases = [ + ( + "openai:chat", + json!({ + "model": "gpt-source", + "messages": [{"role": "user", "content": "hello"}], + "future_field": {"enabled": true} + }), + "future_field", + ), + ( + "openai:responses", + json!({ + "model": "gpt-source", + "input": [{"role": "user", "content": "hello"}], + "future_field": {"enabled": true} + }), + "future_field", + ), + ( + "claude:messages", + json!({ + "model": "claude-source", + "max_tokens": 1024, + "messages": [{"role": "user", "content": "hello"}], + "future_field": {"enabled": true} + }), + "future_field", + ), + ( + "gemini:generate_content", + json!({ + "model": "gemini-source", + "contents": [{"role": "user", "parts": [{"text": "hello"}]}], + "futureField": {"enabled": true} + }), + "futureField", + ), + ]; + + for (format, body, field) in cases { + let converted = convert_request_pure(format, format, &body) + .unwrap_or_else(|err| panic!("{format} same-format roundtrip failed: {err}")); + assert_eq!( + converted.value.get(field), + body.get(field), + "{format} must preserve unknown provider root fields in canonical roundtrip" + ); + } + } + + #[test] + fn pure_openai_chat_to_claude_blocks_target_unsupported_generation_field() { + let body = json!({ + "model": "gpt-source", + "messages": [{"role": "user", "content": "hello"}], + "top_logprobs": 2 + }); + + let error = convert_request_pure("openai:chat", "claude:messages", &body) + .expect_err("target-unsupported generation field should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "top_logprobs" + )); + } + + #[test] + fn pure_openai_chat_to_gemini_blocks_unknown_content_part_loss() { + let body = json!({ + "model": "gpt-source", + "messages": [{ + "role": "user", + "content": [{ + "type": "input_video", + "input_video": {"url": "https://example.com/movie.mp4"} + }] + }] + }); + + let error = convert_request_pure("openai:chat", "gemini:generate_content", &body) + .expect_err("unknown content part should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "messages[].content[].type" + )); + } + + #[test] + fn pure_openai_responses_to_chat_blocks_unmapped_context_management() { + let body = json!({ + "model": "gpt-source", + "input": [{"role": "user", "content": "hello"}], + "context_management": [] + }); + + let error = convert_request_pure("openai:responses", "openai:chat", &body) + .expect_err("Responses context_management should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "openai_responses.context_management" + )); + } + + #[test] + fn pure_claude_to_openai_chat_blocks_container_loss() { + let body = json!({ + "model": "claude-sonnet", + "messages": [{"role": "user", "content": "hello"}], + "max_tokens": 64, + "container": "container_123" + }); + + let error = convert_request_pure("claude:messages", "openai:chat", &body) + .expect_err("Claude container should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "claude.container" + )); + } + + #[test] + fn pure_gemini_to_openai_chat_blocks_service_tier_loss() { + let body = json!({ + "contents": [{"role": "user", "parts": [{"text": "hello"}]}], + "serviceTier": "priority" + }); + + let error = convert_request_pure("gemini:generate_content", "openai:chat", &body) + .expect_err("Gemini serviceTier should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "gemini.serviceTier" + )); + } + + #[test] + fn pure_openai_cross_format_rejects_invalid_reasoning_effort_enum() { + let body = json!({ + "model": "gpt-source", + "messages": [{"role": "user", "content": "hello"}], + "reasoning_effort": "max" + }); + + let error = convert_request_pure("openai:chat", "openai:responses", &body) + .expect_err("invalid OpenAI enum should fail closed"); + + assert!(matches!( + error, + super::FormatError::InvalidEnumValue { ref field, ref value, .. } + if field == "reasoning_effort" && value == "max" + )); + } + + #[test] + fn pure_openai_responses_to_chat_rejects_invalid_reasoning_effort_enum() { + let body = json!({ + "model": "gpt-source", + "input": [{"role": "user", "content": "hello"}], + "reasoning": { + "effort": "max" + } + }); + + let error = convert_request_pure("openai:responses", "openai:chat", &body) + .expect_err("invalid Responses reasoning enum should fail closed"); + + assert!(matches!( + error, + super::FormatError::InvalidEnumValue { ref field, ref value, .. } + if field == "reasoning.effort" && value == "max" + )); + } + + #[test] + fn pure_response_conversion_rejects_unknown_openai_chat_finish_reason() { + let body = json!({ + "id": "chatcmpl_unknown_finish", + "object": "chat.completion", + "model": "gpt-source", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "hello" + }, + "finish_reason": "future_reason" + }] + }); + + let error = convert_response_pure("openai:chat", "claude:messages", &body) + .expect_err("unknown OpenAI finish reason should fail closed cross-format"); + + assert!(matches!( + error, + super::FormatError::InvalidEnumValue { ref field, ref value, .. } + if field == "choices[].finish_reason" && value == "future_reason" + )); + } + + #[test] + fn pure_response_conversion_blocks_known_but_unmappable_gemini_finish_reason() { + let body = json!({ + "responseId": "resp_other_finish", + "modelVersion": "gemini-2.5-pro", + "candidates": [{ + "index": 0, + "content": { + "role": "model", + "parts": [{"text": "hello"}] + }, + "finishReason": "OTHER" + }] + }); + + let error = convert_response_pure("gemini:generate_content", "openai:chat", &body) + .expect_err("unmappable Gemini finish reason should fail closed cross-format"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "candidates[].finishReason" + )); + } + + #[test] + fn pure_response_conversion_preserves_openai_responses_content_filter_finish_reason() { + let body = json!({ + "id": "resp_content_filter", + "object": "response", + "model": "gpt-source", + "status": "incomplete", + "incomplete_details": { + "reason": "content_filter" + }, + "output": [{ + "type": "message", + "id": "msg_content_filter", + "role": "assistant", + "content": [{ + "type": "output_text", + "text": "blocked" + }] + }] + }); + + let converted = convert_response_pure("openai:responses", "openai:chat", &body) + .expect("content_filter incomplete reason should map to Chat") + .value; + + assert_eq!(converted["choices"][0]["finish_reason"], "content_filter"); + } + + #[test] + fn pure_response_conversion_reports_response_field_mappings() { + let body = json!({ + "id": "resp_report", + "object": "response", + "model": "gpt-source", + "status": "completed", + "output": [{ + "type": "message", + "id": "msg_report", + "role": "assistant", + "content": [{ + "type": "output_text", + "text": "hello" + }] + }] + }); + + let converted = convert_response_pure("openai:responses", "openai:chat", &body) + .expect("response conversion should succeed"); + + assert!(converted.report.fields.iter().any(|field| { + field.field == "output" && field.status == super::ConversionFieldStatus::Mapped + })); + assert!(converted.report.fields.iter().any(|field| { + field.field == "choices" && field.status == super::ConversionFieldStatus::Mapped + })); + } + + #[test] + fn pure_response_conversion_blocks_non_terminal_openai_responses_status() { + let body = json!({ + "id": "resp_queued", + "object": "response", + "model": "gpt-source", + "status": "queued", + "output": [{ + "type": "message", + "id": "msg_queued", + "role": "assistant", + "content": [{ + "type": "output_text", + "text": "not final" + }] + }] + }); + + let error = convert_response_pure("openai:responses", "openai:chat", &body) + .expect_err("non-terminal Responses status should fail closed"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } if field == "status" + )); + } + + #[test] + fn pure_openai_chat_response_same_format_preserves_unknown_finish_reason() { + let body = json!({ + "id": "chatcmpl_unknown_finish", + "object": "chat.completion", + "model": "gpt-source", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "hello" + }, + "finish_reason": "future_reason" + }] + }); + + let converted = convert_response_pure("openai:chat", "openai:chat", &body) + .expect("same-format response roundtrip should preserve unknown finish reason") + .value; + + assert_eq!(converted["choices"][0]["finish_reason"], "future_reason"); + } + + #[test] + fn pure_openai_responses_response_same_format_preserves_incomplete_status() { + let body = json!({ + "id": "resp_incomplete", + "object": "response", + "model": "gpt-source", + "status": "incomplete", + "incomplete_details": { + "reason": "max_output_tokens" + }, + "output": [{ + "type": "message", + "id": "msg_incomplete", + "role": "assistant", + "content": [{ + "type": "output_text", + "text": "partial" + }] + }] + }); + + let converted = convert_response_pure("openai:responses", "openai:responses", &body) + .expect("same-format response roundtrip should preserve Responses status") + .value; + + assert_eq!(converted["status"], "incomplete"); + assert_eq!( + converted["incomplete_details"]["reason"], + "max_output_tokens" + ); + } + + #[test] + fn pure_claude_response_same_format_preserves_unknown_stop_reason() { + let body = json!({ + "id": "msg_unknown_stop", + "type": "message", + "role": "assistant", + "model": "claude-sonnet", + "content": [{ + "type": "text", + "text": "hello" + }], + "stop_reason": "future_reason", + "stop_sequence": null, + "usage": { + "input_tokens": 1, + "output_tokens": 1 + } + }); + + let converted = convert_response_pure("claude:messages", "claude:messages", &body) + .expect("same-format response roundtrip should preserve unknown stop reason") + .value; + + assert_eq!(converted["stop_reason"], "future_reason"); + assert_eq!(converted["stop_sequence"], json!(null)); + } + + #[test] + fn pure_gemini_response_same_format_preserves_unknown_finish_reason() { + let body = json!({ + "responseId": "resp_unknown_finish", + "modelVersion": "gemini-2.5-pro", + "candidates": [{ + "index": 0, + "content": { + "role": "model", + "parts": [{"text": "hello"}] + }, + "finishReason": "FUTURE_REASON" + }] + }); + + let converted = + convert_response_pure("gemini:generate_content", "gemini:generate_content", &body) + .expect("same-format response roundtrip should preserve unknown finish reason") + .value; + + assert_eq!(converted["candidates"][0]["finishReason"], "FUTURE_REASON"); + } + + #[test] + fn pure_claude_same_format_roundtrip_preserves_raw_tool_input_schema() { + let body = json!({ + "model": "claude-sonnet", + "messages": [{"role": "user", "content": "hello"}], + "max_tokens": 64, + "tools": [{ + "name": "lookup", + "description": "Lookup", + "input_schema": {"type": "object"} + }] + }); + + let converted = convert_request_pure("claude:messages", "claude:messages", &body) + .expect("same-format roundtrip should succeed") + .value; + + assert_eq!( + converted["tools"][0]["input_schema"], + json!({"type": "object"}) + ); + } + + #[test] + fn pure_gemini_same_format_roundtrip_preserves_raw_function_parameters() { + let body = json!({ + "contents": [{"role": "user", "parts": [{"text": "hello"}]}], + "tools": [{ + "functionDeclarations": [{ + "name": "lookup", + "description": "Lookup", + "parameters": {"type": "object"} + }] + }] + }); + + let converted = + convert_request_pure("gemini:generate_content", "gemini:generate_content", &body) + .expect("same-format roundtrip should succeed") + .value; + + assert_eq!( + converted["tools"][0]["functionDeclarations"][0]["parameters"], + json!({"type": "object"}) + ); + } + + #[test] + fn legacy_openai_responses_to_chat_does_not_leak_responses_only_extensions() { + let body = json!({ + "model": "gpt-source", + "input": [{"role": "user", "content": "hello"}], + "include": ["reasoning.encrypted_content"], + "previous_response_id": "resp_123", + "stream": true + }); + + let converted = convert_request( + "openai:responses", + "openai:chat", + &body, + &FormatContext::default(), + ) + .expect("legacy conversion should still emit a chat body"); + + assert!(converted.get("stream").is_none()); + assert!(converted.get("include").is_none()); + assert!(converted.get("previous_response_id").is_none()); + } + #[test] fn converts_openai_embedding_to_jina_without_chat_fields() { let body = json!({ @@ -292,6 +3663,140 @@ mod tests { assert!(converted.get("messages").is_none()); } + #[test] + fn pure_embedding_conversion_parses_gemini_source_to_openai() { + let body = json!({ + "model": "models/gemini-embedding-001", + "content": { + "parts": [{"text": "alpha"}] + }, + "outputDimensionality": 768 + }); + + let converted = convert_request_pure("gemini:embedding", "openai:embedding", &body) + .expect("Gemini embedding source should parse") + .value; + + assert_eq!(converted["model"], "models/gemini-embedding-001"); + assert_eq!(converted["input"], "alpha"); + assert_eq!(converted["dimensions"], 768); + } + + #[test] + fn pure_embedding_conversion_blocks_gemini_task_to_openai() { + let body = json!({ + "model": "models/gemini-embedding-001", + "content": { + "parts": [{"text": "alpha"}] + }, + "taskType": "RETRIEVAL_QUERY" + }); + + let error = convert_request_pure("gemini:embedding", "openai:embedding", &body) + .expect_err("OpenAI embeddings have no task field"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } if field == "task" + )); + } + + #[test] + fn pure_embedding_conversion_parses_aliyun_text_source_to_openai() { + let body = json!({ + "model": "qwen3-vl-embedding", + "input": { + "contents": [ + {"text": "alpha"}, + {"text": "beta"} + ] + }, + "parameters": { + "dimension": 1024 + } + }); + + let converted = + convert_request_pure("aliyun:multimodal_embedding", "openai:embedding", &body) + .expect("Aliyun text embedding source should parse") + .value; + + assert_eq!(converted["model"], "qwen3-vl-embedding"); + assert_eq!(converted["input"], json!(["alpha", "beta"])); + assert_eq!(converted["dimensions"], 1024); + } + + #[test] + fn pure_embedding_conversion_blocks_token_input_to_gemini() { + let body = json!({ + "model": "text-embedding-3-small", + "input": [1, 2, 3] + }); + + let error = convert_request_pure("openai:embedding", "gemini:embedding", &body) + .expect_err("Gemini cannot receive token-array embedding input"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } if field == "input" + )); + } + + #[test] + fn pure_embedding_conversion_blocks_openai_only_fields_to_doubao() { + let body = json!({ + "model": "text-embedding-3-small", + "input": "alpha", + "encoding_format": "base64" + }); + + let error = convert_request_pure("openai:embedding", "doubao:embedding", &body) + .expect_err("Doubao cannot carry OpenAI encoding_format"); + + assert!(matches!( + error, + super::FormatError::LossyConversionBlocked { ref field, .. } + if field == "encoding_format" + )); + } + + #[test] + fn pure_embedding_conversion_blocks_unknown_provider_fields_cross_format() { + let body = json!({ + "model": "text-embedding-3-small", + "input": "alpha", + "unknown_vendor_field": true + }); + + let error = convert_request_pure("openai:embedding", "jina:embedding", &body) + .expect_err("unknown provider fields cannot be dropped cross-format"); + + assert!(matches!( + error, + super::FormatError::UnsupportedField { ref field, .. } + if field == "embedding.extensions" + )); + } + + #[test] + fn pure_rerank_conversion_blocks_unknown_provider_fields_cross_format() { + let body = json!({ + "model": "rerank-model", + "query": "rust", + "documents": ["rust book"], + "unknown_vendor_field": true + }); + + let error = convert_request_pure("openai:rerank", "jina:rerank", &body) + .expect_err("unknown rerank provider fields cannot be dropped cross-format"); + + assert!(matches!( + error, + super::FormatError::UnsupportedField { ref field, .. } + if field == "rerank.extensions" + )); + } + #[test] fn aliyun_embedding_conversion_rejects_token_arrays() { let body = json!({ @@ -372,15 +3877,26 @@ mod tests { } #[test] - fn embedding_registry_keeps_gemini_and_doubao_emit_only() { - let body = json!({ + fn embedding_registry_parses_gemini_and_doubao_sources() { + let gemini_body = json!({ "model": "gemini-embedding-001", "content": {"parts": [{"text": "alpha"}]} }); + let doubao_body = json!({ + "model": "doubao-embedding", + "input": ["alpha"] + }); let ctx = FormatContext::default(); - assert!(convert_request("gemini:embedding", "openai:embedding", &body, &ctx).is_err()); - assert!(convert_request("doubao:embedding", "openai:embedding", &body, &ctx).is_err()); + let gemini = convert_request("gemini:embedding", "openai:embedding", &gemini_body, &ctx) + .expect("Gemini embedding source should parse"); + assert_eq!(gemini["model"], "gemini-embedding-001"); + assert_eq!(gemini["input"], "alpha"); + + let doubao = convert_request("doubao:embedding", "openai:embedding", &doubao_body, &ctx) + .expect("Doubao embedding source should parse"); + assert_eq!(doubao["model"], "doubao-embedding"); + assert_eq!(doubao["input"], json!(["alpha"])); } #[test] @@ -429,6 +3945,102 @@ mod tests { } } + #[test] + fn field_coverage_matrix_covers_all_documented_provider_schema_fields() { + let definitions = include_str!("../../../../docs/api/provider-interface-definitions.md"); + let matrix = include_str!("../../../../docs/api/format-field-coverage-matrix.md"); + let documented = parse_documented_schema_fields(definitions); + let covered = parse_field_coverage_matrix_fields(matrix); + + let missing = documented + .difference(&covered) + .take(20) + .cloned() + .collect::>(); + let extra = covered + .difference(&documented) + .take(20) + .cloned() + .collect::>(); + + assert!( + missing.is_empty(), + "field coverage matrix is missing documented schema fields: {missing:?}" + ); + assert!( + extra.is_empty(), + "field coverage matrix contains fields not present in provider definitions: {extra:?}" + ); + assert!( + matrix.contains(&format!( + "Total covered schema fields: {}.", + documented.len() + )), + "field coverage matrix total must match provider-interface-definitions.md" + ); + assert_field_coverage_statuses_are_explicit(matrix); + } + + #[test] + fn request_root_field_whitelists_cover_documented_generation_request_schemas() { + let definitions = include_str!("../../../../docs/api/provider-interface-definitions.md"); + let documented = parse_documented_schema_fields(definitions); + + for (format, provider, schema) in [ + ( + FormatId::OpenAiChat, + "OpenAI", + "CreateChatCompletionRequest", + ), + (FormatId::OpenAiResponses, "OpenAI", "CreateResponse"), + ( + FormatId::OpenAiResponsesCompact, + "OpenAI", + "CompactResponseMethodPublicBody", + ), + ( + FormatId::ClaudeMessages, + "Claude", + "MessageCreateParamsBase", + ), + ( + FormatId::GeminiGenerateContent, + "Gemini", + "GenerateContentRequest", + ), + ] { + let missing = documented + .iter() + .filter(|field| field.provider == provider && field.schema == schema) + .filter(|field| { + !super::standard_request_root_field_is_audited(format, &field.field) + }) + .map(|field| field.field.clone()) + .collect::>(); + assert!( + missing.is_empty(), + "{provider} `{schema}` has root fields missing from runtime audit whitelist: {missing:?}" + ); + } + } + + #[test] + fn format_conversion_audit_has_no_unresolved_field_coverage_markers() { + let audit = include_str!("../../../../docs/api/format-conversion-audit.md"); + for forbidden in [ + "strict audit pending", + "Nested per-field", + "field-by-field decision pending", + "is still pending", + "coverage exists, but strict audit is pending", + ] { + assert!( + !audit.contains(forbidden), + "format conversion audit still contains unresolved marker: {forbidden}" + ); + } + } + #[test] fn registry_does_not_call_wire_specific_canonical_functions_directly() { let implementation = include_str!("registry.rs") @@ -450,4 +4062,146 @@ mod tests { ); } } + + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + struct DocumentedSchemaField { + provider: String, + schema: String, + field: String, + } + + fn parse_documented_schema_fields( + source: &str, + ) -> std::collections::BTreeSet { + let mut fields = std::collections::BTreeSet::new(); + let mut provider: Option<&str> = None; + let mut schema: Option = None; + + for line in source.lines() { + if line.starts_with("## ") { + provider = if line.contains("OpenAI Schema") { + Some("OpenAI") + } else if line.contains("Claude / Anthropic TypeScript") { + Some("Claude") + } else if line.contains("Gemini Schema") { + Some("Gemini") + } else { + None + }; + schema = None; + continue; + } + let Some(active_provider) = provider else { + continue; + }; + if let Some(schema_name) = markdown_code_heading(line) { + schema = Some(schema_name); + continue; + } + let Some(active_schema) = schema.as_ref() else { + continue; + }; + if !line.starts_with("| `") { + continue; + } + let cells = split_markdown_row(line); + if cells.len() < 4 || !matches!(cells[2].as_str(), "是" | "否") { + continue; + } + fields.insert(DocumentedSchemaField { + provider: active_provider.to_string(), + schema: active_schema.clone(), + field: strip_markdown_code(&cells[1]), + }); + } + + fields + } + + fn parse_field_coverage_matrix_fields( + matrix: &str, + ) -> std::collections::BTreeSet { + let mut fields = std::collections::BTreeSet::new(); + for line in matrix.lines() { + if !line.starts_with("| ") { + continue; + } + let cells = split_markdown_row(line); + if cells.len() < 10 || !matches!(cells[1].as_str(), "OpenAI" | "Claude" | "Gemini") { + continue; + } + fields.insert(DocumentedSchemaField { + provider: cells[1].to_string(), + schema: strip_markdown_code(&cells[2]), + field: strip_markdown_code(&cells[3]), + }); + } + fields + } + + fn assert_field_coverage_statuses_are_explicit(matrix: &str) { + const VALID_STATUSES: &[&str] = &[ + "native", + "mapped", + "mapped/lossy-blocked", + "extension-preserved", + "unaudited", + "unsupported", + "invalid-enum", + "lossy-blocked", + "not-in-conversion-surface", + ]; + + for line in matrix.lines() { + if !line.starts_with("| ") { + continue; + } + let cells = split_markdown_row(line); + if cells.len() < 10 || !matches!(cells[1].as_str(), "OpenAI" | "Claude" | "Gemini") { + continue; + } + for index in [7, 8, 9] { + assert!( + VALID_STATUSES.contains(&cells[index].as_str()), + "field coverage matrix has invalid status `{}` in row `{line}`", + cells[index] + ); + } + } + } + + fn markdown_code_heading(line: &str) -> Option { + line.strip_prefix("### `") + .and_then(|rest| rest.split_once('`')) + .map(|(value, _)| value.to_string()) + } + + fn strip_markdown_code(value: &str) -> String { + value + .trim() + .strip_prefix('`') + .and_then(|value| value.strip_suffix('`')) + .unwrap_or_else(|| value.trim()) + .replace("\\|", "|") + } + + fn split_markdown_row(line: &str) -> Vec { + let mut cells = Vec::new(); + let mut current = String::new(); + let mut escaped = false; + for ch in line.chars() { + if ch == '|' && !escaped { + cells.push(current.trim().to_string()); + current.clear(); + } else { + current.push(ch); + } + escaped = ch == '\\' && !escaped; + if escaped && ch != '\\' { + escaped = false; + } + } + cells.push(current.trim().to_string()); + cells + } } diff --git a/crates/aether-ai-formats/src/formats/shared/model_directives.rs b/crates/aether-ai-formats/src/formats/shared/model_directives.rs index 5eb46d1c5..2511df03b 100644 --- a/crates/aether-ai-formats/src/formats/shared/model_directives.rs +++ b/crates/aether-ai-formats/src/formats/shared/model_directives.rs @@ -14,6 +14,8 @@ pub enum ModelOverride { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ReasoningEffort { + None, + Minimal, Low, Medium, High, @@ -24,6 +26,8 @@ pub enum ReasoningEffort { impl ReasoningEffort { pub fn parse(value: &str) -> Option { 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), @@ -35,6 +39,8 @@ impl ReasoningEffort { pub fn as_openai_chat_value(self) -> &'static str { match self { + Self::None => "none", + Self::Minimal => "minimal", Self::Low => "low", Self::Medium => "medium", Self::High => "high", @@ -44,6 +50,8 @@ impl ReasoningEffort { pub fn as_openai_responses_value(self) -> &'static str { match self { + Self::None => "none", + Self::Minimal => "minimal", Self::Low => "low", Self::Medium => "medium", Self::High => "high", @@ -53,6 +61,7 @@ impl ReasoningEffort { pub fn as_claude_output_value(self) -> &'static str { match self { + Self::None | Self::Minimal => "low", Self::Low => "low", Self::Medium => "medium", Self::High => "high", @@ -63,7 +72,7 @@ impl ReasoningEffort { pub fn as_gemini_level_value(self) -> &'static str { match self { - Self::Low => "low", + Self::None | Self::Minimal | Self::Low => "low", Self::Medium => "medium", Self::High | Self::XHigh | Self::Max => "high", } @@ -71,6 +80,8 @@ impl ReasoningEffort { pub fn thinking_budget_tokens(self) -> u64 { match self { + Self::None => 0, + Self::Minimal => 512, Self::Low => 1280, Self::Medium => 2048, Self::High => 4096, diff --git a/crates/aether-ai-formats/src/formats/shared/sse.rs b/crates/aether-ai-formats/src/formats/shared/sse.rs index b2f9ab79d..24fc49a3e 100644 --- a/crates/aether-ai-formats/src/formats/shared/sse.rs +++ b/crates/aether-ai-formats/src/formats/shared/sse.rs @@ -2,19 +2,17 @@ use serde_json::Value; use crate::formats::shared::AiSurfaceFinalizeError; -pub fn map_claude_stop_reason( - stop_reason: Option<&str>, - has_tool_calls: bool, -) -> Option<&'static str> { +pub fn map_claude_stop_reason(stop_reason: Option<&str>, has_tool_calls: bool) -> Option { let mapped = match stop_reason { - Some("end_turn") | Some("stop_sequence") => Some("stop"), - Some("max_tokens") => Some("length"), - Some("tool_use") => Some("tool_calls"), - Some("pause_turn") => Some("stop"), + 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.is_none_or(|value| value == "stop") { - Some("tool_calls") + if has_tool_calls && mapped.as_deref().is_none_or(|value| value == "stop") { + Some("tool_calls".to_string()) } else { mapped } diff --git a/crates/aether-ai-formats/src/formats/shared/standard_matrix.rs b/crates/aether-ai-formats/src/formats/shared/standard_matrix.rs index 441445943..65b3c4e24 100644 --- a/crates/aether-ai-formats/src/formats/shared/standard_matrix.rs +++ b/crates/aether-ai-formats/src/formats/shared/standard_matrix.rs @@ -150,6 +150,10 @@ pub fn build_standard_request_body_with_model_directives_and_request_headers( &mut provider_request_body, provider_api_format, ); + strip_openai_responses_input_content_cache_control( + &mut provider_request_body, + provider_api_format, + ); let require_body_stream_field = body_json .as_object() .is_some_and(|object| object.contains_key("stream")) @@ -246,9 +250,61 @@ pub fn build_standard_request_body_from_canonical_with_model_directives( None, ); } + strip_openai_responses_input_content_cache_control( + &mut provider_request_body, + provider_api_format, + ); Some(provider_request_body) } +fn strip_openai_responses_input_content_cache_control( + provider_request_body: &mut Value, + provider_api_format: &str, +) { + if !matches!( + aether_ai_formats::normalize_api_format_alias(provider_api_format).as_str(), + "openai:responses" | "openai:responses:compact" + ) { + return; + } + let Some(input) = provider_request_body.get_mut("input") else { + return; + }; + strip_responses_input_items_content_cache_control(input); +} + +fn strip_responses_input_items_content_cache_control(value: &mut Value) { + match value { + Value::Array(items) => { + for item in items { + strip_responses_input_items_content_cache_control(item); + } + } + Value::Object(item) => { + if let Some(content) = item.get_mut("content") { + strip_responses_content_cache_control(content); + } + } + _ => {} + } +} + +fn strip_responses_content_cache_control(content: &mut Value) { + match content { + Value::Array(parts) => { + for part in parts { + if let Some(part) = part.as_object_mut() { + part.remove("cache_control"); + } + } + } + Value::Object(part) => { + part.remove("cache_control"); + } + _ => {} + } +} + pub fn normalize_standard_request_to_openai_chat_request( body_json: &Value, client_api_format: &str, @@ -1236,6 +1292,98 @@ mod tests { } } + #[test] + fn standard_openai_responses_strips_content_cache_control_after_body_rules() { + let request = json!({ + "model": "gpt-5.1", + "input": [{ + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "hello"}] + }], + "prompt_cache_key": "cache_123" + }); + let body_rules = json!([ + { + "action": "set", + "path": "input[0].content[0].cache_control", + "value": {"type": "ephemeral"} + } + ]); + + let converted = build_standard_request_body( + &request, + "openai:responses", + "gpt-5.1", + "openai", + "openai:responses", + "/v1/responses", + false, + Some(&body_rules), + None, + ) + .expect("responses request should build"); + + assert_eq!(converted["prompt_cache_key"], "cache_123"); + assert!(!converted["input"].to_string().contains("cache_control")); + } + + #[test] + fn standard_codex_responses_derives_prompt_cache_key_before_stripping_cache_control() { + fn claude_request(user_text: &str) -> Value { + json!({ + "model": "claude-sonnet", + "system": [{ + "type": "text", + "text": "stable system brief", + "cache_control": {"type": "ephemeral"} + }], + "messages": [{ + "role": "user", + "content": [{"type": "text", "text": user_text}] + }], + "max_tokens": 128 + }) + } + + let body_a = claude_request("new turn A"); + let body_b = claude_request("new turn B"); + let converted_a = build_standard_request_body( + &body_a, + "claude:messages", + "gpt-5.4", + "codex", + "openai:responses", + "/v1/messages", + true, + None, + Some("key-a"), + ) + .expect("claude to codex responses request should build"); + let converted_b = build_standard_request_body( + &body_b, + "claude:messages", + "gpt-5.4", + "codex", + "openai:responses", + "/v1/messages", + true, + None, + Some("key-a"), + ) + .expect("claude to codex responses request should build"); + + assert!(converted_a["prompt_cache_key"] + .as_str() + .is_some_and(|value| !value.trim().is_empty())); + assert_eq!( + converted_a["prompt_cache_key"], + converted_b["prompt_cache_key"] + ); + assert!(!converted_a.to_string().contains("cache_control")); + assert!(!converted_b.to_string().contains("cache_control")); + } + #[test] fn builds_openai_chat_request_from_claude_chat_source() { let request = json!({ diff --git a/crates/aether-ai-formats/src/formats/shared/stream_core/common.rs b/crates/aether-ai-formats/src/formats/shared/stream_core/common.rs index 4a7bc4970..f881c12e7 100644 --- a/crates/aether-ai-formats/src/formats/shared/stream_core/common.rs +++ b/crates/aether-ai-formats/src/formats/shared/stream_core/common.rs @@ -19,6 +19,93 @@ pub fn decode_json_data_line(line: &[u8]) -> Option { 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 { + 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 { + 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 { + let object = payload.as_object()?; + if object.is_empty() { + return None; + } + let fields = object + .keys() + .take(8) + .map(|key| key.as_str()) + .collect::>() + .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>, @@ -110,26 +197,27 @@ pub fn canonical_usage_from_openai_usage(value: Option<&Value>) -> Option 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 payload.get("error").is_some() { - return true; - } - if matches!( - event_type, - "error" | "response.failed" | "response.incomplete" - ) { + if matches!(event_type, "error" | "response.failed") { return true; } - payload - .get("response") - .and_then(Value::as_object) + response .and_then(|response| response.get("status")) .and_then(Value::as_str) - .is_some_and(|status| matches!(status, "failed" | "incomplete")) + .is_some_and(|status| status == "failed") } pub fn openai_stream_terminal_error_body(payload: &Value) -> Option { @@ -699,3 +787,38 @@ fn inclusive_total_tokens_from_usage(usage: &CanonicalUsage, input_tokens: u64) 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()); + } +} diff --git a/crates/aether-ai-formats/src/formats/shared/stream_core/format_matrix.rs b/crates/aether-ai-formats/src/formats/shared/stream_core/format_matrix.rs index 54615b91a..393c62f4a 100644 --- a/crates/aether-ai-formats/src/formats/shared/stream_core/format_matrix.rs +++ b/crates/aether-ai-formats/src/formats/shared/stream_core/format_matrix.rs @@ -15,7 +15,7 @@ use crate::formats::shared::error_body::{ use crate::formats::shared::sse::encode_json_sse; use crate::formats::shared::stream_core::common::{ decode_json_data_line, openai_stream_terminal_error_body, openai_stream_terminal_error_message, - CanonicalStreamEvent, CanonicalStreamFrame, CanonicalUsage, + unsupported_stream_event_message, CanonicalStreamEvent, CanonicalStreamFrame, CanonicalUsage, }; use crate::formats::shared::AiSurfaceFinalizeError; @@ -84,6 +84,22 @@ impl StreamingStandardFormatMatrix { }; let mut out = Vec::new(); for frame in frames { + if let CanonicalStreamEvent::Finish { + finish_reason: Some(ref finish_reason), + .. + } = frame.event + { + if !canonical_stream_finish_reason_is_supported(finish_reason) { + self.terminated = true; + out.extend(client.emit_unsupported_finish_reason(finish_reason)?); + break; + } + } + if let CanonicalStreamEvent::UnknownEvent(payload) = &frame.event { + self.terminated = true; + out.extend(client.emit_unknown_event(payload)?); + break; + } out.extend(client.emit(frame)?); } Ok(out) @@ -386,6 +402,52 @@ impl ClientStreamEmitter { } } } + + fn emit_unknown_event(&mut self, payload: &Value) -> Result, AiSurfaceFinalizeError> { + let Some(error_body) = build_core_error_body_for_client_format( + self.api_format(), + &unsupported_stream_event_message(payload), + Some("unsupported_stream_event"), + LocalCoreSyncErrorKind::ServerError, + ) else { + return Ok(Vec::new()); + }; + self.emit_error(error_body) + } + + fn emit_unsupported_finish_reason( + &mut self, + finish_reason: &str, + ) -> Result, AiSurfaceFinalizeError> { + let Some(error_body) = build_core_error_body_for_client_format( + self.api_format(), + &format!( + "Unsupported provider stream finish reason cannot be converted losslessly: field $.finish_reason = {}", + serde_json::json!(finish_reason) + ), + Some("unsupported_finish_reason"), + LocalCoreSyncErrorKind::ServerError, + ) else { + return Ok(Vec::new()); + }; + self.emit_error(error_body) + } + + fn api_format(&self) -> &'static str { + match self { + ClientStreamEmitter::OpenAIChat(_) => "openai:chat", + ClientStreamEmitter::OpenAIResponses(_) => "openai:responses", + ClientStreamEmitter::Claude(_) => "claude:messages", + ClientStreamEmitter::Gemini(_) => "gemini:generate_content", + } + } +} + +fn canonical_stream_finish_reason_is_supported(finish_reason: &str) -> bool { + matches!( + finish_reason.trim(), + "stop" | "length" | "tool_calls" | "function_call" | "content_filter" + ) } fn build_client_error_body_for_line(report_context: &Value, line: &[u8]) -> Option { @@ -850,6 +912,485 @@ mod tests { } } + #[test] + fn transforms_unknown_provider_stream_events_to_visible_client_errors() { + let cases = [ + ( + "openai:chat", + "data: {\"error\":", + "\"code\":\"unsupported_stream_event\"", + ), + ( + "openai:responses", + "event: response.failed\n", + "\"code\":\"unsupported_stream_event\"", + ), + ( + "claude:messages", + "event: error\n", + "\"code\":\"unsupported_stream_event\"", + ), + ( + "gemini:generate_content", + "data: {\"error\":", + "\"status\":\"INTERNAL\"", + ), + ]; + + for (client_api_format, prefix, marker) in cases { + let mut report_context = report_context("openai:responses", client_api_format); + report_context["provider_stream_event_api_format"] = json!("openai:responses"); + let mut matrix = StreamingStandardFormatMatrix::default(); + let output = matrix + .transform_line( + &report_context, + data_line(json!({ + "type": "response.future.delta", + "response": { + "id": "resp_unknown_123", + "model": "gpt-5.4", + }, + "payload": { + "kept": true, + }, + })), + ) + .expect("unknown provider event should fail closed visibly"); + let sse = String::from_utf8(output).expect("sse should be utf8"); + + assert!(sse.contains(prefix), "{client_api_format}: {sse}"); + assert!( + sse.contains("Unsupported provider stream event cannot be converted losslessly"), + "{client_api_format}: {sse}" + ); + assert!( + sse.contains("field $.type = \\\"response.future.delta\\\""), + "{sse}" + ); + assert!(sse.contains(marker), "{client_api_format}: {sse}"); + assert!(matrix + .finish(&report_context) + .expect("finish should succeed") + .is_empty()); + assert!(matrix + .transform_line( + &report_context, + data_line(json!({ + "type": "response.output_text.delta", + "response_id": "resp_unknown_123", + "output_index": 0, + "content_index": 0, + "delta": "after", + })), + ) + .expect("terminated matrix should ignore later lines") + .is_empty()); + } + } + + #[test] + fn transforms_openai_responses_known_sidecar_events_without_unsupported_errors() { + let report_context = report_context("openai:responses", "claude:messages"); + let mut matrix = StreamingStandardFormatMatrix::default(); + let mut output = Vec::new(); + + for line in [ + data_line(json!({ + "type": "response.created", + "response": { + "id": "resp_sidecar_123", + "model": "gpt-5.4", + "status": "in_progress", + "output": [], + }, + })), + data_line(json!({ + "type": "response.output_item.added", + "response_id": "resp_sidecar_123", + "output_index": 0, + "item": { + "type": "web_search_call", + "id": "ws_123", + "status": "in_progress", + "action": {"type": "search", "query": "aether format conversion"}, + }, + })), + data_line(json!({ + "type": "response.web_search_call.searching", + "item_id": "ws_123", + "output_index": 0, + })), + data_line(json!({ + "type": "response.output_text.annotation.added", + "response_id": "resp_sidecar_123", + "output_index": 1, + "content_index": 0, + "annotation_index": 0, + "annotation": {"type": "url_citation", "url": "https://example.invalid"}, + })), + data_line(json!({ + "type": "response.output_text.delta", + "response_id": "resp_sidecar_123", + "output_index": 1, + "content_index": 0, + "delta": "sidecar ok", + })), + data_line(json!({ + "type": "response.completed", + "response": { + "id": "resp_sidecar_123", + "object": "response", + "model": "gpt-5.4", + "status": "completed", + "output": [], + "usage": { + "input_tokens": 1, + "output_tokens": 2, + "total_tokens": 3, + }, + }, + })), + ] { + output.extend( + matrix + .transform_line(&report_context, line) + .expect("known responses sidecar event should convert or be ignored"), + ); + } + + let sse = String::from_utf8(output).expect("sse should be utf8"); + assert!(!sse.contains("unsupported_stream_event"), "{sse}"); + assert!(!sse.contains("Unsupported provider stream event"), "{sse}"); + assert!(sse.contains("sidecar ok"), "{sse}"); + assert!(sse.contains("event: message_stop"), "{sse}"); + } + + #[test] + fn transforms_openai_responses_incomplete_max_tokens_as_normal_finish() { + let report_context = report_context("openai:responses", "claude:messages"); + let mut matrix = StreamingStandardFormatMatrix::default(); + let output = matrix + .transform_line( + &report_context, + data_line(json!({ + "type": "response.incomplete", + "response": { + "id": "resp_incomplete_123", + "object": "response", + "model": "gpt-5.4", + "status": "incomplete", + "incomplete_details": { + "reason": "max_output_tokens", + }, + "output": [{ + "type": "message", + "id": "msg_incomplete_123", + "role": "assistant", + "status": "incomplete", + "content": [{ + "type": "output_text", + "text": "partial answer", + }], + }], + "usage": { + "input_tokens": 10, + "output_tokens": 20, + "total_tokens": 30, + }, + }, + })), + ) + .expect("incomplete max token response should convert as length finish"); + + let sse = String::from_utf8(output).expect("sse should be utf8"); + assert!(!sse.contains("Response incomplete"), "{sse}"); + assert!(!sse.contains("unsupported_stream_event"), "{sse}"); + assert!(sse.contains("partial answer"), "{sse}"); + assert!(sse.contains("\"stop_reason\":\"max_tokens\""), "{sse}"); + assert!(matrix + .finish(&report_context) + .expect("finish should be terminated") + .is_empty()); + } + + #[test] + fn transforms_openai_responses_local_shell_call_to_claude_tool_use() { + let report_context = report_context("openai:responses", "claude:messages"); + let mut matrix = StreamingStandardFormatMatrix::default(); + let mut output = Vec::new(); + + for line in [ + data_line(json!({ + "type": "response.output_item.done", + "response_id": "resp_shell_123", + "output_index": 0, + "item": { + "type": "local_shell_call", + "id": "lsc_123", + "call_id": "call_shell_123", + "status": "completed", + "action": { + "type": "exec", + "command": ["pwd"], + "env": {}, + }, + }, + })), + data_line(json!({ + "type": "response.completed", + "response": { + "id": "resp_shell_123", + "object": "response", + "model": "gpt-5.4", + "status": "completed", + "output": [], + }, + })), + ] { + output.extend( + matrix + .transform_line(&report_context, line) + .expect("local shell call should convert to a generic tool use"), + ); + } + + let sse = String::from_utf8(output).expect("sse should be utf8"); + assert!(!sse.contains("unsupported_stream_event"), "{sse}"); + assert!(sse.contains("\"type\":\"tool_use\""), "{sse}"); + assert!(sse.contains("\"name\":\"local_shell\""), "{sse}"); + assert!(sse.contains("\\\"command\\\":[\\\"pwd\\\"]"), "{sse}"); + assert!(sse.contains("\"stop_reason\":\"tool_use\""), "{sse}"); + } + + #[test] + fn transforms_unknown_stream_finish_reasons_to_visible_client_errors() { + let cases = [ + ( + "openai:chat", + "data: {\"error\":", + "\"code\":\"unsupported_finish_reason\"", + ), + ( + "openai:responses", + "event: response.failed\n", + "\"code\":\"unsupported_finish_reason\"", + ), + ( + "claude:messages", + "event: error\n", + "\"code\":\"unsupported_finish_reason\"", + ), + ( + "gemini:generate_content", + "data: {\"error\":", + "\"status\":\"INTERNAL\"", + ), + ]; + + for (client_api_format, prefix, marker) in cases { + let report_context = report_context("openai:chat", client_api_format); + let mut matrix = StreamingStandardFormatMatrix::default(); + let output = matrix + .transform_line( + &report_context, + data_line(json!({ + "id": "chatcmpl_unknown_finish", + "object": "chat.completion.chunk", + "model": "gpt-5.4", + "choices": [{ + "index": 0, + "delta": {}, + "finish_reason": "future_reason" + }], + "usage": { + "prompt_tokens": 1, + "completion_tokens": 2, + "total_tokens": 3 + } + })), + ) + .expect("unknown finish reason should fail closed visibly"); + let sse = String::from_utf8(output).expect("sse should be utf8"); + + assert!(sse.contains(prefix), "{client_api_format}: {sse}"); + assert!( + sse.contains("Unsupported provider stream finish reason"), + "{client_api_format}: {sse}" + ); + assert!( + sse.contains("field $.finish_reason = \\\"future_reason\\\""), + "{client_api_format}: {sse}" + ); + assert!(sse.contains("future_reason"), "{client_api_format}: {sse}"); + assert!(sse.contains(marker), "{client_api_format}: {sse}"); + assert!(matrix + .finish(&report_context) + .expect("finish should succeed") + .is_empty()); + } + } + + #[test] + fn transforms_unmappable_gemini_stream_finish_reasons_to_visible_client_errors() { + let cases = [ + ( + "openai:chat", + "data: {\"error\":", + "\"code\":\"unsupported_finish_reason\"", + ), + ( + "openai:responses", + "event: response.failed\n", + "\"code\":\"unsupported_finish_reason\"", + ), + ( + "claude:messages", + "event: error\n", + "\"code\":\"unsupported_finish_reason\"", + ), + ( + "gemini:generate_content", + "data: {\"error\":", + "\"status\":\"INTERNAL\"", + ), + ]; + + for (client_api_format, prefix, marker) in cases { + let report_context = report_context("gemini:generate_content", client_api_format); + let mut matrix = StreamingStandardFormatMatrix::default(); + let output = matrix + .transform_line( + &report_context, + data_line(json!({ + "responseId": "gemini_unmappable_finish", + "modelVersion": "gemini-2.5-pro", + "candidates": [{ + "index": 0, + "content": { + "role": "model", + "parts": [{"text": "partial"}] + }, + "finishReason": "OTHER" + }], + "usageMetadata": { + "promptTokenCount": 1, + "candidatesTokenCount": 2, + "totalTokenCount": 3 + } + })), + ) + .expect("unmappable Gemini finish reason should fail closed visibly"); + let sse = String::from_utf8(output).expect("sse should be utf8"); + + assert!(sse.contains(prefix), "{client_api_format}: {sse}"); + assert!( + sse.contains("Unsupported provider stream finish reason"), + "{client_api_format}: {sse}" + ); + assert!( + sse.contains("field $.finish_reason = \\\"OTHER\\\""), + "{client_api_format}: {sse}" + ); + assert!(sse.contains("OTHER"), "{client_api_format}: {sse}"); + assert!(sse.contains(marker), "{client_api_format}: {sse}"); + assert!(matrix + .finish(&report_context) + .expect("finish should succeed") + .is_empty()); + } + } + + #[test] + fn transforms_unknown_claude_stream_stop_reasons_to_visible_client_errors() { + let cases = [ + ( + "openai:chat", + "data: {\"error\":", + "\"code\":\"unsupported_finish_reason\"", + ), + ( + "openai:responses", + "event: response.failed\n", + "\"code\":\"unsupported_finish_reason\"", + ), + ( + "claude:messages", + "event: error\n", + "\"code\":\"unsupported_finish_reason\"", + ), + ( + "gemini:generate_content", + "data: {\"error\":", + "\"status\":\"INTERNAL\"", + ), + ]; + + for (client_api_format, prefix, marker) in cases { + let report_context = report_context("claude:messages", client_api_format); + let mut matrix = StreamingStandardFormatMatrix::default(); + let output = matrix + .transform_line( + &report_context, + data_line(json!({ + "type": "message_delta", + "delta": { + "stop_reason": "future_reason" + }, + "usage": { + "input_tokens": 1, + "output_tokens": 2 + } + })), + ) + .expect("unknown Claude stop reason should fail closed visibly"); + let sse = String::from_utf8(output).expect("sse should be utf8"); + + assert!(sse.contains(prefix), "{client_api_format}: {sse}"); + assert!( + sse.contains("Unsupported provider stream finish reason"), + "{client_api_format}: {sse}" + ); + assert!(sse.contains("future_reason"), "{client_api_format}: {sse}"); + assert!(sse.contains(marker), "{client_api_format}: {sse}"); + assert!(matrix + .finish(&report_context) + .expect("finish should succeed") + .is_empty()); + } + } + + #[test] + fn openai_responses_client_emits_incomplete_for_length_finish_reason() { + let report_context = report_context("openai:chat", "openai:responses"); + let mut matrix = StreamingStandardFormatMatrix::default(); + let output = matrix + .transform_line( + &report_context, + data_line(json!({ + "id": "chatcmpl_length_finish", + "object": "chat.completion.chunk", + "model": "gpt-5.4", + "choices": [{ + "index": 0, + "delta": {}, + "finish_reason": "length" + }], + "usage": { + "prompt_tokens": 1, + "completion_tokens": 2, + "total_tokens": 3 + } + })), + ) + .expect("length finish reason should map to response.incomplete"); + let sse = String::from_utf8(output).expect("sse should be utf8"); + + assert!(sse.contains("event: response.incomplete\n")); + assert!(sse.contains("\"status\":\"incomplete\"")); + assert!(sse.contains("\"incomplete_details\":{\"reason\":\"max_output_tokens\"}")); + assert!(!sse.contains("event: response.completed\n")); + } + #[test] fn rewrites_gemini_inline_image_streams_to_claude_image_blocks() { let report_context = report_context("gemini:generate_content", "claude:messages"); @@ -972,6 +1513,8 @@ mod tests { "object": "response", "model": "gpt-5.5", "status": "completed", + "error": null, + "incomplete_details": null, "output": [], "usage": { "input_tokens": 26, @@ -994,6 +1537,8 @@ mod tests { .latest_summary() .cloned() .expect("summary should exist"); + assert!(summary.observed_finish); + assert_eq!(summary.parser_error, None); let usage = summary .standardized_usage .expect("standardized usage should exist"); @@ -1098,6 +1643,45 @@ mod tests { assert_eq!(summary.unknown_event_count, 1); } + #[test] + fn terminal_observer_marks_openai_responses_incomplete_as_length_finish() { + let mut report_context = report_context("openai:chat", "openai:responses"); + report_context["provider_stream_event_api_format"] = json!("openai:responses"); + let mut observer = StreamingStandardTerminalObserver::default(); + + observer + .push_line( + &report_context, + data_line(json!({ + "type": "response.incomplete", + "response": { + "id": "resp_incomplete_123", + "model": "gpt-5.4", + "status": "incomplete", + "incomplete_details": { + "reason": "max_output_tokens", + }, + "output": [], + "usage": { + "input_tokens": 10, + "output_tokens": 20, + "total_tokens": 30, + }, + }, + })), + ) + .expect("incomplete event should be observed as terminal finish"); + + let summary = observer + .latest_summary() + .cloned() + .expect("summary should exist"); + assert!(summary.observed_finish); + assert_eq!(summary.finish_reason.as_deref(), Some("length")); + assert_eq!(summary.parser_error, None); + assert_eq!(summary.unknown_event_count, 0); + } + #[test] fn terminal_observer_tracks_openai_image_stream_usage() { let mut report_context = report_context("openai:image", "openai:chat"); diff --git a/crates/aether-ai-formats/src/formats/shared/stream_rewrite.rs b/crates/aether-ai-formats/src/formats/shared/stream_rewrite.rs index 5e527b481..cc06e3390 100644 --- a/crates/aether-ai-formats/src/formats/shared/stream_rewrite.rs +++ b/crates/aether-ai-formats/src/formats/shared/stream_rewrite.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use serde_json::{json, Map, Value}; use crate::formats::openai::image::stream::{OpenAiImageChatStreamState, OpenAiImageStreamState}; +use crate::formats::openai::responses::response::ensure_modern_openai_responses_response_fields; use crate::formats::shared::model_directives::model_directive_display_model_from_report_context; use crate::formats::shared::response::{ remove_empty_pages_from_tool_arguments, remove_empty_pages_from_tool_input_value, @@ -20,6 +21,7 @@ use crate::provider_compat::surfaces::{ pub enum FinalizeStreamRewriteMode { EnvelopeUnwrap, ModelDirectiveDisplay, + OpenAiResponsesCompat, OpenAiImage, OpenAiImageToOpenAiChat, ClaudeReadToolSanitize, @@ -91,6 +93,11 @@ pub fn resolve_finalize_stream_rewrite_mode( // Parsing→rebuilding only adds overhead and may lose information // (encrypted_content, original item IDs, etc.). if is_same_format_family(provider_api_format.as_str(), client_api_format.as_str()) { + if is_openai_responses_family(provider_api_format.as_str()) + && is_openai_responses_family(client_api_format.as_str()) + { + return Some(FinalizeStreamRewriteMode::OpenAiResponsesCompat); + } if provider_api_format == "claude:messages" && client_api_format == "claude:messages" { return Some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize); } @@ -128,6 +135,12 @@ pub fn resolve_finalize_stream_rewrite_mode( return Some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize); } + if provider_api_format == client_api_format + && is_openai_responses_family(provider_api_format.as_str()) + { + return Some(FinalizeStreamRewriteMode::OpenAiResponsesCompat); + } + (provider_api_format == client_api_format && provider_adaptation_should_unwrap_stream_envelope( envelope_name.as_str(), @@ -159,6 +172,7 @@ fn client_consumes_same_private_stream_envelope( enum AiSurfaceStreamRewriteState { EnvelopeUnwrap, ModelDirectiveDisplay, + OpenAiResponsesCompat, OpenAiImage(Box), OpenAiImageToOpenAiChat(Box), ClaudeReadToolSanitize(Box), @@ -185,6 +199,9 @@ pub fn maybe_build_ai_surface_stream_rewriter<'a>( FinalizeStreamRewriteMode::ModelDirectiveDisplay => { AiSurfaceStreamRewriteState::ModelDirectiveDisplay } + FinalizeStreamRewriteMode::OpenAiResponsesCompat => { + AiSurfaceStreamRewriteState::OpenAiResponsesCompat + } FinalizeStreamRewriteMode::OpenAiImage => { AiSurfaceStreamRewriteState::OpenAiImage(Box::::default()) } @@ -240,6 +257,7 @@ impl AiSurfaceStreamRewriter<'_> { } AiSurfaceStreamRewriteState::EnvelopeUnwrap | AiSurfaceStreamRewriteState::ModelDirectiveDisplay + | AiSurfaceStreamRewriteState::OpenAiResponsesCompat | AiSurfaceStreamRewriteState::Standard(_) => { self.buffered.extend_from_slice(chunk); let mut output = Vec::new(); @@ -275,6 +293,7 @@ impl AiSurfaceStreamRewriter<'_> { } AiSurfaceStreamRewriteState::EnvelopeUnwrap | AiSurfaceStreamRewriteState::ModelDirectiveDisplay + | AiSurfaceStreamRewriteState::OpenAiResponsesCompat | AiSurfaceStreamRewriteState::Standard(_) => { if self.buffered.is_empty() { if let AiSurfaceStreamRewriteState::Standard(state) = &mut self.state { @@ -302,6 +321,9 @@ impl AiSurfaceStreamRewriter<'_> { AiSurfaceStreamRewriteState::ModelDirectiveDisplay => { rewrite_model_directive_stream_line(self.report_context, line) } + AiSurfaceStreamRewriteState::OpenAiResponsesCompat => { + rewrite_openai_responses_compat_stream_line(self.report_context, line) + } AiSurfaceStreamRewriteState::Standard(state) => { transform_standard_line(state, self.report_context, line) } @@ -611,6 +633,50 @@ fn rewrite_model_directive_stream_line( Ok(output) } +fn rewrite_openai_responses_compat_stream_line( + report_context: &Value, + line: Vec, +) -> Result, AiSurfaceFinalizeError> { + let text = match std::str::from_utf8(&line) { + Ok(text) => text, + Err(_) => return Ok(line), + }; + let trimmed_line_end = text.trim_end_matches(['\r', '\n']); + let trailing = &text[trimmed_line_end.len()..]; + let Some((prefix, payload)) = trimmed_line_end.split_once(':') else { + return Ok(line); + }; + if prefix.trim() != "data" { + return Ok(line); + } + let payload = payload.trim_start(); + if payload.is_empty() || payload == "[DONE]" { + return Ok(line); + } + let mut value = match serde_json::from_str::(payload) { + Ok(value) => value, + Err(_) => return Ok(line), + }; + let mut changed = rewrite_stream_payload_model_from_context(report_context, &mut value); + let event_type = value + .get("type") + .and_then(Value::as_str) + .unwrap_or_default(); + if matches!(event_type, "response.completed" | "response.done") { + if let Some(response) = value.get_mut("response").and_then(Value::as_object_mut) { + changed |= ensure_modern_openai_responses_response_fields(response); + } + } + if !changed { + return Ok(line); + } + let mut output = Vec::new(); + output.extend_from_slice(b"data: "); + output.extend(serde_json::to_vec(&value)?); + output.extend_from_slice(trailing.as_bytes()); + Ok(output) +} + fn rewrite_stream_payload_model(value: &mut Value, display_model: &str) -> bool { let Some(object) = value.as_object_mut() else { return false; @@ -975,16 +1041,35 @@ data: {\"type\":\"response.output_item.added\",\"response_id\":\"resp_123\",\"ou } #[test] - fn same_family_responses_without_display_model_passes_through_verbatim() { - // When provider and client are both OpenAI Responses family but - // there is no display model override, the rewriter returns None - // (complete passthrough, no interception at all). + fn same_family_responses_without_display_model_runs_terminal_compat_only() { let report_context = json!({ "provider_api_format": "openai:responses", "client_api_format": "openai:responses:compact", "needs_conversion": true, }); - assert!(maybe_build_ai_surface_stream_rewriter(Some(&report_context)).is_none()); + let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context)) + .expect("responses compat rewriter should exist"); + let mut output = rewriter + .push_chunk( + b"event: response.output_item.added\n\ +data: {\"type\":\"response.output_item.added\",\"response_id\":\"resp_123\",\"output_index\":0,\"item\":{\"type\":\"reasoning\",\"id\":\"rs_abc\",\"encrypted_content\":\"EWxvY2tlZA==\"}}\n\n", + ) + .expect("non-terminal event should pass through"); + output.extend( + rewriter + .push_chunk( + b"event: response.completed\n\ +data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"completed\"}}\n\n", + ) + .expect("terminal event should be normalized"), + ); + let output = String::from_utf8(output).expect("output should be utf8"); + + assert!(output.contains("\"encrypted_content\":\"EWxvY2tlZA==\"")); + assert!(output.contains("event: response.completed")); + assert!(output.contains("\"output\":[]")); + assert!(output.contains("\"output_text\":\"\"")); + assert!(output.contains("\"completed_at\":")); } #[test] diff --git a/crates/aether-ai-formats/src/formats/shared/sync_products.rs b/crates/aether-ai-formats/src/formats/shared/sync_products.rs index 41f502eb1..aee4955e6 100644 --- a/crates/aether-ai-formats/src/formats/shared/sync_products.rs +++ b/crates/aether-ai-formats/src/formats/shared/sync_products.rs @@ -7,7 +7,8 @@ use aether_ai_formats::formats::conversion::response::{ convert_openai_chat_response_to_openai_responses, convert_openai_responses_response_to_openai_chat, }; -use aether_ai_formats::formats::registry::{convert_response, FormatContext}; +use aether_ai_formats::formats::openai::responses::response::ensure_modern_openai_responses_response_fields; +use aether_ai_formats::formats::registry::{convert_response, FormatContext, FormatError}; use aether_ai_formats::{ canonical_to_claude_response, canonical_to_embedding_response, canonical_to_gemini_response, canonical_to_openai_chat_response, canonical_to_openai_responses_compact_response, @@ -19,7 +20,9 @@ use aether_ai_formats::{ use serde_json::{json, Map, Value}; use super::AiSurfaceFinalizeError; +use crate::formats::claude::messages::stream::ClaudeProviderState; use crate::formats::gemini::generate_content::stream::GeminiProviderState; +use crate::formats::openai::chat::stream::{OpenAIChatProviderState, OpenAIResponsesProviderState}; use crate::formats::shared::model_directives::model_directive_display_model_from_report_context; use crate::formats::shared::response::{ remove_empty_pages_from_tool_arguments, remove_empty_pages_from_tool_input_value, @@ -27,8 +30,9 @@ use crate::formats::shared::response::{ }; use crate::formats::shared::stream_core::common::{ content_part_from_openai_image_generation_item, gemini_usage_metadata_from_usage, - map_openai_finish_reason_to_gemini, parse_json_arguments_value, CanonicalContentPart, - CanonicalStreamEvent, CanonicalUsage, + map_openai_finish_reason_to_gemini, parse_json_arguments_value, + unsupported_stream_event_message, CanonicalContentPart, CanonicalStreamEvent, + CanonicalStreamFrame, CanonicalUsage, }; #[derive(Clone, Debug, PartialEq)] @@ -70,9 +74,9 @@ pub fn maybe_build_standard_cross_format_sync_product_from_normalized_payload( Some(body_base64) => { let body_bytes = base64::engine::general_purpose::STANDARD.decode(body_base64)?; if is_standard_chat_finalize_kind(report_kind) { - aggregate_standard_chat_stream_sync_response(&body_bytes, provider_api_format) + try_aggregate_standard_chat_stream_sync_response(&body_bytes, provider_api_format)? } else if is_standard_cli_finalize_kind(report_kind) { - aggregate_standard_cli_stream_sync_response(&body_bytes, provider_api_format) + try_aggregate_standard_cli_stream_sync_response(&body_bytes, provider_api_format)? } else { return Ok(None); } @@ -544,9 +548,9 @@ fn maybe_build_standard_same_format_stream_sync_body( }; let body_bytes = base64::engine::general_purpose::STANDARD.decode(body_base64)?; Ok( - aggregate_same_format_stream_sync_response(expected_api_format, &body_bytes).map(|body| { - client_body_with_report_context_model(body, report_context, &client_api_format) - }), + try_aggregate_same_format_stream_sync_response(expected_api_format, &body_bytes)?.map( + |body| client_body_with_report_context_model(body, report_context, &client_api_format), + ), ) } @@ -646,7 +650,7 @@ fn maybe_build_openai_responses_same_family_stream_sync_body( }; let body_bytes = base64::engine::general_purpose::STANDARD.decode(body_base64)?; Ok( - aggregate_openai_responses_stream_sync_response(&body_bytes).map(|body| { + try_aggregate_openai_responses_stream_sync_response(&body_bytes)?.map(|body| { client_body_with_report_context_model(body, report_context, &client_api_format) }), ) @@ -663,10 +667,12 @@ fn maybe_build_openai_cross_format_provider_body_from_normalized_payload( let normalized_provider_api_format = normalize_openai_responses_family_api_format(provider_api_format); match normalized_provider_api_format.as_str() { - "claude:messages" => aggregate_claude_stream_sync_response(&body_bytes), - "gemini:generate_content" => aggregate_gemini_stream_sync_response(&body_bytes), + "claude:messages" => try_aggregate_claude_stream_sync_response(&body_bytes)?, + "gemini:generate_content" => { + try_aggregate_gemini_stream_sync_response(&body_bytes)? + } "openai:responses" | "openai:responses:compact" => { - aggregate_openai_responses_stream_sync_response(&body_bytes) + try_aggregate_openai_responses_stream_sync_response(&body_bytes)? } _ => None, } @@ -759,14 +765,23 @@ pub fn aggregate_standard_chat_stream_sync_response( body: &[u8], provider_api_format: &str, ) -> Option { + try_aggregate_standard_chat_stream_sync_response(body, provider_api_format) + .ok() + .flatten() +} + +fn try_aggregate_standard_chat_stream_sync_response( + body: &[u8], + provider_api_format: &str, +) -> Result, AiSurfaceFinalizeError> { match aether_ai_formats::normalize_api_format_alias(provider_api_format).as_str() { - "openai:chat" => aggregate_openai_chat_stream_sync_response(body), + "openai:chat" => try_aggregate_openai_chat_stream_sync_response(body), "openai:responses" | "openai:responses:compact" => { - aggregate_openai_responses_stream_sync_response(body) + try_aggregate_openai_responses_stream_sync_response(body) } - "claude:messages" => aggregate_claude_stream_sync_response(body), - "gemini:generate_content" => aggregate_gemini_stream_sync_response(body), - _ => None, + "claude:messages" => try_aggregate_claude_stream_sync_response(body), + "gemini:generate_content" => try_aggregate_gemini_stream_sync_response(body), + _ => Ok(None), } } @@ -776,13 +791,15 @@ pub fn convert_standard_chat_response( client_api_format: &str, report_context: &Value, ) -> Option { - if let Ok(converted) = convert_response( + match convert_response( provider_api_format, client_api_format, body_json, &format_context_from_report_context(report_context), ) { - return Some(converted); + Ok(converted) => return Some(converted), + Err(error) if response_conversion_error_requires_fail_closed(&error) => return None, + Err(_) => {} } if matches!( @@ -835,19 +852,28 @@ pub fn aggregate_standard_cli_stream_sync_response( aggregate_standard_chat_stream_sync_response(body, provider_api_format) } +fn try_aggregate_standard_cli_stream_sync_response( + body: &[u8], + provider_api_format: &str, +) -> Result, AiSurfaceFinalizeError> { + try_aggregate_standard_chat_stream_sync_response(body, provider_api_format) +} + pub fn convert_standard_cli_response( body_json: &Value, provider_api_format: &str, client_api_format: &str, report_context: &Value, ) -> Option { - if let Ok(converted) = convert_response( + match convert_response( provider_api_format, client_api_format, body_json, &format_context_from_report_context(report_context), ) { - return Some(converted); + Ok(converted) => return Some(converted), + Err(error) if response_conversion_error_requires_fail_closed(&error) => return None, + Err(_) => {} } if matches!( @@ -907,6 +933,17 @@ pub fn convert_standard_cli_response( } } +fn response_conversion_error_requires_fail_closed(error: &FormatError) -> bool { + matches!( + error, + FormatError::UnsupportedField { .. } + | FormatError::UnauditedField { .. } + | FormatError::InvalidEnumValue { .. } + | FormatError::LossyConversionBlocked { .. } + | FormatError::InvalidTargetField { .. } + ) +} + fn format_context_from_report_context(report_context: &Value) -> FormatContext { let mut context = FormatContext::default().with_report_context(report_context.clone()); if let Some(mapped_model) = report_context @@ -1427,12 +1464,15 @@ fn standard_same_format_api_format(report_kind: &str) -> Option<&'static str> { } } -fn aggregate_same_format_stream_sync_response(api_format: &str, body: &[u8]) -> Option { +fn try_aggregate_same_format_stream_sync_response( + api_format: &str, + body: &[u8], +) -> Result, AiSurfaceFinalizeError> { match api_format { - "openai:chat" => aggregate_openai_chat_stream_sync_response(body), - "claude:messages" => aggregate_claude_stream_sync_response(body), - "gemini:generate_content" => aggregate_gemini_stream_sync_response(body), - _ => None, + "openai:chat" => try_aggregate_openai_chat_stream_sync_response(body), + "claude:messages" => try_aggregate_claude_stream_sync_response(body), + "gemini:generate_content" => try_aggregate_gemini_stream_sync_response(body), + _ => Ok(None), } } @@ -1502,6 +1542,58 @@ fn parse_stream_json_events(body: &[u8]) -> Option> { Some(events) } +fn try_aggregate_openai_chat_stream_sync_response( + body: &[u8], +) -> Result, AiSurfaceFinalizeError> { + let report_context = Value::Object(Map::new()); + let mut provider = OpenAIChatProviderState::default(); + ensure_no_unknown_provider_stream_events(body, |line| { + provider.push_line(&report_context, line) + })?; + Ok(aggregate_openai_chat_stream_sync_response(body)) +} + +fn try_aggregate_openai_responses_stream_sync_response( + body: &[u8], +) -> Result, AiSurfaceFinalizeError> { + let report_context = Value::Object(Map::new()); + let mut provider = OpenAIResponsesProviderState::default(); + ensure_no_unknown_provider_stream_events(body, |line| { + provider.push_line(&report_context, line) + })?; + Ok(aggregate_openai_responses_stream_sync_response(body)) +} + +fn try_aggregate_claude_stream_sync_response( + body: &[u8], +) -> Result, AiSurfaceFinalizeError> { + let report_context = Value::Object(Map::new()); + let mut provider = ClaudeProviderState::default(); + ensure_no_unknown_provider_stream_events(body, |line| { + provider.push_line(&report_context, line) + })?; + Ok(aggregate_claude_stream_sync_response(body)) +} + +fn ensure_no_unknown_provider_stream_events( + body: &[u8], + mut push_line: impl FnMut(Vec) -> Result, AiSurfaceFinalizeError>, +) -> Result<(), AiSurfaceFinalizeError> { + let Ok(text) = std::str::from_utf8(body) else { + return Ok(()); + }; + for raw_line in text.lines() { + let frames = push_line(raw_line.as_bytes().to_vec())?; + if let Some(payload) = frames.iter().find_map(|frame| match &frame.event { + CanonicalStreamEvent::UnknownEvent(payload) => Some(payload), + _ => None, + }) { + return Err(unsupported_stream_event_finalize_error(payload)); + } + } + Ok(()) +} + pub fn aggregate_openai_chat_stream_sync_response(body: &[u8]) -> Option { let text = std::str::from_utf8(body).ok()?; let mut response_id: Option = None; @@ -1764,6 +1856,50 @@ pub fn aggregate_openai_responses_stream_sync_response(body: &[u8]) -> Option { + let output_index = openai_responses_event_output_index(event_object).unwrap_or(0); + let content_index = openai_responses_event_content_index(event_object); + merge_openai_responses_message_text_annotation( + message_states.entry(output_index).or_default(), + content_index, + event_object, + ); + } + "response.refusal.delta" => { + let output_index = openai_responses_event_output_index(event_object).unwrap_or(0); + let content_index = openai_responses_event_content_index(event_object); + let delta = event_object + .get("delta") + .and_then(Value::as_str) + .unwrap_or_default(); + append_openai_responses_message_refusal_delta( + message_states.entry(output_index).or_default(), + content_index, + delta, + ); + } + "response.refusal.done" => { + let output_index = openai_responses_event_output_index(event_object).unwrap_or(0); + let content_index = openai_responses_event_content_index(event_object); + let part = event_object.get("part").and_then(Value::as_object); + let refusal = event_object + .get("refusal") + .and_then(Value::as_str) + .or_else(|| { + event_object + .get("part") + .and_then(Value::as_object) + .and_then(|part| part.get("refusal")) + .and_then(Value::as_str) + }) + .unwrap_or_default(); + merge_openai_responses_message_refusal_part( + message_states.entry(output_index).or_default(), + content_index, + refusal, + part, + ); + } "response.content_part.added" | "response.content_part.done" => { let Some(part) = event_object.get("part").and_then(Value::as_object) else { continue; @@ -1776,7 +1912,7 @@ pub fn aggregate_openai_responses_stream_sync_response(body: &[u8]) -> Option { + "response.reasoning_text.delta" | "response.reasoning_summary_text.delta" => { let output_index = openai_responses_event_output_index(event_object).unwrap_or(0); let delta = event_object .get("delta") @@ -1791,7 +1927,7 @@ pub fn aggregate_openai_responses_stream_sync_response(body: &[u8]) -> Option { + "response.reasoning_text.done" | "response.reasoning_summary_text.done" => { let output_index = openai_responses_event_output_index(event_object).unwrap_or(0); let text = event_object .get("text") @@ -1917,7 +2053,7 @@ pub fn aggregate_openai_responses_stream_sync_response(body: &[u8]) -> Option { + "response.completed" | "response.done" => { response_object = event_object .get("response") .and_then(Value::as_object) @@ -1988,6 +2124,7 @@ pub fn aggregate_openai_responses_stream_sync_response(body: &[u8]) -> Option Option Value { }) } +fn default_openai_responses_refusal_part() -> Value { + json!({ + "type": "refusal", + "refusal": "", + }) +} + fn append_openai_responses_message_text_delta( state: &mut OpenAIResponsesSyncMessageState, content_index: usize, @@ -2190,6 +2336,115 @@ fn merge_openai_responses_message_text_part( .or_insert_with(|| Value::Array(Vec::new())); } +fn append_openai_responses_message_refusal_delta( + state: &mut OpenAIResponsesSyncMessageState, + content_index: usize, + delta: &str, +) { + if delta.is_empty() { + return; + } + let part = state + .parts + .entry(content_index) + .or_insert_with(default_openai_responses_refusal_part); + let Some(part) = part.as_object_mut() else { + return; + }; + if part.get("type").and_then(Value::as_str) != Some("refusal") { + return; + } + let current = part + .get("refusal") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(); + part.insert("type".to_string(), Value::String("refusal".to_string())); + part.insert( + "refusal".to_string(), + Value::String(format!("{current}{delta}")), + ); +} + +fn merge_openai_responses_message_refusal_part( + state: &mut OpenAIResponsesSyncMessageState, + content_index: usize, + refusal: &str, + template_part: Option<&Map>, +) { + if refusal.is_empty() && template_part.is_none() { + return; + } + let part = state.parts.entry(content_index).or_insert_with(|| { + template_part + .map(|part| Value::Object(part.clone())) + .unwrap_or_else(default_openai_responses_refusal_part) + }); + let Some(part) = part.as_object_mut() else { + return; + }; + if let Some(template_part) = template_part { + for (key, value) in template_part { + if key != "refusal" { + part.insert(key.clone(), value.clone()); + } + } + } + part.insert("type".to_string(), Value::String("refusal".to_string())); + let mut current = part + .get("refusal") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(); + reconcile_openai_responses_authoritative_text(&mut current, refusal); + part.insert("refusal".to_string(), Value::String(current)); +} + +fn merge_openai_responses_message_text_annotation( + state: &mut OpenAIResponsesSyncMessageState, + content_index: usize, + event: &Map, +) { + let Some(annotation) = event.get("annotation") else { + return; + }; + let annotation_index = event + .get("annotation_index") + .and_then(Value::as_u64) + .map(|value| value as usize); + let part = state + .parts + .entry(content_index) + .or_insert_with(default_openai_responses_output_text_part); + let Some(part) = part.as_object_mut() else { + return; + }; + if !part + .get("type") + .and_then(Value::as_str) + .is_some_and(|value| matches!(value, "output_text" | "text")) + { + return; + } + part.insert("type".to_string(), Value::String("output_text".to_string())); + part.entry("text".to_string()) + .or_insert_with(|| Value::String(String::new())); + let annotations = part + .entry("annotations".to_string()) + .or_insert_with(|| Value::Array(Vec::new())); + let Some(annotations) = annotations.as_array_mut() else { + return; + }; + if let Some(annotation_index) = annotation_index { + if annotations.len() <= annotation_index { + annotations.resize(annotation_index + 1, Value::Null); + } + annotations[annotation_index] = annotation.clone(); + } else { + annotations.push(annotation.clone()); + } +} + fn merge_openai_responses_message_part( state: &mut OpenAIResponsesSyncMessageState, content_index: usize, @@ -2202,6 +2457,12 @@ fn merge_openai_responses_message_part( { let text = part.get("text").and_then(Value::as_str).unwrap_or_default(); merge_openai_responses_message_text_part(state, content_index, text, Some(part)); + } else if part.get("type").and_then(Value::as_str) == Some("refusal") { + let refusal = part + .get("refusal") + .and_then(Value::as_str) + .unwrap_or_default(); + merge_openai_responses_message_refusal_part(state, content_index, refusal, Some(part)); } else { state .parts @@ -2622,9 +2883,19 @@ pub fn aggregate_claude_stream_sync_response(body: &[u8]) -> Option { } pub fn aggregate_gemini_stream_sync_response(body: &[u8]) -> Option { - let events = parse_stream_json_events(body)?; + try_aggregate_gemini_stream_sync_response(body) + .ok() + .flatten() +} + +fn try_aggregate_gemini_stream_sync_response( + body: &[u8], +) -> Result, AiSurfaceFinalizeError> { + let Some(events) = parse_stream_json_events(body) else { + return Ok(None); + }; if events.is_empty() { - return None; + return Ok(None); } let report_context = Value::Object(Map::new()); @@ -2643,7 +2914,9 @@ pub fn aggregate_gemini_stream_sync_response(body: &[u8]) -> Option { let mut usage_from_frames: Option = None; for event in &events { - let raw_event_object = event.as_object()?; + let Some(raw_event_object) = event.as_object() else { + return Ok(None); + }; if let Some(id) = raw_event_object.get("responseId") { response_id = Some(id.clone()); } @@ -2696,7 +2969,7 @@ pub fn aggregate_gemini_stream_sync_response(body: &[u8]) -> Option { } let line = format!("data: {event}\n").into_bytes(); - let frames = provider.push_line(&report_context, line).ok()?; + let frames = provider.push_line(&report_context, line)?; for frame in frames { if response_id.is_none() && !frame.id.is_empty() { response_id = Some(Value::String(frame.id.clone())); @@ -2774,7 +3047,9 @@ pub fn aggregate_gemini_stream_sync_response(body: &[u8]) -> Option { content, )); } - CanonicalStreamEvent::UnknownEvent(_) => {} + CanonicalStreamEvent::UnknownEvent(payload) => { + return Err(unsupported_stream_event_finalize_error(&payload)) + } CanonicalStreamEvent::ReasoningSummaryDone => {} CanonicalStreamEvent::Finish { finish_reason: frame_finish_reason, @@ -2793,7 +3068,7 @@ pub fn aggregate_gemini_stream_sync_response(body: &[u8]) -> Option { } } - let frames = provider.finish(&report_context).ok()?; + let frames = provider.finish(&report_context)?; for frame in frames { if response_id.is_none() && !frame.id.is_empty() { response_id = Some(Value::String(frame.id.clone())); @@ -2801,22 +3076,29 @@ pub fn aggregate_gemini_stream_sync_response(body: &[u8]) -> Option { if model_version.is_none() && !frame.model.is_empty() { model_version = Some(Value::String(frame.model.clone())); } - if let CanonicalStreamEvent::Finish { - finish_reason: frame_finish_reason, - usage, - } = frame.event - { - finish_reason = frame_finish_reason - .map(|value| map_openai_finish_reason_to_gemini(Some(value.as_str())).to_string()) - .or(finish_reason); - if usage.is_some() { - usage_from_frames = usage; + match frame.event { + CanonicalStreamEvent::UnknownEvent(payload) => { + return Err(unsupported_stream_event_finalize_error(&payload)) } + CanonicalStreamEvent::Finish { + finish_reason: frame_finish_reason, + usage, + } => { + finish_reason = frame_finish_reason + .map(|value| { + map_openai_finish_reason_to_gemini(Some(value.as_str())).to_string() + }) + .or(finish_reason); + if usage.is_some() { + usage_from_frames = usage; + } + } + _ => {} } } if !saw_candidate { - return None; + return Ok(None); } candidate.insert( @@ -2856,7 +3138,11 @@ pub fn aggregate_gemini_stream_sync_response(body: &[u8]) -> Option { if let Some(prompt) = prompt_feedback { response.insert("promptFeedback".to_string(), prompt); } - Some(Value::Object(response)) + Ok(Some(Value::Object(response))) +} + +fn unsupported_stream_event_finalize_error(payload: &Value) -> AiSurfaceFinalizeError { + AiSurfaceFinalizeError::new(unsupported_stream_event_message(payload)) } fn append_gemini_text_part(parts: &mut Vec, text: String, thought: bool) { @@ -3084,6 +3370,7 @@ fn guess_media_type_from_reference(reference: &str, default_mime: &str) -> Strin mod tests { use super::{ aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response, + aggregate_openai_chat_stream_sync_response, aggregate_openai_responses_stream_sync_response, convert_standard_chat_response, convert_standard_cli_response, maybe_build_openai_chat_cross_format_sync_product_from_normalized_payload, @@ -3105,6 +3392,41 @@ mod tests { use base64::Engine as _; use serde_json::json; + #[test] + fn aggregates_openai_chat_stream_tool_usage_and_finish_into_sync_body() { + let body = concat!( + "data: {\"id\":\"chatcmpl_stream_123\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"}}]}\n\n", + "data: {\"id\":\"chatcmpl_stream_123\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello \"}}]}\n\n", + "data: {\"id\":\"chatcmpl_stream_123\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_123\",\"type\":\"function\",\"function\":{\"name\":\"lookup\",\"arguments\":\"{\\\"q\\\"\"}}]}}]}\n\n", + "data: {\"id\":\"chatcmpl_stream_123\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\":\\\"rust\\\"}\"}}]}}]}\n\n", + "data: {\"id\":\"chatcmpl_stream_123\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":2,\"total_tokens\":3}}\n\n", + ); + + let result = aggregate_openai_chat_stream_sync_response(body.as_bytes()) + .expect("openai chat stream should aggregate into a sync body"); + + assert_eq!(result["id"], "chatcmpl_stream_123"); + assert_eq!(result["model"], "gpt-5"); + assert_eq!(result["choices"][0]["message"]["role"], "assistant"); + assert_eq!(result["choices"][0]["message"]["content"], "Hello "); + assert_eq!( + result["choices"][0]["message"]["tool_calls"][0]["id"], + "call_123" + ); + assert_eq!( + result["choices"][0]["message"]["tool_calls"][0]["function"]["name"], + "lookup" + ); + assert_eq!( + result["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"], + "{\"q\":\"rust\"}" + ); + assert_eq!(result["choices"][0]["finish_reason"], "tool_calls"); + assert_eq!(result["usage"]["prompt_tokens"], 1); + assert_eq!(result["usage"]["completion_tokens"], 2); + assert_eq!(result["usage"]["total_tokens"], 3); + } + #[test] fn aggregates_claude_stream_thinking_signatures_into_sync_body() { let body = concat!( @@ -3243,6 +3565,55 @@ mod tests { assert_eq!(aggregated["usageMetadata"]["totalTokenCount"], 5); } + #[test] + fn gemini_stream_aggregation_rejects_unknown_parts() { + let body = "data: {\"responseId\":\"resp_gem_unknown_123\",\"modelVersion\":\"gemini-2.5-pro\",\"candidates\":[{\"index\":0,\"content\":{\"role\":\"model\",\"parts\":[{\"futurePart\":{\"kept\":true}}]}}]}\n\n"; + + assert!( + aggregate_gemini_stream_sync_response(body.as_bytes()).is_none(), + "unknown Gemini stream parts must not be silently aggregated into a successful sync body" + ); + } + + #[test] + fn gemini_stream_finalize_rejects_unknown_parts_even_with_json_fallback() { + let report_context = json!({ + "provider_api_format": "gemini:generate_content", + "client_api_format": "openai:chat", + }); + let stream_body = "data: {\"responseId\":\"resp_gem_unknown_456\",\"modelVersion\":\"gemini-2.5-pro\",\"candidates\":[{\"index\":0,\"content\":{\"role\":\"model\",\"parts\":[{\"futurePart\":{\"kept\":true}}]}}]}\n\n"; + let provider_body_json = json!({ + "responseId": "resp_gem_unknown_456", + "modelVersion": "gemini-2.5-pro", + "candidates": [{ + "index": 0, + "content": { + "role": "model", + "parts": [{ + "text": "fallback" + }] + } + }] + }); + + let result = maybe_build_standard_cross_format_sync_product_from_normalized_payload( + "openai_chat_sync_finalize", + 200, + Some(&report_context), + Some(&provider_body_json), + Some(&base64::engine::general_purpose::STANDARD.encode(stream_body)), + ); + + assert!(result.is_err()); + let error = result.expect_err("unknown Gemini stream parts should fail closed"); + assert!(error + .to_string() + .contains("Unsupported provider stream event cannot be converted losslessly")); + assert!(error + .to_string() + .contains("field $.futurePart is unsupported")); + } + #[test] fn aggregates_gemini_stream_function_response_into_sync_body() { let body = concat!( @@ -3691,6 +4062,9 @@ mod tests { assert_eq!(body_json.get("id"), Some(&json!("resp_123"))); assert_eq!(body_json.get("status"), Some(&json!("completed"))); assert_eq!(body_json["output"][0]["content"][0]["text"], json!("Hello")); + assert_eq!(body_json["output_text"], "Hello"); + assert!(body_json["created_at"].as_i64().is_some()); + assert!(body_json["completed_at"].as_i64().is_some()); } #[test] @@ -3818,6 +4192,58 @@ mod tests { assert_eq!(result["output"][0]["content"][0]["refusal"], "blocked"); } + #[test] + fn aggregates_official_refusal_stream_events() { + let body = concat!( + "event: response.output_item.added\n", + "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"id\":\"msg_refusal_123\",\"role\":\"assistant\",\"status\":\"in_progress\",\"content\":[]}}\n\n", + "event: response.content_part.added\n", + "data: {\"type\":\"response.content_part.added\",\"output_index\":0,\"content_index\":0,\"part\":{\"type\":\"refusal\",\"refusal\":\"\"}}\n\n", + "event: response.refusal.delta\n", + "data: {\"type\":\"response.refusal.delta\",\"output_index\":0,\"content_index\":0,\"item_id\":\"msg_refusal_123\",\"delta\":\"I can't\"}\n\n", + "event: response.refusal.done\n", + "data: {\"type\":\"response.refusal.done\",\"output_index\":0,\"content_index\":0,\"item_id\":\"msg_refusal_123\",\"refusal\":\"I can't help with that.\"}\n\n", + "event: response.completed\n", + "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_refusal_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"completed\",\"output\":[]}}\n\n", + ); + + let result = aggregate_openai_responses_stream_sync_response(body.as_bytes()) + .expect("openai-responses refusal stream should aggregate into a sync body"); + + assert_eq!(result["output"][0]["content"][0]["type"], "refusal"); + assert_eq!( + result["output"][0]["content"][0]["refusal"], + "I can't help with that." + ); + assert_eq!(result["output_text"], ""); + } + + #[test] + fn aggregates_official_output_text_annotation_added_event() { + let body = concat!( + "event: response.output_text.delta\n", + "data: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"content_index\":0,\"delta\":\"Hello annotated\"}\n\n", + "event: response.output_text.annotation.added\n", + "data: {\"type\":\"response.output_text.annotation.added\",\"output_index\":0,\"content_index\":0,\"annotation_index\":0,\"annotation\":{\"type\":\"text_annotation\",\"text\":\"annotated\",\"start\":6,\"end\":15}}\n\n", + "event: response.completed\n", + "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_annotation_added_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"completed\",\"output\":[]}}\n\n", + ); + + let result = aggregate_openai_responses_stream_sync_response(body.as_bytes()) + .expect("openai-responses annotation stream should aggregate into a sync body"); + + assert_eq!(result["output"][0]["content"][0]["text"], "Hello annotated"); + assert_eq!( + result["output"][0]["content"][0]["annotations"][0]["type"], + "text_annotation" + ); + assert_eq!( + result["output"][0]["content"][0]["annotations"][0]["start"], + 6 + ); + assert_eq!(result["output_text"], "Hello annotated"); + } + #[test] fn authoritative_output_text_done_preserves_annotations() { let body = concat!( @@ -3863,6 +4289,27 @@ mod tests { assert_eq!(result["output"][0]["arguments"], r#"{"location": "Tokyo"}"#); } + #[test] + fn aggregates_modern_reasoning_text_and_response_done_alias() { + let body = concat!( + "event: response.reasoning_text.delta\n", + "data: {\"type\":\"response.reasoning_text.delta\",\"output_index\":0,\"delta\":\"Need\"}\n\n", + "event: response.reasoning_text.done\n", + "data: {\"type\":\"response.reasoning_text.done\",\"output_index\":0,\"text\":\"Need care\"}\n\n", + "event: response.done\n", + "data: {\"type\":\"response.done\",\"response\":{\"id\":\"resp_done_alias_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"completed\"}}\n\n", + ); + + let result = aggregate_openai_responses_stream_sync_response(body.as_bytes()) + .expect("modern response.done stream should aggregate"); + + assert_eq!(result["output"][0]["type"], "reasoning"); + assert_eq!(result["output"][0]["summary"][0]["text"], "Need care"); + assert!(result["output"].as_array().is_some()); + assert_eq!(result["output_text"], ""); + assert!(result["completed_at"].as_i64().is_some()); + } + #[test] fn accepts_openai_responses_same_family_stream_when_needs_conversion_is_true() { let body = concat!( @@ -4566,6 +5013,180 @@ mod tests { assert!(product.is_none()); } + #[test] + fn strict_response_conversion_errors_do_not_use_legacy_fallback() { + let openai_context = json!({ + "provider_api_format": "openai:chat", + "client_api_format": "claude:messages", + "model": "claude-sonnet-4-5", + "mapped_model": "gpt-5", + }); + let openai_body = json!({ + "id": "chatcmpl_unknown_finish", + "object": "chat.completion", + "model": "gpt-5", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "done" + }, + "finish_reason": "future_reason" + }] + }); + + assert!(convert_standard_chat_response( + &openai_body, + "openai:chat", + "claude:messages", + &openai_context, + ) + .is_none()); + + let claude_context = json!({ + "provider_api_format": "claude:messages", + "client_api_format": "openai:chat", + "model": "gpt-5", + "mapped_model": "claude-sonnet-4-5", + }); + let claude_body = json!({ + "id": "msg_unknown_stop", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [], + "stop_reason": "future_reason", + "usage": { + "input_tokens": 1, + "output_tokens": 2 + } + }); + + assert!(convert_standard_chat_response( + &claude_body, + "claude:messages", + "openai:chat", + &claude_context, + ) + .is_none()); + } + + #[test] + fn stream_finalize_rejects_unknown_openai_chat_events_without_body_fallback() { + let report_context = json!({ + "provider_api_format": "openai:chat", + "client_api_format": "claude:messages", + "model": "claude-sonnet-4-5", + "mapped_model": "gpt-5", + }); + let stream_body = "data: {\"id\":\"chatcmpl_unknown_stream\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"future_delta\":\"x\"}}]}\n\n"; + let fallback_body = json!({ + "id": "chatcmpl_fallback", + "object": "chat.completion", + "model": "gpt-5", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": "fallback"}, + "finish_reason": "stop" + }] + }); + + let error = maybe_build_standard_cross_format_sync_product_from_normalized_payload( + "claude_chat_sync_finalize", + 200, + Some(&report_context), + Some(&fallback_body), + Some(&base64::engine::general_purpose::STANDARD.encode(stream_body)), + ) + .expect_err("unknown stream event should fail closed before body fallback"); + + assert!(error + .to_string() + .contains("Unsupported provider stream event cannot be converted losslessly")); + } + + #[test] + fn stream_finalize_rejects_unknown_claude_events_without_body_fallback() { + let report_context = json!({ + "provider_api_format": "claude:messages", + "client_api_format": "openai:chat", + "model": "gpt-5", + "mapped_model": "claude-sonnet-4-5", + }); + let stream_body = concat!( + "event: message_start\n", + "data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_unknown_stream\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null}}\n\n", + "event: future_event\n", + "data: {\"type\":\"future_event\",\"payload\":{\"kept\":true}}\n\n", + ); + let fallback_body = json!({ + "id": "msg_fallback", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [{"type": "text", "text": "fallback"}], + "stop_reason": "end_turn", + "usage": { + "input_tokens": 1, + "output_tokens": 2 + } + }); + + let error = maybe_build_standard_cross_format_sync_product_from_normalized_payload( + "openai_chat_sync_finalize", + 200, + Some(&report_context), + Some(&fallback_body), + Some(&base64::engine::general_purpose::STANDARD.encode(stream_body)), + ) + .expect_err("unknown stream event should fail closed before body fallback"); + + assert!(error + .to_string() + .contains("Unsupported provider stream event cannot be converted losslessly")); + assert!(error + .to_string() + .contains("field $.type = \"future_event\"")); + } + + #[test] + fn responses_same_family_stream_finalize_rejects_unknown_events_without_body_fallback() { + let report_context = json!({ + "provider_api_format": "openai:responses", + "client_api_format": "openai:responses", + "needs_conversion": false, + "model": "gpt-5", + "mapped_model": "gpt-5", + }); + let stream_body = concat!( + "event: response.future.delta\n", + "data: {\"type\":\"response.future.delta\",\"response\":{\"id\":\"resp_unknown_stream\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"in_progress\"},\"payload\":{\"kept\":true}}\n\n", + ); + let fallback_body = json!({ + "id": "resp_fallback", + "object": "response", + "model": "gpt-5", + "status": "completed", + "output": [] + }); + + let error = maybe_build_openai_responses_same_family_sync_body_from_normalized_payload( + "openai_responses_sync_finalize", + 200, + Some(&report_context), + Some(&fallback_body), + Some(&base64::engine::general_purpose::STANDARD.encode(stream_body)), + ) + .expect_err("unknown stream event should fail closed before body fallback"); + + assert!(error + .to_string() + .contains("Unsupported provider stream event cannot be converted losslessly")); + assert!(error + .to_string() + .contains("field $.type = \"response.future.delta\"")); + } + #[test] fn standard_sync_finalize_product_prefers_same_format_success_body() { let report_context = json!({ diff --git a/crates/aether-ai-formats/src/lib.rs b/crates/aether-ai-formats/src/lib.rs index 24034bb68..52523be2b 100644 --- a/crates/aether-ai-formats/src/lib.rs +++ b/crates/aether-ai-formats/src/lib.rs @@ -6,7 +6,10 @@ pub mod formats; pub mod protocol; pub mod provider_compat; -pub use formats::context::{FormatContext, FormatError}; +pub use formats::context::{ + ConversionFieldRecord, ConversionFieldStatus, ConversionReport, Converted, FormatContext, + FormatError, +}; pub use formats::id::{ api_format_alias_matches, api_format_storage_aliases, api_format_uses_body_stream_field, is_openai_responses_compact_format, is_openai_responses_family_format, @@ -19,7 +22,11 @@ pub use formats::matrix::{ sync_cli_response_conversion_kind, RequestConversionKind, SyncChatResponseConversionKind, SyncCliResponseConversionKind, }; -pub use formats::registry::{build_stream_transcoder, convert_request, convert_response}; +pub use formats::registry::{ + build_stream_transcoder, convert_request, convert_request_pure, + convert_request_pure_with_context, convert_response, convert_response_pure, emit_request_pure, + emit_response_pure, parse_request_pure, parse_response_pure, +}; pub use 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, diff --git a/crates/aether-ai-formats/src/protocol/canonical.rs b/crates/aether-ai-formats/src/protocol/canonical.rs index ed5c81a8a..de385423f 100644 --- a/crates/aether-ai-formats/src/protocol/canonical.rs +++ b/crates/aether-ai-formats/src/protocol/canonical.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; use crate::formats::openai::shared::map_thinking_budget_to_openai_reasoning_effort; +use crate::formats::shared::model_directives::ReasoningEffort; use crate::formats::shared::response::remove_empty_pages_from_tool_input_value; pub use crate::protocol::stream::{CanonicalStreamEvent, CanonicalStreamFrame}; @@ -15,6 +16,7 @@ const CLAUDE_MESSAGES_REQUEST_SOURCE_MARKER: &str = "claude_messages_request"; const CLAUDE_SYSTEM_SOURCE_MARKER: &str = "claude_system"; const CLAUDE_THINKING_SOURCE_MARKER: &str = "claude_thinking"; const CLAUDE_TOOL_RESULT_SOURCE_MARKER: &str = "claude_tool_result"; +const OPENAI_THINKING_SOURCE_MARKER: &str = "openai_thinking"; const OPENAI_CHAT_TOOL_RESULT_SOURCE_MARKER: &str = "openai_chat_tool_result"; const OPENAI_RESPONSES_TOOL_RESULT_SOURCE_MARKER: &str = "openai_responses_tool_result"; const OPENAI_CHAT_TOOL_ERROR_PREFIX: &str = "[tool error]"; @@ -186,6 +188,8 @@ pub struct CanonicalToolDefinition { pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub parameters: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub strict: Option, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub extensions: BTreeMap, } @@ -459,8 +463,11 @@ pub fn from_openai_chat_to_canonical_request(body_json: &Value) -> Option Value { - crate::formats::openai::chat::request::to_raw(canonical) +pub fn canonical_to_openai_chat_request(canonical: &CanonicalRequest) -> Option { + crate::formats::openai::chat::request::to( + canonical, + &crate::formats::context::FormatContext::default(), + ) } pub fn from_openai_responses_to_canonical_request(body_json: &Value) -> Option { @@ -1448,6 +1455,7 @@ pub(crate) fn openai_message_content_blocks( let mut extensions = BTreeMap::new(); canonical_extension_object_mut(&mut extensions, "openai") .insert("omit_reasoning_parts".to_string(), Value::Bool(true)); + let extensions = openai_thinking_extensions(extensions); blocks.insert( 0, CanonicalContentBlock::Thinking { @@ -1568,6 +1576,7 @@ pub(crate) fn openai_reasoning_blocks(message: &Map) -> Vec) -> Vec CanonicalContentBlock { let mut extensions = BTreeMap::new(); canonical_extension_object_mut(&mut extensions, "openai") .insert("omit_reasoning_parts".to_string(), Value::Bool(true)); + let extensions = openai_thinking_extensions(extensions); CanonicalContentBlock::Thinking { text, signature: None, @@ -1939,6 +1953,11 @@ pub(crate) fn openai_responses_output_to_canonical_blocks( } "reasoning" => { let mut emitted = false; + let encrypted_content = item_object + .get("encrypted_content") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned); if let Some(summary_items) = item_object.get("summary").and_then(Value::as_array) { for summary in summary_items { let Some(summary_object) = summary.as_object() else { @@ -1957,18 +1976,32 @@ pub(crate) fn openai_responses_output_to_canonical_blocks( ); canonical_extension_object_mut(&mut extensions, "openai") .insert("omit_reasoning_parts".to_string(), Value::Bool(true)); + let extensions = openai_thinking_extensions(extensions); blocks.push(CanonicalContentBlock::Thinking { text: text.to_string(), signature: None, - encrypted_content: item_object - .get("encrypted_content") - .and_then(Value::as_str) - .map(ToOwned::to_owned), + encrypted_content: encrypted_content.clone(), extensions, }); emitted = true; } } + if !emitted && encrypted_content.is_some() { + let mut extensions = openai_responses_extensions( + item_object, + &["type", "id", "status", "summary", "encrypted_content"], + ); + canonical_extension_object_mut(&mut extensions, "openai") + .insert("omit_reasoning_parts".to_string(), Value::Bool(true)); + let extensions = openai_thinking_extensions(extensions); + blocks.push(CanonicalContentBlock::Thinking { + text: String::new(), + signature: None, + encrypted_content, + extensions, + }); + emitted = true; + } if !emitted { blocks.push(CanonicalContentBlock::Unknown { raw_type: item_type, @@ -2195,7 +2228,7 @@ pub(crate) fn openai_responses_part_to_canonical_block( ); canonical_extension_object_mut(&mut extensions, "openai") .insert("omit_reasoning_parts".to_string(), Value::Bool(true)); - extensions + openai_thinking_extensions(extensions) }, }), "input_image" | "output_image" | "image_url" => { @@ -2656,6 +2689,14 @@ fn claude_thinking_extensions(mut extensions: BTreeMap) -> BTreeM extensions } +fn openai_thinking_extensions(mut extensions: BTreeMap) -> BTreeMap { + canonical_extension_object_mut(&mut extensions, AETHER_EXTENSION_NAMESPACE).insert( + "source".to_string(), + Value::String(OPENAI_THINKING_SOURCE_MARKER.to_string()), + ); + extensions +} + pub(crate) fn is_claude_thinking_block(extensions: &BTreeMap) -> bool { extensions .get(AETHER_EXTENSION_NAMESPACE) @@ -2664,6 +2705,14 @@ pub(crate) fn is_claude_thinking_block(extensions: &BTreeMap) -> == Some(CLAUDE_THINKING_SOURCE_MARKER) } +pub(crate) fn is_openai_thinking_block(extensions: &BTreeMap) -> bool { + extensions + .get(AETHER_EXTENSION_NAMESPACE) + .and_then(|value| value.get("source")) + .and_then(Value::as_str) + == Some(OPENAI_THINKING_SOURCE_MARKER) +} + pub(crate) fn is_claude_tool_result(extensions: &BTreeMap) -> bool { extensions .get(AETHER_EXTENSION_NAMESPACE) @@ -2725,16 +2774,16 @@ fn anthropic_tool_result_blocks_to_openai_chat_content(parts: &[Value]) -> Value } let mut has_media_part = false; - let converted_parts = parts - .iter() - .map(|part| { - let openai_part = anthropic_tool_result_block_to_openai_chat_part(part); - if !openai_chat_part_is_text(&openai_part) { - has_media_part = true; - } - openai_part - }) - .collect::>(); + let mut converted_parts = Vec::with_capacity(parts.len()); + for part in parts { + let Some(openai_part) = anthropic_tool_result_block_to_openai_chat_part(part) else { + return Value::String(Value::Array(parts.to_vec()).to_string()); + }; + if !openai_chat_part_is_text(&openai_part) { + has_media_part = true; + } + converted_parts.push(openai_part); + } if has_media_part { Value::Array(converted_parts) @@ -2755,34 +2804,22 @@ fn anthropic_text_blocks_to_string(parts: &[Value]) -> Option { Some(texts.join("\n\n")) } -fn anthropic_tool_result_block_to_openai_chat_part(part: &Value) -> Value { - let Some(part_object) = part.as_object() else { - return openai_text_part("[Claude tool_result non-text content omitted]"); - }; +fn anthropic_tool_result_block_to_openai_chat_part(part: &Value) -> Option { + let part_object = part.as_object()?; match part_object .get("type") .and_then(Value::as_str) .unwrap_or_default() { - "text" => openai_text_part( + "text" => Some(openai_text_part( part_object .get("text") .and_then(Value::as_str) .unwrap_or_default(), - ), - "image" => anthropic_image_block_to_openai_chat_part(part_object).unwrap_or_else(|| { - openai_text_part(anthropic_media_block_summary("image", part_object)) - }), - "document" => { - anthropic_document_block_to_openai_chat_part(part_object).unwrap_or_else(|| { - openai_text_part(anthropic_media_block_summary("document", part_object)) - }) - } - "file" => anthropic_document_block_to_openai_chat_part(part_object).unwrap_or_else(|| { - openai_text_part(anthropic_media_block_summary("file", part_object)) - }), - "" => openai_text_part("[Claude tool_result object content omitted]"), - raw_type => openai_text_part(format!("[Claude tool_result {raw_type} content omitted]")), + )), + "image" => anthropic_image_block_to_openai_chat_part(part_object), + "document" | "file" => anthropic_document_block_to_openai_chat_part(part_object), + _ => None, } } @@ -2837,23 +2874,11 @@ fn anthropic_document_block_to_openai_chat_part(block: &Map) -> O let url = anthropic_source_str(source, "url")?; Some(openai_text_part(format!("[File: {url}]"))) } + "text" => anthropic_source_str(source, "data").map(openai_text_part), _ => None, } } -fn anthropic_media_block_summary(kind: &str, block: &Map) -> String { - let media_type = block - .get("source") - .and_then(Value::as_object) - .and_then(anthropic_source_media_type); - match media_type { - Some(media_type) if !media_type.trim().is_empty() => { - format!("[Claude tool_result {kind} content omitted: {media_type}]") - } - _ => format!("[Claude tool_result {kind} content omitted]"), - } -} - fn openai_text_part(text: impl Into) -> Value { json!({ "type": "text", @@ -3209,10 +3234,18 @@ pub(crate) fn claude_tools_to_canonical( .and_then(Value::as_str) .map(ToOwned::to_owned), parameters: tool_object.get("input_schema").cloned(), - extensions: claude_extensions( - tool_object, - &["type", "name", "description", "input_schema"], - ), + strict: None, + extensions: { + let mut extensions = claude_extensions( + tool_object, + &["type", "name", "description", "input_schema"], + ); + if let Some(input_schema) = tool_object.get("input_schema").cloned() { + canonical_extension_object_mut(&mut extensions, "claude") + .insert("raw_input_schema".to_string(), input_schema); + } + extensions + }, }); } Some((canonical, builtin_tools, web_search_options)) @@ -3361,14 +3394,7 @@ pub(crate) fn claude_thinking_to_canonical( } pub(crate) fn claude_output_effort_to_openai_reasoning_effort(value: &str) -> Option<&'static str> { - match value.trim().to_ascii_lowercase().as_str() { - "low" => Some("low"), - "medium" => Some("medium"), - "high" => Some("high"), - "xhigh" => Some("xhigh"), - "max" => Some("max"), - _ => None, - } + ReasoningEffort::parse(value).map(ReasoningEffort::as_openai_chat_value) } pub(crate) fn canonical_openai_reasoning_effort( @@ -3416,13 +3442,20 @@ pub(crate) fn gemini_thinking_to_canonical( .get("thinkingBudget") .or_else(|| thinking_config.get("thinking_budget")) .and_then(Value::as_u64); + let thinking_level = thinking_config + .get("thinkingLevel") + .or_else(|| thinking_config.get("thinking_level")) + .and_then(Value::as_str) + .and_then(ReasoningEffort::parse) + .map(ReasoningEffort::as_openai_chat_value); let mut extensions = BTreeMap::new(); extensions.insert( "gemini".to_string(), json!({ "thinking_config": Value::Object(thinking_config.clone()) }), ); - if let Some(reasoning_effort) = - budget_tokens.map(map_thinking_budget_to_openai_reasoning_effort) + if let Some(reasoning_effort) = budget_tokens + .map(map_thinking_budget_to_openai_reasoning_effort) + .or(thinking_level) { extensions.insert( "openai".to_string(), @@ -3625,10 +3658,18 @@ pub(crate) fn gemini_tools_to_canonical(value: Option<&Value>) -> Option Value if let Some(parameters) = &tool.parameters { function.insert("parameters".to_string(), parameters.clone()); } + if let Some(strict) = tool.strict { + function.insert("strict".to_string(), Value::Bool(strict)); + } json!({ "type": "function", "function": Value::Object(function), @@ -4086,7 +4137,11 @@ pub(crate) fn canonical_block_to_claude( encrypted_content, extensions, } => { - if let Some(data) = encrypted_content.as_ref().filter(|value| !value.is_empty()) { + if let Some(data) = encrypted_content + .as_ref() + .filter(|value| !value.is_empty()) + .filter(|_| is_claude_thinking_block(extensions)) + { let mut out = Map::new(); out.insert( "type".to_string(), @@ -4428,9 +4483,18 @@ pub(crate) fn canonical_tools_to_claude(canonical: &CanonicalRequest) -> Vec>(); @@ -4544,9 +4608,29 @@ pub(crate) fn apply_gemini_request_extensions( if let Some(raw_tool_config) = gemini.get("raw_tool_config").cloned() { output_object.insert("toolConfig".to_string(), raw_tool_config); } + for (key, value) in gemini { + if GEMINI_REQUEST_EXTENSION_INTERNAL_KEYS.contains(&key.as_str()) { + continue; + } + output_object + .entry(key.clone()) + .or_insert_with(|| value.clone()); + } Some(()) } +const GEMINI_REQUEST_EXTENSION_INTERNAL_KEYS: &[&str] = &[ + "builtin_tools", + "cached_content", + "generation_config_extra", + "grounding", + "raw_tool_config", + "raw_tools", + "response_modalities", + "safety_settings", + "thinking_config", +]; + fn should_reuse_raw_gemini_tools(gemini: &Map) -> bool { let Some(google_search) = gemini .get("grounding") @@ -4912,9 +4996,15 @@ pub(crate) fn gemini_stop_reason_to_canonical(value: &str) -> Option CanonicalStopReason::EndTurn, "MAX_TOKENS" => CanonicalStopReason::MaxTokens, - "SAFETY" | "RECITATION" | "BLOCKLIST" | "PROHIBITED_CONTENT" | "SPII" => { - CanonicalStopReason::ContentFiltered - } + "SAFETY" + | "RECITATION" + | "LANGUAGE" + | "BLOCKLIST" + | "PROHIBITED_CONTENT" + | "SPII" + | "IMAGE_SAFETY" + | "IMAGE_PROHIBITED_CONTENT" + | "IMAGE_RECITATION" => CanonicalStopReason::ContentFiltered, "OTHER" => CanonicalStopReason::Unknown, _ => CanonicalStopReason::Unknown, }) @@ -5154,65 +5244,6 @@ pub(crate) fn gemini_generation_config_extra( .collect() } -pub(crate) fn gemini_openai_extra_body(request: &Map) -> Option { - let mut extra_body = Map::new(); - if let Some(generation_config) = request - .get("generationConfig") - .or_else(|| request.get("generation_config")) - .and_then(Value::as_object) - { - let mut google = Map::new(); - if let Some(thinking_config) = - gemini_value_by_case(generation_config, "thinkingConfig", "thinking_config").cloned() - { - google.insert("thinking_config".to_string(), thinking_config); - } - if let Some(response_modalities) = gemini_value_by_case( - generation_config, - "responseModalities", - "response_modalities", - ) - .cloned() - { - google.insert("response_modalities".to_string(), response_modalities); - } - if !google.is_empty() { - extra_body.insert("google".to_string(), Value::Object(google)); - } - - let generation_config_extra = gemini_generation_config_extra(generation_config); - if !generation_config_extra.is_empty() { - extra_body.insert( - "gemini".to_string(), - json!({ "generation_config_extra": generation_config_extra }), - ); - } - } - if let Some(safety_settings) = request - .get("safetySettings") - .or_else(|| request.get("safety_settings")) - .cloned() - { - let gemini = extra_body - .entry("gemini".to_string()) - .or_insert_with(|| Value::Object(Map::new())) - .as_object_mut()?; - gemini.insert("safety_settings".to_string(), safety_settings); - } - if let Some(cached_content) = request - .get("cachedContent") - .or_else(|| request.get("cached_content")) - .cloned() - { - let gemini = extra_body - .entry("gemini".to_string()) - .or_insert_with(|| Value::Object(Map::new())) - .as_object_mut()?; - gemini.insert("cached_content".to_string(), cached_content); - } - (!extra_body.is_empty()).then_some(Value::Object(extra_body)) -} - pub(crate) fn extract_gemini_model_from_path(path: &str) -> Option { let marker = "/models/"; let start = path.find(marker)? + marker.len(); @@ -5733,7 +5764,7 @@ mod tests { assert_eq!(canonical_unknown_block_count(user_blocks), 1); assert_eq!(canonical_request_unknown_block_count(&canonical), 1); - let rebuilt = canonical_to_openai_chat_request(&canonical); + let rebuilt = canonical_to_openai_chat_request(&canonical).expect("openai chat request"); assert_eq!(rebuilt["model"], "gpt-5"); assert_eq!(rebuilt["messages"][0]["role"], "system"); assert_eq!(rebuilt["messages"][1]["role"], "developer"); @@ -5821,7 +5852,7 @@ mod tests { "n": 2 }); let canonical = from_openai_chat_to_canonical_request(&request).expect("canonical request"); - let rebuilt = canonical_to_openai_chat_request(&canonical); + let rebuilt = canonical_to_openai_chat_request(&canonical).expect("openai chat request"); assert_eq!(rebuilt["model"], request["model"]); assert_eq!(rebuilt["messages"], request["messages"]); assert_eq!(rebuilt["stop"], Value::Array(vec![json!("x"), json!("y")])); @@ -6316,7 +6347,8 @@ mod tests { CanonicalContentBlock::ToolUse { ref id, .. } if id == "toolu_auto_0" )); - let openai_chat = canonical_to_openai_chat_request(&canonical); + let openai_chat = + canonical_to_openai_chat_request(&canonical).expect("openai chat request"); assert_eq!( openai_chat["messages"][2]["reasoning_parts"][0]["signature"], "sig_123" @@ -6336,6 +6368,32 @@ mod tests { assert_eq!(rebuilt["output_config"]["effort"], "medium"); } + #[test] + fn canonical_to_openai_chat_request_rejects_unrepresentable_claude_tool_result() { + let request = json!({ + "model": "claude-sonnet", + "messages": [{ + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": "toolu_read", + "content": [{ + "type": "image", + "source": { + "type": "unsupported", + "media_type": "image/png", + "data": "AAAA" + } + }] + }] + }] + }); + + let canonical = from_claude_to_canonical_request(&request).expect("canonical request"); + + assert!(canonical_to_openai_chat_request(&canonical).is_none()); + } + #[test] fn claude_response_adapter_preserves_thinking_signature_tool_and_cache_usage() { let response = json!({ diff --git a/crates/aether-ai-serving/src/attempt_plan.rs b/crates/aether-ai-serving/src/attempt_plan.rs index 8336b878c..cbb5317d4 100644 --- a/crates/aether-ai-serving/src/attempt_plan.rs +++ b/crates/aether-ai-serving/src/attempt_plan.rs @@ -4,7 +4,9 @@ use aether_ai_formats::api::ExecutionRuntimeAuthContext; use aether_contracts::{ExecutionPlan, RequestBody}; use url::Url; -use crate::dto::AiExecutionDecision; +use crate::dto::{AiExecutionDecision, AiRequestGzipPolicy}; + +const DEFAULT_REQUEST_GZIP_MIN_JSON_BYTES: usize = 64 * 1024; #[derive(Debug, Clone, PartialEq, Eq)] pub struct AiDecisionPlanCore { @@ -112,6 +114,10 @@ pub fn build_ai_execution_plan_from_decision( payload: &mut AiExecutionDecision, parts: AiExecutionPlanFromDecisionParts, ) -> ExecutionPlan { + let explicit_content_encoding = take_ai_non_empty_string(&mut payload.content_encoding); + let request_gzip = payload.request_gzip.take(); + let content_encoding = explicit_content_encoding + .or_else(|| infer_ai_execution_plan_content_encoding(&parts, request_gzip.as_ref())); ExecutionPlan { request_id: parts.core.request_id, candidate_id: payload.candidate_id.take(), @@ -123,7 +129,7 @@ pub fn build_ai_execution_plan_from_decision( url: parts.url, headers: parts.headers, content_type: parts.content_type, - content_encoding: None, + content_encoding, body: parts.body, stream: parts.stream, client_api_format: parts.core.client_api_format, @@ -135,6 +141,52 @@ pub fn build_ai_execution_plan_from_decision( } } +fn infer_ai_execution_plan_content_encoding( + parts: &AiExecutionPlanFromDecisionParts, + request_gzip: Option<&AiRequestGzipPolicy>, +) -> Option { + if let Some(should_gzip) = should_gzip_explicit_json_request(parts, request_gzip) { + return should_gzip.then(|| "gzip".to_string()); + } + + None +} + +fn should_gzip_explicit_json_request( + parts: &AiExecutionPlanFromDecisionParts, + request_gzip: Option<&AiRequestGzipPolicy>, +) -> Option { + let request_gzip = request_gzip?; + let enabled = request_gzip + .enabled + .unwrap_or(request_gzip.min_bytes.is_some()); + if !enabled { + return Some(false); + } + Some(json_request_body_len_at_least( + parts, + request_gzip + .min_bytes + .unwrap_or(DEFAULT_REQUEST_GZIP_MIN_JSON_BYTES), + )) +} + +fn json_request_body_len_at_least( + parts: &AiExecutionPlanFromDecisionParts, + min_bytes: usize, +) -> bool { + if parts.body.body_bytes_b64.is_some() || parts.body.body_ref.is_some() { + return false; + } + let Some(json_body) = parts.body.json_body.as_ref() else { + return false; + }; + + serde_json::to_vec(json_body) + .map(|body| body.len() >= min_bytes) + .unwrap_or(false) +} + pub fn build_ai_execution_decision_from_plan( parts: AiExecutionDecisionFromPlanParts, ) -> AiExecutionDecision { @@ -149,7 +201,7 @@ pub fn build_ai_execution_decision_from_plan( url, headers, content_type, - content_encoding: _content_encoding, + content_encoding, body, stream, client_api_format, @@ -208,6 +260,8 @@ pub fn build_ai_execution_decision_from_plan( provider_request_body: json_body, provider_request_body_base64: body_bytes_b64, content_type, + content_encoding, + request_gzip: None, proxy, transport_profile, timeouts, @@ -398,6 +452,171 @@ mod tests { assert!(payload.model_name.is_none()); } + #[test] + fn build_ai_execution_plan_without_request_gzip_policy_leaves_json_uncompressed() { + let large_codex_url = test_plan_for_url_and_body( + "https://chatgpt.com/backend-api/codex/responses", + RequestBody::from_json(json!({ + "model": "gpt-5.5", + "input": "x".repeat(DEFAULT_REQUEST_GZIP_MIN_JSON_BYTES), + })), + ); + let large_openai = test_plan_for_url_and_body( + "https://api.openai.com/v1/responses", + RequestBody::from_json(json!({ + "model": "gpt-5.5", + "input": "x".repeat(DEFAULT_REQUEST_GZIP_MIN_JSON_BYTES), + })), + ); + + assert_eq!(large_codex_url.content_encoding, None); + assert_eq!(large_openai.content_encoding, None); + } + + #[test] + fn build_ai_execution_plan_does_not_gzip_raw_body_even_when_explicit() { + let mut payload = test_decision(); + payload.request_gzip = Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes: Some(0), + }); + let core = + take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); + + let plan = build_ai_execution_plan_from_decision( + &mut payload, + AiExecutionPlanFromDecisionParts { + core, + method: "POST".to_string(), + url: "https://api.example.com/v1/chat/completions".to_string(), + headers: BTreeMap::new(), + content_type: Some("application/json".to_string()), + body: RequestBody { + json_body: None, + body_bytes_b64: Some("dGVzdA==".to_string()), + body_ref: None, + }, + stream: false, + }, + ); + + assert_eq!(plan.content_encoding, None); + } + + #[test] + fn build_ai_execution_plan_preserves_explicit_content_encoding_for_raw_body() { + let mut payload = test_decision(); + payload.content_encoding = Some("gzip".to_string()); + payload.request_gzip = Some(AiRequestGzipPolicy { + enabled: Some(false), + min_bytes: None, + }); + let core = + take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); + + let plan = build_ai_execution_plan_from_decision( + &mut payload, + AiExecutionPlanFromDecisionParts { + core, + method: "POST".to_string(), + url: "https://api.example.com/v1/chat/completions".to_string(), + headers: BTreeMap::new(), + content_type: Some("application/octet-stream".to_string()), + body: RequestBody { + json_body: None, + body_bytes_b64: Some("dGVzdA==".to_string()), + body_ref: None, + }, + stream: false, + }, + ); + + assert_eq!(plan.content_encoding.as_deref(), Some("gzip")); + } + + #[test] + fn build_ai_execution_plan_gzips_explicit_json_request_for_non_codex() { + let mut payload = test_decision(); + payload.request_gzip = Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes: Some(1), + }); + let core = + take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); + + let plan = build_ai_execution_plan_from_decision( + &mut payload, + AiExecutionPlanFromDecisionParts { + core, + method: "POST".to_string(), + url: "https://api.example.com/v1/chat/completions".to_string(), + headers: BTreeMap::new(), + content_type: Some("application/json".to_string()), + body: RequestBody::from_json(json!({"model": "gpt-test"})), + stream: false, + }, + ); + + assert_eq!(plan.content_encoding.as_deref(), Some("gzip")); + } + + #[test] + fn build_ai_execution_plan_respects_explicit_request_gzip_threshold() { + let mut payload = test_decision(); + payload.request_gzip = Some(AiRequestGzipPolicy { + enabled: Some(true), + min_bytes: Some(1024), + }); + let core = + take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); + + let plan = build_ai_execution_plan_from_decision( + &mut payload, + AiExecutionPlanFromDecisionParts { + core, + method: "POST".to_string(), + url: "https://api.example.com/v1/chat/completions".to_string(), + headers: BTreeMap::new(), + content_type: Some("application/json".to_string()), + body: RequestBody::from_json(json!({"model": "gpt-test"})), + stream: false, + }, + ); + + assert_eq!(plan.content_encoding, None); + } + + #[test] + fn build_ai_execution_plan_explicit_request_gzip_false_disables_gzip() { + let mut payload = test_decision(); + payload.provider_api_format = Some("openai:responses".to_string()); + payload.client_api_format = Some("openai:responses".to_string()); + payload.request_gzip = Some(AiRequestGzipPolicy { + enabled: Some(false), + min_bytes: None, + }); + let core = + take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); + + let plan = build_ai_execution_plan_from_decision( + &mut payload, + AiExecutionPlanFromDecisionParts { + core, + method: "POST".to_string(), + url: "https://chatgpt.com/backend-api/codex/responses".to_string(), + headers: BTreeMap::new(), + content_type: Some("application/json".to_string()), + body: RequestBody::from_json(json!({ + "model": "gpt-5.5", + "input": "x".repeat(DEFAULT_REQUEST_GZIP_MIN_JSON_BYTES), + })), + stream: true, + }, + ); + + assert_eq!(plan.content_encoding, None); + } + #[test] fn infer_ai_upstream_base_url_preserves_codex_base_path() { assert_eq!( @@ -482,6 +701,88 @@ mod tests { assert_eq!(decision.report_kind.as_deref(), Some("report")); } + #[test] + fn plan_decision_round_trip_preserves_raw_body_content_encoding() { + let original = ExecutionPlan { + request_id: "plan-request".to_string(), + candidate_id: Some("candidate-1".to_string()), + provider_name: Some("provider".to_string()), + provider_id: "provider-1".to_string(), + endpoint_id: "endpoint-1".to_string(), + key_id: "key-1".to_string(), + method: "POST".to_string(), + url: "https://api.example.com/v1/upload".to_string(), + headers: BTreeMap::from([( + "content-type".to_string(), + "application/octet-stream".to_string(), + )]), + content_type: Some("application/octet-stream".to_string()), + content_encoding: Some("gzip".to_string()), + body: RequestBody { + json_body: None, + body_bytes_b64: Some("dGVzdA==".to_string()), + body_ref: None, + }, + stream: false, + client_api_format: "openai:chat".to_string(), + provider_api_format: "openai:chat".to_string(), + model_name: Some("gpt-test".to_string()), + proxy: None, + transport_profile: None, + timeouts: None, + }; + + let mut decision = + build_ai_execution_decision_from_plan(AiExecutionDecisionFromPlanParts { + action: "execution_runtime.sync_decision".to_string(), + decision_kind: Some("raw_upload_sync".to_string()), + request_id: None, + upstream_base_url: Some("https://api.example.com".to_string()), + include_auth_pair: false, + plan: original, + report_kind: None, + report_context: None, + auth_context: None, + }); + + assert_eq!(decision.content_encoding.as_deref(), Some("gzip")); + assert!(decision.request_gzip.is_none()); + + let core = + take_ai_decision_plan_core(&mut decision).expect("core fields should be available"); + let method = take_ai_non_empty_string(&mut decision.provider_request_method) + .expect("method should round-trip"); + let url = + take_ai_non_empty_string(&mut decision.upstream_url).expect("url should round-trip"); + let headers = std::mem::take(&mut decision.provider_request_headers); + let content_type = decision.content_type.take(); + let body = resolve_ai_passthrough_sync_request_body( + decision.provider_request_body.take(), + decision.provider_request_body_base64.take(), + ); + let stream = decision.upstream_is_stream; + + let round_tripped = build_ai_execution_plan_from_decision( + &mut decision, + AiExecutionPlanFromDecisionParts { + core, + method, + url, + headers, + content_type, + body, + stream, + }, + ); + + assert_eq!(round_tripped.content_encoding.as_deref(), Some("gzip")); + assert_eq!( + round_tripped.body.body_bytes_b64.as_deref(), + Some("dGVzdA==") + ); + assert!(round_tripped.body.json_body.is_none()); + } + fn test_decision() -> AiExecutionDecision { AiExecutionDecision { action: "sync".to_string(), @@ -511,6 +812,8 @@ mod tests { provider_request_body: None, provider_request_body_base64: None, content_type: None, + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, @@ -520,4 +823,35 @@ mod tests { auth_context: None, } } + + fn test_plan_for_url_and_body(url: &str, body: RequestBody) -> ExecutionPlan { + test_plan_for_url_body_and_format(url, body, "openai:responses") + } + + fn test_plan_for_url_body_and_format( + url: &str, + body: RequestBody, + provider_api_format: &str, + ) -> ExecutionPlan { + let mut payload = test_decision(); + payload.provider_api_format = Some(provider_api_format.to_string()); + payload.client_api_format = Some(provider_api_format.to_string()); + let core = + take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); + build_ai_execution_plan_from_decision( + &mut payload, + AiExecutionPlanFromDecisionParts { + core, + method: "POST".to_string(), + url: url.to_string(), + headers: BTreeMap::from([( + "content-type".to_string(), + "application/json".to_string(), + )]), + content_type: Some("application/json".to_string()), + body, + stream: true, + }, + ) + } } diff --git a/crates/aether-ai-serving/src/decision_payload.rs b/crates/aether-ai-serving/src/decision_payload.rs index bdee2c978..a5755c74c 100644 --- a/crates/aether-ai-serving/src/decision_payload.rs +++ b/crates/aether-ai-serving/src/decision_payload.rs @@ -10,7 +10,7 @@ use aether_contracts::{ }; use serde_json::{json, Map, Value}; -use crate::{AiExecutionDecision, ConversionMode, ExecutionStrategy}; +use crate::{AiExecutionDecision, AiRequestGzipPolicy, ConversionMode, ExecutionStrategy}; pub struct AiExecutionDecisionResponseParts { pub decision_is_stream: bool, @@ -37,6 +37,8 @@ pub struct AiExecutionDecisionResponseParts { pub provider_request_body: Option, pub provider_request_body_base64: Option, pub content_type: Option, + pub content_encoding: Option, + pub request_gzip: Option, pub proxy: Option, pub transport_profile: Option, pub timeouts: Option, @@ -83,6 +85,8 @@ pub fn build_ai_execution_decision_response( provider_request_body: parts.provider_request_body, provider_request_body_base64: parts.provider_request_body_base64, content_type: parts.content_type, + content_encoding: parts.content_encoding, + request_gzip: parts.request_gzip, proxy: parts.proxy, transport_profile: parts.transport_profile, timeouts: parts.timeouts, @@ -222,6 +226,8 @@ mod tests { provider_request_body: Some(json!({"model": "gpt-5"})), provider_request_body_base64: None, content_type: Some("application/json".to_string()), + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, diff --git a/crates/aether-ai-serving/src/dto.rs b/crates/aether-ai-serving/src/dto.rs index 2cdac6160..52629adf6 100644 --- a/crates/aether-ai-serving/src/dto.rs +++ b/crates/aether-ai-serving/src/dto.rs @@ -44,6 +44,14 @@ impl ConversionMode { } } +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct AiRequestGzipPolicy { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_bytes: Option, +} + #[derive(Debug, Deserialize, Serialize)] pub struct AiExecutionPlanPayload { pub action: String, @@ -114,6 +122,10 @@ pub struct AiExecutionDecision { pub provider_request_body_base64: Option, #[serde(default)] pub content_type: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content_encoding: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub request_gzip: Option, #[serde(default)] pub proxy: Option, #[serde(default)] @@ -216,6 +228,8 @@ mod tests { provider_request_body: None, provider_request_body_base64: None, content_type: None, + content_encoding: None, + request_gzip: None, proxy: None, transport_profile: None, timeouts: None, diff --git a/crates/aether-ai-serving/src/failure_diagnostic.rs b/crates/aether-ai-serving/src/failure_diagnostic.rs index b2d618651..a0bb59158 100644 --- a/crates/aether-ai-serving/src/failure_diagnostic.rs +++ b/crates/aether-ai-serving/src/failure_diagnostic.rs @@ -68,6 +68,11 @@ impl CandidateFailureDiagnostic { self } + pub fn has_specific_path(&self) -> bool { + let path = self.path.trim(); + !path.is_empty() && path != "$" + } + pub fn to_extra_data(&self) -> Value { let diagnostic = self.to_value(); let mut extra_data = json!({ @@ -75,17 +80,31 @@ impl CandidateFailureDiagnostic { }); // Compatibility for current usage UI and already persisted trace readers. - if self.kind == CandidateFailureDiagnosticKind::RequestBodyBuild { - if let Some(object) = extra_data.as_object_mut() { - object.insert( - "request_body_build_error".to_string(), - json!({ - "path": self.path, - "message": self.message, - "client_api_format": self.client_api_format, - "provider_api_format": self.provider_api_format, - }), - ); + if let Some(object) = extra_data.as_object_mut() { + match self.kind { + CandidateFailureDiagnosticKind::RequestBodyBuild => { + object.insert( + "request_body_build_error".to_string(), + json!({ + "path": self.path, + "message": self.message, + "client_api_format": self.client_api_format, + "provider_api_format": self.provider_api_format, + }), + ); + } + CandidateFailureDiagnosticKind::RequestConversion => { + object.insert( + "request_conversion_error".to_string(), + json!({ + "path": self.path, + "message": self.message, + "client_api_format": self.client_api_format, + "provider_api_format": self.provider_api_format, + }), + ); + } + _ => {} } } diff --git a/crates/aether-ai-serving/src/lib.rs b/crates/aether-ai-serving/src/lib.rs index ea399af0c..96ea4693d 100644 --- a/crates/aether-ai-serving/src/lib.rs +++ b/crates/aether-ai-serving/src/lib.rs @@ -107,8 +107,8 @@ pub use decision_payload::{ }; pub use dto::{ augment_sync_report_context, generic_decision_missing_exact_provider_request, - AiExecutionDecision, AiExecutionPlanPayload, AiStreamAttempt, AiSyncAttempt, ConversionMode, - ExecutionStrategy, + AiExecutionDecision, AiExecutionPlanPayload, AiRequestGzipPolicy, AiStreamAttempt, + AiSyncAttempt, ConversionMode, ExecutionStrategy, }; pub use execution_path::{ run_ai_stream_execution_path, run_ai_sync_execution_path, AiPlanFallbackReason, @@ -126,7 +126,8 @@ pub use report_context::{ AiExecutionReportContextParts, AiRequestOrigin, }; pub use request_body_diagnostics::{ - request_body_build_failure_extra_data, same_format_provider_request_body_failure_extra_data, + request_body_build_failure_extra_data, request_conversion_failure_extra_data, + same_format_provider_request_body_failure_extra_data, }; pub use runtime_miss::{ apply_ai_runtime_candidate_evaluation_progress, diff --git a/crates/aether-ai-serving/src/request_body_diagnostics.rs b/crates/aether-ai-serving/src/request_body_diagnostics.rs index b11919727..fd9ecc488 100644 --- a/crates/aether-ai-serving/src/request_body_diagnostics.rs +++ b/crates/aether-ai-serving/src/request_body_diagnostics.rs @@ -2,7 +2,9 @@ use serde_json::Value; use aether_ai_formats::api::{ is_claude_messages_shaped_body_on_openai_chat_endpoint, is_openai_responses_family_format, + normalize_api_format_alias, }; +use aether_ai_formats::{convert_request_pure_with_context, FormatContext, FormatError}; use crate::{CandidateFailureDiagnostic, CandidateFailureDiagnosticKind}; @@ -24,6 +26,31 @@ pub fn request_body_build_failure_extra_data( ) } +pub fn request_conversion_failure_extra_data( + body_json: &Value, + client_api_format: &str, + provider_api_format: &str, + mapped_model: Option<&str>, + request_path: Option<&str>, + upstream_is_stream: bool, + source: impl Into, +) -> Option { + let diagnostic = diagnose_request_conversion_failure( + body_json, + client_api_format, + provider_api_format, + mapped_model, + request_path, + upstream_is_stream, + )?; + Some( + diagnostic + .formats(client_api_format, provider_api_format) + .source(source) + .to_extra_data(), + ) +} + pub fn same_format_provider_request_body_failure_extra_data( body_json: &Value, provider_api_format: &str, @@ -41,6 +68,67 @@ pub fn same_format_provider_request_body_failure_extra_data( } type RequestBodyBuildDiagnostic = CandidateFailureDiagnostic; +type RequestConversionDiagnostic = CandidateFailureDiagnostic; + +fn diagnose_request_conversion_failure( + body_json: &Value, + client_api_format: &str, + provider_api_format: &str, + mapped_model: Option<&str>, + request_path: Option<&str>, + upstream_is_stream: bool, +) -> Option { + let mut context = FormatContext::default().with_upstream_stream(upstream_is_stream); + if let Some(mapped_model) = mapped_model + .map(str::trim) + .filter(|value| !value.is_empty()) + { + context = context.with_mapped_model(mapped_model); + } + if let Some(request_path) = request_path + .map(str::trim) + .filter(|value| !value.is_empty()) + { + context = context.with_request_path(request_path); + } + + let source_format = + compatible_source_format_for_diagnostic(body_json, client_api_format, provider_api_format); + match convert_request_pure_with_context( + source_format.as_str(), + provider_api_format, + body_json, + &context, + ) { + Ok(_) => { + diagnose_request_body_build_failure(body_json, client_api_format, provider_api_format) + .filter(CandidateFailureDiagnostic::has_specific_path) + .or_else(|| Some(fallback_request_conversion_diagnostic())) + } + Err(error) => { + let format_diagnostic = + diagnostic_from_format_error(&error, client_api_format, provider_api_format); + if format_diagnostic.has_specific_path() { + Some(format_diagnostic) + } else { + diagnose_request_body_build_failure( + body_json, + client_api_format, + provider_api_format, + ) + .filter(CandidateFailureDiagnostic::has_specific_path) + .or(Some(format_diagnostic)) + } + } + } +} + +fn fallback_request_conversion_diagnostic() -> RequestConversionDiagnostic { + diagnostic( + "$", + "请求体转换本身已通过;失败可能发生在 Body 规则应用或后续上游请求体语义校验", + ) +} fn diagnose_request_body_build_failure( body_json: &Value, @@ -74,6 +162,123 @@ fn diagnose_request_body_build_failure( )) } +fn compatible_source_format_for_diagnostic( + body_json: &Value, + client_api_format: &str, + provider_api_format: &str, +) -> String { + 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:chat" + && provider_api_format == "claude:messages" + && is_claude_messages_shaped_body_on_openai_chat_endpoint(body_json) + { + return "claude:messages".to_string(); + } + client_api_format +} + +fn diagnostic_from_format_error( + error: &FormatError, + client_api_format: &str, + provider_api_format: &str, +) -> RequestConversionDiagnostic { + CandidateFailureDiagnostic::new( + CandidateFailureDiagnosticKind::RequestConversion, + format_error_path(error), + format_error_message(error, client_api_format, provider_api_format), + ) +} + +fn format_error_path(error: &FormatError) -> String { + match error { + FormatError::UnsupportedField { field, .. } + | FormatError::UnauditedField { field, .. } + | FormatError::InvalidEnumValue { field, .. } + | FormatError::LossyConversionBlocked { field, .. } + | FormatError::InvalidTargetField { field, .. } => field_to_json_path(field), + FormatError::UnsupportedFormat(_) + | FormatError::RequestParseFailed { .. } + | FormatError::RequestEmitFailed { .. } + | FormatError::ResponseParseFailed { .. } + | FormatError::ResponseEmitFailed { .. } => "$".to_string(), + } +} + +fn field_to_json_path(field: &str) -> String { + let field = field.trim(); + if field.is_empty() || field == "$" { + return "$".to_string(); + } + if field.starts_with('$') { + return field.to_string(); + } + format!("$.{}", field) + .replace("[].", "[*].") + .replace("[]", "[*]") +} + +fn format_error_message( + error: &FormatError, + client_api_format: &str, + provider_api_format: &str, +) -> String { + match error { + FormatError::UnsupportedFormat(format) => { + format!("不支持的 API 格式 {format},无法执行 {client_api_format} → {provider_api_format} 转换") + } + FormatError::RequestParseFailed { format } => { + format!("无法按 {format} 解析请求体;请检查请求体结构和字段类型是否符合该格式") + } + FormatError::RequestEmitFailed { format } => { + format!("无法生成 {format} 上游请求体;请检查源请求是否缺少目标格式必需字段或包含不可映射结构") + } + FormatError::ResponseParseFailed { format } => { + format!("无法按 {format} 解析响应体") + } + FormatError::ResponseEmitFailed { format } => { + format!("无法生成 {format} 响应体") + } + FormatError::UnsupportedField { + format, + field, + reason, + } => { + format!("{format} 字段 {field} 不支持跨格式转换:{reason}") + } + FormatError::UnauditedField { + source_format, + target_format, + field, + reason, + } => { + format!("{source_format} 字段 {field} 尚未审计,不能转换到 {target_format}:{reason}") + } + FormatError::InvalidEnumValue { + format, + field, + value, + } => { + format!("{format} 字段 {field} 的枚举值 {value:?} 无效,无法转换") + } + FormatError::LossyConversionBlocked { + source_format, + target_format, + field, + reason, + } => { + format!("{source_format} 字段 {field} 不能无损转换到 {target_format}:{reason}") + } + FormatError::InvalidTargetField { + format, + field, + reason, + } => { + format!("目标格式 {format} 字段 {field} 无效:{reason}") + } + } +} + fn is_openai_responses_client_format(client_api_format: &str) -> bool { is_openai_responses_family_format(client_api_format) } @@ -593,7 +798,7 @@ fn request_body_build_source(client_api_format: &str, provider_api_format: &str) mod tests { use serde_json::json; - use super::request_body_build_failure_extra_data; + use super::{request_body_build_failure_extra_data, request_conversion_failure_extra_data}; #[test] fn openai_chat_to_claude_recognizes_compatible_claude_native_tool_shape() { @@ -714,6 +919,37 @@ mod tests { ); } + #[test] + fn request_conversion_reports_lossy_incompatible_field_path() { + let body = json!({ + "model": "gpt-5.4", + "messages": [{ "role": "user", "content": "hello" }], + "n": 2 + }); + + let diagnostic = request_conversion_failure_extra_data( + &body, + "openai:chat", + "openai:responses", + Some("gpt-5.4"), + Some("/v1/chat/completions"), + false, + "test_conversion", + ) + .expect("diagnostic"); + + assert_eq!( + diagnostic["failure_diagnostic"]["kind"], + "request_conversion" + ); + assert_eq!(diagnostic["failure_diagnostic"]["path"], "$.n"); + assert_eq!(diagnostic["request_conversion_error"]["path"], "$.n"); + assert!(diagnostic["failure_diagnostic"]["message"] + .as_str() + .expect("message") + .contains("字段 n")); + } + #[test] fn same_format_provider_reports_non_object_body() { let diagnostic = super::same_format_provider_request_body_failure_extra_data( diff --git a/crates/aether-data-contracts/src/repository/candidates/types.rs b/crates/aether-data-contracts/src/repository/candidates/types.rs index 4c70f2cf0..49d89d9b6 100644 --- a/crates/aether-data-contracts/src/repository/candidates/types.rs +++ b/crates/aether-data-contracts/src/repository/candidates/types.rs @@ -442,6 +442,18 @@ pub trait RequestCandidateReadRepository: Send + Sync { request_id: &str, ) -> Result, crate::DataLayerError>; + async fn list_attempted_by_request_id( + &self, + request_id: &str, + ) -> Result, crate::DataLayerError> { + Ok(self + .list_by_request_id(request_id) + .await? + .into_iter() + .filter(|candidate| candidate.status.is_attempted(candidate.started_at_unix_ms)) + .collect()) + } + async fn list_recent( &self, limit: usize, diff --git a/crates/aether-data-contracts/src/repository/provider_catalog/types.rs b/crates/aether-data-contracts/src/repository/provider_catalog/types.rs index 035a916f9..65254c499 100644 --- a/crates/aether-data-contracts/src/repository/provider_catalog/types.rs +++ b/crates/aether-data-contracts/src/repository/provider_catalog/types.rs @@ -651,6 +651,7 @@ pub trait ProviderCatalogWriteRepository: Send + Sync { async fn cleanup_deleted_provider_refs( &self, provider_id: &str, + provider_deleted: bool, endpoint_ids: &[String], key_ids: &[String], ) -> Result<(), crate::DataLayerError>; diff --git a/crates/aether-data-contracts/src/repository/usage/types.rs b/crates/aether-data-contracts/src/repository/usage/types.rs index e018d54c2..644deb470 100644 --- a/crates/aether-data-contracts/src/repository/usage/types.rs +++ b/crates/aether-data-contracts/src/repository/usage/types.rs @@ -730,6 +730,8 @@ pub struct UsageAuditListQuery { pub provider_name: Option, pub model: Option, pub api_format: Option, + pub client_family: Option, + pub exclude_unknown_model_or_provider: bool, pub statuses: Option>, pub exclude_status_codes: Vec, pub is_stream: Option, @@ -747,6 +749,8 @@ pub struct UsageAuditKeywordSearchQuery { pub provider_name: Option, pub model: Option, pub api_format: Option, + pub client_family: Option, + pub exclude_unknown_model_or_provider: bool, pub statuses: Option>, pub exclude_status_codes: Vec, pub is_stream: Option, @@ -1450,6 +1454,13 @@ pub trait UsageReadRepository: Send + Sync { request_id: &str, ) -> Result, crate::DataLayerError>; + async fn find_by_request_id_shallow( + &self, + request_id: &str, + ) -> Result, crate::DataLayerError> { + self.find_by_request_id(request_id).await + } + async fn resolve_body_ref( &self, body_ref: &str, diff --git a/crates/aether-data/src/repository/candidates/mysql.rs b/crates/aether-data/src/repository/candidates/mysql.rs index 40d96fb1d..a86786217 100644 --- a/crates/aether-data/src/repository/candidates/mysql.rs +++ b/crates/aether-data/src/repository/candidates/mysql.rs @@ -86,6 +86,23 @@ impl RequestCandidateReadRepository for MysqlRequestCandidateRepository { rows.iter().map(map_candidate_row).collect() } + async fn list_attempted_by_request_id( + &self, + request_id: &str, + ) -> Result, DataLayerError> { + let rows = sqlx::query(&format!( + "{CANDIDATE_COLUMNS} WHERE request_id = ? \ + AND (status IN ('streaming', 'success', 'failed', 'cancelled') \ + OR (status = 'pending' AND started_at IS NOT NULL)) \ + ORDER BY candidate_index ASC, retry_index ASC, created_at ASC" + )) + .bind(request_id) + .fetch_all(&self.pool) + .await + .map_sql_err()?; + rows.iter().map(map_candidate_row).collect() + } + async fn list_recent( &self, limit: usize, diff --git a/crates/aether-data/src/repository/candidates/postgres.rs b/crates/aether-data/src/repository/candidates/postgres.rs index e5816378a..e357e5021 100644 --- a/crates/aether-data/src/repository/candidates/postgres.rs +++ b/crates/aether-data/src/repository/candidates/postgres.rs @@ -229,6 +229,26 @@ impl SqlxRequestCandidateReadRepository { collect_query_rows(builder.build().fetch(&self.pool), map_request_candidate_row).await } + pub async fn list_attempted_by_request_id( + &self, + request_id: &str, + ) -> Result, DataLayerError> { + let mut builder = QueryBuilder::::new(candidate_columns()); + let mut where_clause = WhereClause::new(); + push_eq( + &mut builder, + &mut where_clause, + "request_id", + request_id.to_string(), + ); + builder.push( + " AND (status IN ('streaming', 'success', 'failed', 'cancelled') \ + OR (status = 'pending' AND started_at IS NOT NULL)) \ + ORDER BY candidate_index ASC, retry_index ASC, created_at ASC", + ); + collect_query_rows(builder.build().fetch(&self.pool), map_request_candidate_row).await + } + pub async fn list_recent( &self, limit: usize, @@ -513,6 +533,13 @@ impl RequestCandidateReadRepository for SqlxRequestCandidateReadRepository { Self::list_by_request_id(self, request_id).await } + async fn list_attempted_by_request_id( + &self, + request_id: &str, + ) -> Result, DataLayerError> { + Self::list_attempted_by_request_id(self, request_id).await + } + async fn list_recent( &self, limit: usize, diff --git a/crates/aether-data/src/repository/candidates/sqlite.rs b/crates/aether-data/src/repository/candidates/sqlite.rs index 564222211..17d6c3a6c 100644 --- a/crates/aether-data/src/repository/candidates/sqlite.rs +++ b/crates/aether-data/src/repository/candidates/sqlite.rs @@ -87,6 +87,23 @@ impl RequestCandidateReadRepository for SqliteRequestCandidateRepository { rows.iter().map(map_candidate_row).collect() } + async fn list_attempted_by_request_id( + &self, + request_id: &str, + ) -> Result, DataLayerError> { + let rows = sqlx::query(&format!( + "{CANDIDATE_COLUMNS} WHERE request_id = ? \ + AND (status IN ('streaming', 'success', 'failed', 'cancelled') \ + OR (status = 'pending' AND started_at IS NOT NULL)) \ + ORDER BY candidate_index ASC, retry_index ASC, created_at ASC" + )) + .bind(request_id) + .fetch_all(&self.pool) + .await + .map_sql_err()?; + rows.iter().map(map_candidate_row).collect() + } + async fn list_recent( &self, limit: usize, diff --git a/crates/aether-data/src/repository/provider_catalog/memory.rs b/crates/aether-data/src/repository/provider_catalog/memory.rs index aab603c9d..9108a98d9 100644 --- a/crates/aether-data/src/repository/provider_catalog/memory.rs +++ b/crates/aether-data/src/repository/provider_catalog/memory.rs @@ -652,6 +652,7 @@ impl ProviderCatalogWriteRepository for InMemoryProviderCatalogReadRepository { async fn cleanup_deleted_provider_refs( &self, _provider_id: &str, + _provider_deleted: bool, _endpoint_ids: &[String], _key_ids: &[String], ) -> Result<(), DataLayerError> { diff --git a/crates/aether-data/src/repository/provider_catalog/mysql.rs b/crates/aether-data/src/repository/provider_catalog/mysql.rs index 5e05d36d3..d88a6fd30 100644 --- a/crates/aether-data/src/repository/provider_catalog/mysql.rs +++ b/crates/aether-data/src/repository/provider_catalog/mysql.rs @@ -310,10 +310,60 @@ WHERE id = ? pub async fn cleanup_deleted_provider_refs( &self, provider_id: &str, - _endpoint_ids: &[String], - _key_ids: &[String], + provider_deleted: bool, + endpoint_ids: &[String], + key_ids: &[String], ) -> Result<(), DataLayerError> { validate_non_empty(provider_id, "provider catalog provider_id")?; + let mut tx = self.pool.begin().await.map_sql_err()?; + + if provider_deleted { + sqlx::query( + "UPDATE user_preferences SET default_provider_id = NULL WHERE default_provider_id = ?", + ) + .bind(provider_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + sqlx::query("UPDATE video_tasks SET provider_id = NULL WHERE provider_id = ?") + .bind(provider_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + sqlx::query("DELETE FROM request_candidates WHERE provider_id = ?") + .bind(provider_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + } + + for endpoint_id in endpoint_ids { + sqlx::query("UPDATE video_tasks SET endpoint_id = NULL WHERE endpoint_id = ?") + .bind(endpoint_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + sqlx::query("DELETE FROM request_candidates WHERE endpoint_id = ?") + .bind(endpoint_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + } + + for key_id in key_ids { + sqlx::query("DELETE FROM gemini_file_mappings WHERE key_id = ?") + .bind(key_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + sqlx::query("UPDATE video_tasks SET key_id = NULL WHERE key_id = ?") + .bind(key_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + } + + tx.commit().await.map_sql_err()?; Ok(()) } @@ -990,10 +1040,18 @@ impl ProviderCatalogWriteRepository for MysqlProviderCatalogReadRepository { async fn cleanup_deleted_provider_refs( &self, provider_id: &str, + provider_deleted: bool, endpoint_ids: &[String], key_ids: &[String], ) -> Result<(), DataLayerError> { - Self::cleanup_deleted_provider_refs(self, provider_id, endpoint_ids, key_ids).await + Self::cleanup_deleted_provider_refs( + self, + provider_id, + provider_deleted, + endpoint_ids, + key_ids, + ) + .await } async fn create_endpoint( diff --git a/crates/aether-data/src/repository/provider_catalog/postgres.rs b/crates/aether-data/src/repository/provider_catalog/postgres.rs index 0c7a5beae..71c9e2a22 100644 --- a/crates/aether-data/src/repository/provider_catalog/postgres.rs +++ b/crates/aether-data/src/repository/provider_catalog/postgres.rs @@ -994,6 +994,7 @@ WHERE id = $1 pub async fn cleanup_deleted_provider_refs( &self, provider_id: &str, + provider_deleted: bool, endpoint_ids: &[String], key_ids: &[String], ) -> Result<(), DataLayerError> { @@ -1005,37 +1006,27 @@ WHERE id = $1 let mut tx = self.pool.begin().await.map_postgres_err()?; - sqlx::query( - "UPDATE user_preferences SET default_provider_id = NULL WHERE default_provider_id = $1", - ) - .bind(provider_id) - .execute(&mut *tx) - .await - .map_postgres_err()?; - sqlx::query("UPDATE usage SET provider_id = NULL WHERE provider_id = $1") - .bind(provider_id) - .execute(&mut *tx) - .await - .map_postgres_err()?; - sqlx::query("UPDATE video_tasks SET provider_id = NULL WHERE provider_id = $1") - .bind(provider_id) - .execute(&mut *tx) - .await - .map_postgres_err()?; - sqlx::query("DELETE FROM request_candidates WHERE provider_id = $1") + if provider_deleted { + sqlx::query( + "UPDATE user_preferences SET default_provider_id = NULL WHERE default_provider_id = $1", + ) .bind(provider_id) .execute(&mut *tx) .await .map_postgres_err()?; + sqlx::query("UPDATE video_tasks SET provider_id = NULL WHERE provider_id = $1") + .bind(provider_id) + .execute(&mut *tx) + .await + .map_postgres_err()?; + sqlx::query("DELETE FROM request_candidates WHERE provider_id = $1") + .bind(provider_id) + .execute(&mut *tx) + .await + .map_postgres_err()?; + } for endpoint_id in endpoint_ids { - sqlx::query( - "UPDATE usage SET provider_endpoint_id = NULL WHERE provider_endpoint_id = $1", - ) - .bind(endpoint_id) - .execute(&mut *tx) - .await - .map_postgres_err()?; sqlx::query("UPDATE video_tasks SET endpoint_id = NULL WHERE endpoint_id = $1") .bind(endpoint_id) .execute(&mut *tx) @@ -1054,13 +1045,6 @@ WHERE id = $1 .execute(&mut *tx) .await .map_postgres_err()?; - sqlx::query( - "UPDATE usage SET provider_api_key_id = NULL WHERE provider_api_key_id = $1", - ) - .bind(key_id) - .execute(&mut *tx) - .await - .map_postgres_err()?; sqlx::query("UPDATE video_tasks SET key_id = NULL WHERE key_id = $1") .bind(key_id) .execute(&mut *tx) @@ -1068,16 +1052,18 @@ WHERE id = $1 .map_postgres_err()?; } - sqlx::query("DELETE FROM api_key_provider_mappings WHERE provider_id = $1") - .bind(provider_id) - .execute(&mut *tx) - .await - .map_postgres_err()?; - sqlx::query("DELETE FROM provider_usage_tracking WHERE provider_id = $1") - .bind(provider_id) - .execute(&mut *tx) - .await - .map_postgres_err()?; + if provider_deleted { + sqlx::query("DELETE FROM api_key_provider_mappings WHERE provider_id = $1") + .bind(provider_id) + .execute(&mut *tx) + .await + .map_postgres_err()?; + sqlx::query("DELETE FROM provider_usage_tracking WHERE provider_id = $1") + .bind(provider_id) + .execute(&mut *tx) + .await + .map_postgres_err()?; + } tx.commit().await.map_err(postgres_error)?; Ok(()) @@ -1995,10 +1981,18 @@ impl ProviderCatalogWriteRepository for SqlxProviderCatalogReadRepository { async fn cleanup_deleted_provider_refs( &self, provider_id: &str, + provider_deleted: bool, endpoint_ids: &[String], key_ids: &[String], ) -> Result<(), DataLayerError> { - Self::cleanup_deleted_provider_refs(self, provider_id, endpoint_ids, key_ids).await + Self::cleanup_deleted_provider_refs( + self, + provider_id, + provider_deleted, + endpoint_ids, + key_ids, + ) + .await } async fn create_endpoint( diff --git a/crates/aether-data/src/repository/provider_catalog/sqlite.rs b/crates/aether-data/src/repository/provider_catalog/sqlite.rs index b30ea042c..5e0d948ff 100644 --- a/crates/aether-data/src/repository/provider_catalog/sqlite.rs +++ b/crates/aether-data/src/repository/provider_catalog/sqlite.rs @@ -734,10 +734,60 @@ WHERE id = ? pub async fn cleanup_deleted_provider_refs( &self, provider_id: &str, - _endpoint_ids: &[String], - _key_ids: &[String], + provider_deleted: bool, + endpoint_ids: &[String], + key_ids: &[String], ) -> Result<(), DataLayerError> { validate_non_empty(provider_id, "provider catalog provider_id")?; + let mut tx = self.pool.begin().await.map_sql_err()?; + + if provider_deleted { + sqlx::query( + "UPDATE user_preferences SET default_provider_id = NULL WHERE default_provider_id = ?", + ) + .bind(provider_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + sqlx::query("UPDATE video_tasks SET provider_id = NULL WHERE provider_id = ?") + .bind(provider_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + sqlx::query("DELETE FROM request_candidates WHERE provider_id = ?") + .bind(provider_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + } + + for endpoint_id in endpoint_ids { + sqlx::query("UPDATE video_tasks SET endpoint_id = NULL WHERE endpoint_id = ?") + .bind(endpoint_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + sqlx::query("DELETE FROM request_candidates WHERE endpoint_id = ?") + .bind(endpoint_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + } + + for key_id in key_ids { + sqlx::query("DELETE FROM gemini_file_mappings WHERE key_id = ?") + .bind(key_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + sqlx::query("UPDATE video_tasks SET key_id = NULL WHERE key_id = ?") + .bind(key_id) + .execute(&mut *tx) + .await + .map_sql_err()?; + } + + tx.commit().await.map_sql_err()?; Ok(()) } @@ -1399,10 +1449,18 @@ impl ProviderCatalogWriteRepository for SqliteProviderCatalogReadRepository { async fn cleanup_deleted_provider_refs( &self, provider_id: &str, + provider_deleted: bool, endpoint_ids: &[String], key_ids: &[String], ) -> Result<(), DataLayerError> { - Self::cleanup_deleted_provider_refs(self, provider_id, endpoint_ids, key_ids).await + Self::cleanup_deleted_provider_refs( + self, + provider_id, + provider_deleted, + endpoint_ids, + key_ids, + ) + .await } async fn create_endpoint( diff --git a/crates/aether-data/src/repository/usage/memory.rs b/crates/aether-data/src/repository/usage/memory.rs index f95a557c3..192525021 100644 --- a/crates/aether-data/src/repository/usage/memory.rs +++ b/crates/aether-data/src/repository/usage/memory.rs @@ -225,6 +225,25 @@ fn accumulate_api_key_usage_contribution( }; } +fn usage_audit_client_family(item: &StoredRequestUsageAudit) -> Option<&str> { + item.client_family + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .or_else(|| usage_request_metadata_client_family(item.request_metadata.as_ref())) +} + +fn usage_admin_unknown_label(value: &str) -> bool { + matches!( + value.trim().to_ascii_lowercase().as_str(), + "unknown" | "unknow" + ) +} + +fn usage_has_admin_unknown_model_or_provider(item: &StoredRequestUsageAudit) -> bool { + usage_admin_unknown_label(&item.model) || usage_admin_unknown_label(&item.provider_name) +} + fn usage_matches_list_query(item: &StoredRequestUsageAudit, query: &UsageAuditListQuery) -> bool { // The field is historically named `created_at_unix_ms`, but usage audit rows // across gateway handlers, SQL repositories and tests are stored as epoch seconds. @@ -258,6 +277,21 @@ fn usage_matches_list_query(item: &StoredRequestUsageAudit, query: &UsageAuditLi return false; } } + if let Some(client_family) = query + .client_family + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + { + if !usage_audit_client_family(item) + .is_some_and(|value| value.eq_ignore_ascii_case(client_family)) + { + return false; + } + } + if query.exclude_unknown_model_or_provider && usage_has_admin_unknown_model_or_provider(item) { + return false; + } if let Some(statuses) = query.statuses.as_ref() { if !statuses.iter().any(|status| status == &item.status) { return false; @@ -324,6 +358,21 @@ fn usage_matches_keyword_search_query( return false; } } + if let Some(client_family) = query + .client_family + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + { + if !usage_audit_client_family(item) + .is_some_and(|value| value.eq_ignore_ascii_case(client_family)) + { + return false; + } + } + if query.exclude_unknown_model_or_provider && usage_has_admin_unknown_model_or_provider(item) { + return false; + } if let Some(statuses) = query.statuses.as_ref() { if !statuses.iter().any(|status| status == &item.status) { return false; diff --git a/crates/aether-data/src/repository/usage/postgres/mod.rs b/crates/aether-data/src/repository/usage/postgres/mod.rs index 8ec84325a..e47fc26f2 100644 --- a/crates/aether-data/src/repository/usage/postgres/mod.rs +++ b/crates/aether-data/src/repository/usage/postgres/mod.rs @@ -1538,6 +1538,45 @@ const REBUILD_PROVIDER_API_KEY_CODEX_WINDOW_USAGE_STATS_SQL: &str = include_str!("queries/rebuild_provider_api_key_codex_window_usage_stats_sql.sql"); const LIST_USAGE_AUDITS_PREFIX: &str = include_str!("queries/list_usage_audits_prefix.sql"); +fn push_postgres_usage_where(builder: &mut QueryBuilder<'_, Postgres>, has_where: &mut bool) { + builder.push(if *has_where { " AND " } else { " WHERE " }); + *has_where = true; +} + +fn push_postgres_usage_client_family_filter( + builder: &mut QueryBuilder<'_, Postgres>, + has_where: &mut bool, + client_family: Option<&str>, +) { + let Some(client_family) = client_family + .map(str::trim) + .filter(|value| !value.is_empty()) + else { + return; + }; + + push_postgres_usage_where(builder, has_where); + builder + .push("LOWER(COALESCE(NULLIF(BTRIM(\"usage\".request_metadata->'client_session_affinity'->>'client_family'), ''), NULLIF(BTRIM(\"usage\".request_metadata->>'client_family'), ''))) = ") + .push_bind(client_family.to_ascii_lowercase()); +} + +fn push_postgres_usage_exclude_unknown_filter( + builder: &mut QueryBuilder<'_, Postgres>, + has_where: &mut bool, + exclude_unknown_model_or_provider: bool, +) { + if !exclude_unknown_model_or_provider { + return; + } + + push_postgres_usage_where(builder, has_where); + builder.push( + "(lower(BTRIM(COALESCE(\"usage\".model, ''))) NOT IN ('unknown', 'unknow') \ +AND lower(BTRIM(COALESCE(\"usage\".provider_name, ''))) NOT IN ('unknown', 'unknow'))", + ); +} + const USAGE_PROVIDER_IDENTITY_FILTER_SQL: &str = r#" AND ( ( BTRIM(COALESCE("usage".provider_id, '')) <> '' @@ -2388,6 +2427,21 @@ ORDER BY request_count DESC, "usage".provider_name ASC } } + pub async fn find_by_request_id_shallow( + &self, + request_id: &str, + ) -> Result, DataLayerError> { + let sql = shallow_usage_body_projection_sql(FIND_BY_REQUEST_ID_SQL); + let row = sqlx::query(&sql) + .bind(request_id) + .fetch_optional(&self.pool) + .await + .map_postgres_err()?; + row.as_ref() + .map(|row| map_usage_row(row, false)) + .transpose() + } + pub async fn find_by_id( &self, id: &str, @@ -2573,6 +2627,16 @@ ORDER BY request_count DESC, "usage".provider_name ASC .push("\"usage\".api_format = ") .push_bind(api_format.to_string()); } + push_postgres_usage_client_family_filter( + &mut builder, + &mut has_where, + query.client_family.as_deref(), + ); + push_postgres_usage_exclude_unknown_filter( + &mut builder, + &mut has_where, + query.exclude_unknown_model_or_provider, + ); if let Some(statuses) = query.statuses.as_deref() { if !statuses.is_empty() { builder.push(if has_where { " AND " } else { " WHERE " }); @@ -2675,6 +2739,16 @@ OR (\"usage\".error_message IS NOT NULL AND BTRIM(\"usage\".error_message) <> '' .push("\"usage\".api_format = ") .push_bind(api_format.to_string()); } + push_postgres_usage_client_family_filter( + &mut builder, + &mut has_where, + query.client_family.as_deref(), + ); + push_postgres_usage_exclude_unknown_filter( + &mut builder, + &mut has_where, + query.exclude_unknown_model_or_provider, + ); if let Some(statuses) = query.statuses.as_deref() { if !statuses.is_empty() { builder.push(if has_where { " AND " } else { " WHERE " }); @@ -2858,6 +2932,16 @@ OR (\"usage\".error_message IS NOT NULL AND BTRIM(\"usage\".error_message) <> '' .push("\"usage\".api_format = ") .push_bind(api_format.to_string()); } + push_postgres_usage_client_family_filter( + &mut builder, + &mut has_where, + query.client_family.as_deref(), + ); + push_postgres_usage_exclude_unknown_filter( + &mut builder, + &mut has_where, + query.exclude_unknown_model_or_provider, + ); if let Some(statuses) = query.statuses.as_deref() { if !statuses.is_empty() { builder.push(if has_where { " AND " } else { " WHERE " }); @@ -2949,6 +3033,16 @@ OR (\"usage\".error_message IS NOT NULL AND BTRIM(\"usage\".error_message) <> '' .push("\"usage\".api_format = ") .push_bind(api_format.to_string()); } + push_postgres_usage_client_family_filter( + &mut builder, + &mut has_where, + query.client_family.as_deref(), + ); + push_postgres_usage_exclude_unknown_filter( + &mut builder, + &mut has_where, + query.exclude_unknown_model_or_provider, + ); if let Some(statuses) = query.statuses.as_deref() { if !statuses.is_empty() { builder.push(if has_where { " AND " } else { " WHERE " }); @@ -8107,6 +8201,12 @@ ORDER BY "usage".user_id ASC .bind(&request_metadata_json) .bind(usage.finalized_at_unix_secs.map(|value| value as f64)) .bind(usage.created_at_unix_ms.map(|value| value as f64)) + .bind(i64::try_from(usage.updated_at_unix_secs).map_err(|_| { + DataLayerError::InvalidInput(format!( + "usage.updated_at_unix_secs out of range: {}", + usage.updated_at_unix_secs + )) + })?) .bind(request_body_storage.has_detached_blob()) .bind(provider_request_body_storage.has_detached_blob()) .bind(response_body_storage.has_detached_blob()) @@ -8857,6 +8957,13 @@ impl UsageReadRepository for SqlxUsageReadRepository { Self::find_by_request_id(self, request_id).await } + async fn find_by_request_id_shallow( + &self, + request_id: &str, + ) -> Result, DataLayerError> { + Self::find_by_request_id_shallow(self, request_id).await + } + async fn resolve_body_ref(&self, body_ref: &str) -> Result, DataLayerError> { Self::resolve_body_ref(self, body_ref).await } @@ -10147,6 +10254,43 @@ fn map_usage_row( Ok(usage) } +fn shallow_usage_body_projection_sql(sql: &str) -> String { + let replacements = [ + ("\"usage\".request_body,", "NULL::json AS request_body,"), + ( + "\"usage\".request_body_compressed,", + "CASE WHEN \"usage\".request_body_compressed IS NULL THEN NULL ELSE ''::bytea END AS request_body_compressed,", + ), + ( + "\"usage\".provider_request_body,", + "NULL::json AS provider_request_body,", + ), + ( + "\"usage\".provider_request_body_compressed,", + "CASE WHEN \"usage\".provider_request_body_compressed IS NULL THEN NULL ELSE ''::bytea END AS provider_request_body_compressed,", + ), + ("\"usage\".response_body,", "NULL::json AS response_body,"), + ( + "\"usage\".response_body_compressed,", + "CASE WHEN \"usage\".response_body_compressed IS NULL THEN NULL ELSE ''::bytea END AS response_body_compressed,", + ), + ( + "\"usage\".client_response_body,", + "NULL::json AS client_response_body,", + ), + ( + "\"usage\".client_response_body_compressed,", + "CASE WHEN \"usage\".client_response_body_compressed IS NULL THEN NULL ELSE ''::bytea END AS client_response_body_compressed,", + ), + ]; + + replacements + .into_iter() + .fold(sql.to_string(), |current, (from, to)| { + current.replace(from, to) + }) +} + fn to_i32(value: u64) -> Result { i32::try_from(value).map_err(|_| { DataLayerError::UnexpectedValue(format!("invalid usage integer value: {value}")) diff --git a/crates/aether-data/src/repository/usage/postgres/queries/find_by_id_sql.sql b/crates/aether-data/src/repository/usage/postgres/queries/find_by_id_sql.sql index f1ef3231b..e67e19c0a 100644 --- a/crates/aether-data/src/repository/usage/postgres/queries/find_by_id_sql.sql +++ b/crates/aether-data/src/repository/usage/postgres/queries/find_by_id_sql.sql @@ -156,14 +156,11 @@ SELECT usage_settlement_snapshots.billing_rule_id AS settlement_billing_rule_id, usage_settlement_snapshots.billing_rule_version AS settlement_billing_rule_version, CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) AS created_at_unix_ms, - CAST( - EXTRACT( - EPOCH FROM COALESCE( - usage_settlement_snapshots.finalized_at, - "usage".finalized_at, - "usage".created_at - ) - ) AS BIGINT + GREATEST( + COALESCE(NULLIF("usage".updated_at_unix_secs, 0), 0), + COALESCE(CAST(EXTRACT(EPOCH FROM usage_settlement_snapshots.finalized_at) AS BIGINT), 0), + COALESCE(CAST(EXTRACT(EPOCH FROM "usage".finalized_at) AS BIGINT), 0), + CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) ) AS updated_at_unix_secs, CAST( EXTRACT( diff --git a/crates/aether-data/src/repository/usage/postgres/queries/find_by_request_id_sql.sql b/crates/aether-data/src/repository/usage/postgres/queries/find_by_request_id_sql.sql index 1fee64f47..c95d1f277 100644 --- a/crates/aether-data/src/repository/usage/postgres/queries/find_by_request_id_sql.sql +++ b/crates/aether-data/src/repository/usage/postgres/queries/find_by_request_id_sql.sql @@ -160,14 +160,11 @@ SELECT usage_settlement_snapshots.billing_rule_id AS settlement_billing_rule_id, usage_settlement_snapshots.billing_rule_version AS settlement_billing_rule_version, CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) AS created_at_unix_ms, - CAST( - EXTRACT( - EPOCH FROM COALESCE( - usage_settlement_snapshots.finalized_at, - "usage".finalized_at, - "usage".created_at - ) - ) AS BIGINT + GREATEST( + COALESCE(NULLIF("usage".updated_at_unix_secs, 0), 0), + COALESCE(CAST(EXTRACT(EPOCH FROM usage_settlement_snapshots.finalized_at) AS BIGINT), 0), + COALESCE(CAST(EXTRACT(EPOCH FROM "usage".finalized_at) AS BIGINT), 0), + CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) ) AS updated_at_unix_secs, CAST( EXTRACT( diff --git a/crates/aether-data/src/repository/usage/postgres/queries/list_recent_usage_audits_prefix.sql b/crates/aether-data/src/repository/usage/postgres/queries/list_recent_usage_audits_prefix.sql index ac0569fd9..a2d786624 100644 --- a/crates/aether-data/src/repository/usage/postgres/queries/list_recent_usage_audits_prefix.sql +++ b/crates/aether-data/src/repository/usage/postgres/queries/list_recent_usage_audits_prefix.sql @@ -114,18 +114,8 @@ SELECT OR NULLIF(BTRIM("usage".request_metadata->>'user_agent'), '') IS NOT NULL OR NULLIF(BTRIM("usage".request_metadata->>'request_path'), '') IS NOT NULL OR NULLIF(BTRIM("usage".request_metadata->>'request_path_and_query'), '') IS NOT NULL - OR CASE - WHEN jsonb_typeof("usage".provider_request_body::jsonb) = 'object' THEN COALESCE( - NULLIF(BTRIM("usage".provider_request_body->>'reasoning_effort'), ''), - NULLIF(BTRIM("usage".provider_request_body->'reasoning'->>'effort'), ''), - NULLIF(BTRIM("usage".provider_request_body->'output_config'->>'effort'), '') - ) - ELSE NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), '') - END IS NOT NULL - OR CASE - WHEN jsonb_typeof("usage".provider_request_body::jsonb) = 'object' THEN NULLIF(BTRIM("usage".provider_request_body->>'service_tier'), '') - ELSE NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), '') - END IS NOT NULL + OR NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), '') IS NOT NULL + OR NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), '') IS NOT NULL OR ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false') OR ("usage".request_metadata->>'upstream_is_stream') IN ('true', 'false') THEN jsonb_strip_nulls(jsonb_build_object( @@ -138,19 +128,9 @@ SELECT 'request_path_and_query', NULLIF(BTRIM("usage".request_metadata->>'request_path_and_query'), ''), 'provider_reasoning_effort', - CASE - WHEN jsonb_typeof("usage".provider_request_body::jsonb) = 'object' THEN COALESCE( - NULLIF(BTRIM("usage".provider_request_body->>'reasoning_effort'), ''), - NULLIF(BTRIM("usage".provider_request_body->'reasoning'->>'effort'), ''), - NULLIF(BTRIM("usage".provider_request_body->'output_config'->>'effort'), '') - ) - ELSE NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), '') - END, + NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), ''), 'provider_service_tier', - CASE - WHEN jsonb_typeof("usage".provider_request_body::jsonb) = 'object' THEN NULLIF(BTRIM("usage".provider_request_body->>'service_tier'), '') - ELSE NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), '') - END, + NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), ''), 'client_requested_stream', CASE WHEN ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false') @@ -238,14 +218,11 @@ SELECT usage_settlement_snapshots.billing_rule_id AS settlement_billing_rule_id, usage_settlement_snapshots.billing_rule_version AS settlement_billing_rule_version, CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) AS created_at_unix_ms, - CAST( - EXTRACT( - EPOCH FROM COALESCE( - usage_settlement_snapshots.finalized_at, - "usage".finalized_at, - "usage".created_at - ) - ) AS BIGINT + GREATEST( + COALESCE(NULLIF("usage".updated_at_unix_secs, 0), 0), + COALESCE(CAST(EXTRACT(EPOCH FROM usage_settlement_snapshots.finalized_at) AS BIGINT), 0), + COALESCE(CAST(EXTRACT(EPOCH FROM "usage".finalized_at) AS BIGINT), 0), + CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) ) AS updated_at_unix_secs, CAST( EXTRACT( diff --git a/crates/aether-data/src/repository/usage/postgres/queries/list_usage_audits_prefix.sql b/crates/aether-data/src/repository/usage/postgres/queries/list_usage_audits_prefix.sql index ac0569fd9..a2d786624 100644 --- a/crates/aether-data/src/repository/usage/postgres/queries/list_usage_audits_prefix.sql +++ b/crates/aether-data/src/repository/usage/postgres/queries/list_usage_audits_prefix.sql @@ -114,18 +114,8 @@ SELECT OR NULLIF(BTRIM("usage".request_metadata->>'user_agent'), '') IS NOT NULL OR NULLIF(BTRIM("usage".request_metadata->>'request_path'), '') IS NOT NULL OR NULLIF(BTRIM("usage".request_metadata->>'request_path_and_query'), '') IS NOT NULL - OR CASE - WHEN jsonb_typeof("usage".provider_request_body::jsonb) = 'object' THEN COALESCE( - NULLIF(BTRIM("usage".provider_request_body->>'reasoning_effort'), ''), - NULLIF(BTRIM("usage".provider_request_body->'reasoning'->>'effort'), ''), - NULLIF(BTRIM("usage".provider_request_body->'output_config'->>'effort'), '') - ) - ELSE NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), '') - END IS NOT NULL - OR CASE - WHEN jsonb_typeof("usage".provider_request_body::jsonb) = 'object' THEN NULLIF(BTRIM("usage".provider_request_body->>'service_tier'), '') - ELSE NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), '') - END IS NOT NULL + OR NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), '') IS NOT NULL + OR NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), '') IS NOT NULL OR ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false') OR ("usage".request_metadata->>'upstream_is_stream') IN ('true', 'false') THEN jsonb_strip_nulls(jsonb_build_object( @@ -138,19 +128,9 @@ SELECT 'request_path_and_query', NULLIF(BTRIM("usage".request_metadata->>'request_path_and_query'), ''), 'provider_reasoning_effort', - CASE - WHEN jsonb_typeof("usage".provider_request_body::jsonb) = 'object' THEN COALESCE( - NULLIF(BTRIM("usage".provider_request_body->>'reasoning_effort'), ''), - NULLIF(BTRIM("usage".provider_request_body->'reasoning'->>'effort'), ''), - NULLIF(BTRIM("usage".provider_request_body->'output_config'->>'effort'), '') - ) - ELSE NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), '') - END, + NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), ''), 'provider_service_tier', - CASE - WHEN jsonb_typeof("usage".provider_request_body::jsonb) = 'object' THEN NULLIF(BTRIM("usage".provider_request_body->>'service_tier'), '') - ELSE NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), '') - END, + NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), ''), 'client_requested_stream', CASE WHEN ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false') @@ -238,14 +218,11 @@ SELECT usage_settlement_snapshots.billing_rule_id AS settlement_billing_rule_id, usage_settlement_snapshots.billing_rule_version AS settlement_billing_rule_version, CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) AS created_at_unix_ms, - CAST( - EXTRACT( - EPOCH FROM COALESCE( - usage_settlement_snapshots.finalized_at, - "usage".finalized_at, - "usage".created_at - ) - ) AS BIGINT + GREATEST( + COALESCE(NULLIF("usage".updated_at_unix_secs, 0), 0), + COALESCE(CAST(EXTRACT(EPOCH FROM usage_settlement_snapshots.finalized_at) AS BIGINT), 0), + COALESCE(CAST(EXTRACT(EPOCH FROM "usage".finalized_at) AS BIGINT), 0), + CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) ) AS updated_at_unix_secs, CAST( EXTRACT( diff --git a/crates/aether-data/src/repository/usage/postgres/queries/upsert_sql.sql b/crates/aether-data/src/repository/usage/postgres/queries/upsert_sql.sql index 6d7ed9758..bda6a8ee3 100644 --- a/crates/aether-data/src/repository/usage/postgres/queries/upsert_sql.sql +++ b/crates/aether-data/src/repository/usage/postgres/queries/upsert_sql.sql @@ -56,7 +56,8 @@ INSERT INTO "usage" ( client_response_body_compressed, request_metadata, finalized_at, - created_at + created_at, + updated_at_unix_secs ) VALUES ( $1, $2, @@ -144,7 +145,11 @@ INSERT INTO "usage" ( WHEN $54 IS NULL THEN NULL ELSE TO_TIMESTAMP($54::double precision) END, - COALESCE(TO_TIMESTAMP($55::double precision), NOW()) + COALESCE(TO_TIMESTAMP($55::double precision), NOW()), + COALESCE( + NULLIF($56::bigint, 0), + CAST(EXTRACT(EPOCH FROM COALESCE(TO_TIMESTAMP($55::double precision), NOW())) AS BIGINT) + ) ) ON CONFLICT (request_id) DO UPDATE SET @@ -218,42 +223,49 @@ DO UPDATE SET billing_status = CASE WHEN "usage".billing_status = 'pending' THEN EXCLUDED.billing_status ELSE "usage".billing_status END, request_headers = NULL, request_body = CASE WHEN "usage".billing_status = 'pending' THEN CASE - WHEN EXCLUDED.request_body_compressed IS NOT NULL OR $56 THEN NULL + WHEN EXCLUDED.request_body_compressed IS NOT NULL OR $57 THEN NULL ELSE COALESCE(EXCLUDED.request_body, "usage".request_body) END ELSE "usage".request_body END, request_body_compressed = CASE WHEN "usage".billing_status = 'pending' THEN CASE - WHEN EXCLUDED.request_body IS NOT NULL OR $56 THEN NULL + WHEN EXCLUDED.request_body IS NOT NULL OR $57 THEN NULL ELSE COALESCE(EXCLUDED.request_body_compressed, "usage".request_body_compressed) END ELSE "usage".request_body_compressed END, provider_request_headers = NULL, provider_request_body = CASE WHEN "usage".billing_status = 'pending' THEN CASE - WHEN EXCLUDED.provider_request_body_compressed IS NOT NULL OR $57 THEN NULL + WHEN EXCLUDED.provider_request_body_compressed IS NOT NULL OR $58 THEN NULL ELSE COALESCE(EXCLUDED.provider_request_body, "usage".provider_request_body) END ELSE "usage".provider_request_body END, provider_request_body_compressed = CASE WHEN "usage".billing_status = 'pending' THEN CASE - WHEN EXCLUDED.provider_request_body IS NOT NULL OR $57 THEN NULL + WHEN EXCLUDED.provider_request_body IS NOT NULL OR $58 THEN NULL ELSE COALESCE(EXCLUDED.provider_request_body_compressed, "usage".provider_request_body_compressed) END ELSE "usage".provider_request_body_compressed END, response_headers = NULL, response_body = CASE WHEN "usage".billing_status = 'pending' THEN CASE - WHEN EXCLUDED.response_body_compressed IS NOT NULL OR $58 THEN NULL + WHEN EXCLUDED.response_body_compressed IS NOT NULL OR $59 THEN NULL ELSE COALESCE(EXCLUDED.response_body, "usage".response_body) END ELSE "usage".response_body END, response_body_compressed = CASE WHEN "usage".billing_status = 'pending' THEN CASE - WHEN EXCLUDED.response_body IS NOT NULL OR $58 THEN NULL + WHEN EXCLUDED.response_body IS NOT NULL OR $59 THEN NULL ELSE COALESCE(EXCLUDED.response_body_compressed, "usage".response_body_compressed) END ELSE "usage".response_body_compressed END, client_response_headers = NULL, client_response_body = CASE WHEN "usage".billing_status = 'pending' THEN CASE - WHEN EXCLUDED.client_response_body_compressed IS NOT NULL OR $59 THEN NULL + WHEN EXCLUDED.client_response_body_compressed IS NOT NULL OR $60 THEN NULL ELSE COALESCE(EXCLUDED.client_response_body, "usage".client_response_body) END ELSE "usage".client_response_body END, client_response_body_compressed = CASE WHEN "usage".billing_status = 'pending' THEN CASE - WHEN EXCLUDED.client_response_body IS NOT NULL OR $59 THEN NULL + WHEN EXCLUDED.client_response_body IS NOT NULL OR $60 THEN NULL ELSE COALESCE(EXCLUDED.client_response_body_compressed, "usage".client_response_body_compressed) END ELSE "usage".client_response_body_compressed END, request_metadata = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.request_metadata, "usage".request_metadata) ELSE "usage".request_metadata END, - finalized_at = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.finalized_at, "usage".finalized_at) ELSE "usage".finalized_at END + finalized_at = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.finalized_at, "usage".finalized_at) ELSE "usage".finalized_at END, + updated_at_unix_secs = CASE WHEN "usage".billing_status = 'pending' THEN + GREATEST( + COALESCE(NULLIF("usage".updated_at_unix_secs, 0), 0), + COALESCE(NULLIF(EXCLUDED.updated_at_unix_secs, 0), 0), + CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) + ) + ELSE "usage".updated_at_unix_secs END RETURNING id, request_id, @@ -352,5 +364,9 @@ RETURNING NULL::varchar AS settlement_billing_rule_id, NULL::varchar AS settlement_billing_rule_version, CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_ms, - CAST(EXTRACT(EPOCH FROM COALESCE(finalized_at, created_at)) AS BIGINT) AS updated_at_unix_secs, + GREATEST( + COALESCE(NULLIF(updated_at_unix_secs, 0), 0), + COALESCE(CAST(EXTRACT(EPOCH FROM finalized_at) AS BIGINT), 0), + CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) + ) AS updated_at_unix_secs, CAST(EXTRACT(EPOCH FROM finalized_at) AS BIGINT) AS finalized_at_unix_secs diff --git a/crates/aether-data/src/repository/usage/postgres/tests.rs b/crates/aether-data/src/repository/usage/postgres/tests.rs index 9ccbe3e8b..10c3840b2 100644 --- a/crates/aether-data/src/repository/usage/postgres/tests.rs +++ b/crates/aether-data/src/repository/usage/postgres/tests.rs @@ -725,7 +725,8 @@ fn usage_sql_uses_json_null_placeholders_for_usage_payload_columns() { super::LIST_RECENT_USAGE_AUDITS_PREFIX, ] { assert!(sql.contains("jsonb_strip_nulls(jsonb_build_object(")); - assert!(sql.contains("jsonb_typeof(\"usage\".provider_request_body::jsonb)")); + assert!(!sql.contains("jsonb_typeof(\"usage\".provider_request_body::jsonb)")); + assert!(!sql.contains("\"usage\".provider_request_body->")); assert!(!sql.contains("jsonb_typeof(\"usage\".provider_request_body)")); assert!(sql.contains("'client_ip'")); assert!(sql.contains("request_metadata->>'client_ip'")); @@ -747,6 +748,33 @@ fn usage_sql_uses_json_null_placeholders_for_usage_payload_columns() { assert!(!super::LIST_RECENT_USAGE_AUDITS_PREFIX.contains("NULL::jsonb")); } +#[test] +fn usage_sql_admin_record_filters_are_pushed_into_postgres_queries() { + let source = include_str!("mod.rs"); + assert!(source.contains("push_postgres_usage_client_family_filter")); + assert!(source.contains("request_metadata->'client_session_affinity'->>'client_family'")); + assert!(source.contains("request_metadata->>'client_family'")); + assert!(source.contains("exclude_unknown_model_or_provider")); + assert!(source.contains("NOT IN ('unknown', 'unknow')")); +} + +#[test] +fn usage_sql_keyword_search_error_filter_keeps_where_state_before_keywords() { + let source = include_str!("mod.rs"); + let function = source + .split("pub async fn list_usage_audits_by_keyword_search") + .nth(1) + .and_then(|tail| tail.split("pub async fn count_usage_audits").next()) + .expect("keyword search function should be present"); + let error_filter = function + .split("if query.error_only") + .nth(1) + .and_then(|tail| tail.split("for (index, keyword)").next()) + .expect("error filter should precede keyword loop"); + + assert!(error_filter.contains("has_where = true;")); +} + #[test] fn usage_sql_reads_list_output_price_from_settlement_snapshots_before_legacy_usage_column() { assert!(super::LIST_USAGE_AUDITS_PREFIX @@ -804,6 +832,37 @@ fn usage_sql_writes_usage_settlement_pricing_snapshots() { assert!(super::UPSERT_USAGE_SETTLEMENT_PRICING_SNAPSHOT_SQL.contains("price_per_request")); } +#[test] +fn usage_sql_settlement_pricing_snapshot_billing_values_use_authoritative_incoming_values() { + let sql = super::UPSERT_USAGE_SETTLEMENT_PRICING_SNAPSHOT_SQL; + for field in [ + "billing_input_tokens", + "billing_effective_input_tokens", + "billing_output_tokens", + "billing_cache_creation_tokens", + "billing_cache_creation_5m_tokens", + "billing_cache_creation_1h_tokens", + "billing_cache_read_tokens", + "billing_total_input_context", + "billing_cache_creation_cost_usd", + "billing_cache_read_cost_usd", + "billing_total_cost_usd", + "billing_actual_total_cost_usd", + ] { + let assignment = format!( + "{field} = COALESCE(\n EXCLUDED.{field},\n usage_settlement_snapshots.{field}\n )" + ); + assert!( + sql.contains(assignment.as_str()), + "missing authoritative billing snapshot assignment: {assignment}" + ); + assert!( + !sql.contains(format!("{field} = GREATEST(").as_str()), + "billing snapshot field should not use max-only conflict resolution: {field}" + ); + } +} + #[test] fn usage_sql_upsert_recovers_missing_provider_links_after_billing_finalizes() { for assignment in [ @@ -820,25 +879,32 @@ fn usage_sql_upsert_recovers_missing_provider_links_after_billing_finalizes() { #[test] fn usage_sql_updates_usage_mirror_columns_from_terminal_events_only() { - for assignment in [ - "input_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".input_tokens, EXCLUDED.input_tokens) ELSE \"usage\".input_tokens END", - "output_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".output_tokens, EXCLUDED.output_tokens) ELSE \"usage\".output_tokens END", - "total_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".total_tokens, EXCLUDED.total_tokens) ELSE \"usage\".total_tokens END", - "input_output_total_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".input_output_total_tokens, EXCLUDED.input_output_total_tokens) ELSE \"usage\".input_output_total_tokens END", - "input_context_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".input_context_tokens, EXCLUDED.input_context_tokens) ELSE \"usage\".input_context_tokens END", - "cache_creation_input_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_creation_input_tokens, EXCLUDED.cache_creation_input_tokens) ELSE \"usage\".cache_creation_input_tokens END", - "cache_creation_input_tokens_5m = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_creation_input_tokens_5m, EXCLUDED.cache_creation_input_tokens_5m) ELSE \"usage\".cache_creation_input_tokens_5m END", - "cache_creation_input_tokens_1h = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_creation_input_tokens_1h, EXCLUDED.cache_creation_input_tokens_1h) ELSE \"usage\".cache_creation_input_tokens_1h END", - "cache_read_input_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_read_input_tokens, EXCLUDED.cache_read_input_tokens) ELSE \"usage\".cache_read_input_tokens END", - "cache_creation_cost_usd = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_creation_cost_usd, EXCLUDED.cache_creation_cost_usd) ELSE \"usage\".cache_creation_cost_usd END", - "cache_read_cost_usd = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_read_cost_usd, EXCLUDED.cache_read_cost_usd) ELSE \"usage\".cache_read_cost_usd END", - "total_cost_usd = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".total_cost_usd, EXCLUDED.total_cost_usd) ELSE \"usage\".total_cost_usd END", - "actual_total_cost_usd = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".actual_total_cost_usd, EXCLUDED.actual_total_cost_usd) ELSE \"usage\".actual_total_cost_usd END", + for field in [ + "input_tokens", + "output_tokens", + "total_tokens", + "input_output_total_tokens", + "input_context_tokens", + "cache_creation_input_tokens", + "cache_creation_input_tokens_5m", + "cache_creation_input_tokens_1h", + "cache_read_input_tokens", + "cache_creation_cost_usd", + "cache_read_cost_usd", + "total_cost_usd", + "actual_total_cost_usd", ] { + let assignment = format!( + "{field} = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".{field}, EXCLUDED.{field}) ELSE \"usage\".{field} END" + ); assert!( - super::UPSERT_SQL.contains(assignment), + super::UPSERT_SQL.contains(assignment.as_str()), "missing terminal mirror assignment: {assignment}" ); + assert!( + !super::UPSERT_SQL.contains(format!("{field} = CASE WHEN EXCLUDED.status IN").as_str()), + "terminal mirror assignment must keep pending billing guard: {field}" + ); } } @@ -880,13 +946,13 @@ fn usage_sql_clears_legacy_header_columns_on_upsert() { #[test] fn usage_sql_detached_body_flags_clear_inline_and_compressed_columns() { assert!(super::UPSERT_SQL - .contains("WHEN EXCLUDED.request_body_compressed IS NOT NULL OR $56 THEN NULL")); + .contains("WHEN EXCLUDED.request_body_compressed IS NOT NULL OR $57 THEN NULL")); assert!(super::UPSERT_SQL - .contains("WHEN EXCLUDED.provider_request_body_compressed IS NOT NULL OR $57 THEN NULL")); + .contains("WHEN EXCLUDED.provider_request_body_compressed IS NOT NULL OR $58 THEN NULL")); assert!(super::UPSERT_SQL - .contains("WHEN EXCLUDED.response_body_compressed IS NOT NULL OR $58 THEN NULL")); + .contains("WHEN EXCLUDED.response_body_compressed IS NOT NULL OR $59 THEN NULL")); assert!(super::UPSERT_SQL - .contains("WHEN EXCLUDED.client_response_body_compressed IS NOT NULL OR $59 THEN NULL")); + .contains("WHEN EXCLUDED.client_response_body_compressed IS NOT NULL OR $60 THEN NULL")); } #[test] diff --git a/crates/aether-data/src/repository/usage/sqlite.rs b/crates/aether-data/src/repository/usage/sqlite.rs index 8809f0f2e..34c4e07cb 100644 --- a/crates/aether-data/src/repository/usage/sqlite.rs +++ b/crates/aether-data/src/repository/usage/sqlite.rs @@ -455,6 +455,21 @@ fn push_sqlite_usage_list_filters( .push("api_format = ") .push_bind(api_format.to_string()); } + if let Some(client_family) = query.client_family.as_deref().map(str::trim) { + if !client_family.is_empty() { + push_sqlite_usage_where(builder, has_where); + builder + .push("LOWER(COALESCE(NULLIF(TRIM(CAST(json_extract(request_metadata, '$.client_session_affinity.client_family') AS TEXT)), ''), NULLIF(TRIM(CAST(json_extract(request_metadata, '$.client_family') AS TEXT)), ''))) = ") + .push_bind(client_family.to_ascii_lowercase()); + } + } + if query.exclude_unknown_model_or_provider { + push_sqlite_usage_where(builder, has_where); + builder.push( + "(LOWER(TRIM(COALESCE(model, ''))) NOT IN ('unknown', 'unknow') \ +AND LOWER(TRIM(COALESCE(provider_name, ''))) NOT IN ('unknown', 'unknow'))", + ); + } if let Some(statuses) = query.statuses.as_deref() { if !statuses.is_empty() { push_sqlite_usage_where(builder, has_where); @@ -514,6 +529,8 @@ fn push_sqlite_usage_keyword_filters( provider_name: query.provider_name.clone(), model: query.model.clone(), api_format: query.api_format.clone(), + client_family: query.client_family.clone(), + exclude_unknown_model_or_provider: query.exclude_unknown_model_or_provider, statuses: query.statuses.clone(), exclude_status_codes: query.exclude_status_codes.clone(), is_stream: query.is_stream, diff --git a/crates/aether-provider-transport/src/auth.rs b/crates/aether-provider-transport/src/auth.rs index 2b304ac31..57b41311f 100644 --- a/crates/aether-provider-transport/src/auth.rs +++ b/crates/aether-provider-transport/src/auth.rs @@ -1,7 +1,8 @@ use std::collections::BTreeMap; use super::headers::{ - should_skip_upstream_complete_passthrough_header, should_skip_upstream_passthrough_header, + normalize_upstream_accept_encoding, should_skip_upstream_complete_passthrough_header, + should_skip_upstream_passthrough_header, }; use super::snapshot::GatewayProviderTransportSnapshot; @@ -21,20 +22,18 @@ fn collect_passthrough_headers( if should_skip_upstream_passthrough_header(&key) { continue; } - let value = value.trim(); - if value.is_empty() { + let Some(value) = normalize_passthrough_header_value(&key, value) else { continue; - } - out.insert(key, value.to_string()); + }; + out.insert(key, value); } for (key, value) in extra_headers { let normalized_key = key.to_ascii_lowercase(); - let value = value.trim(); - if value.is_empty() { + let Some(value) = normalize_passthrough_header_value(&normalized_key, value) else { continue; - } - out.insert(normalized_key, value.to_string()); + }; + out.insert(normalized_key, value); } out @@ -53,25 +52,36 @@ fn collect_complete_passthrough_headers( if should_skip_upstream_complete_passthrough_header(&key) { continue; } - let value = value.trim(); - if value.is_empty() { + let Some(value) = normalize_passthrough_header_value(&key, value) else { continue; - } - out.insert(key, value.to_string()); + }; + out.insert(key, value); } for (key, value) in extra_headers { let normalized_key = key.to_ascii_lowercase(); - let value = value.trim(); - if value.is_empty() { + let Some(value) = normalize_passthrough_header_value(&normalized_key, value) else { continue; - } - out.insert(normalized_key, value.to_string()); + }; + out.insert(normalized_key, value); } out } +fn normalize_passthrough_header_value(key: &str, value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + return None; + } + + if key.eq_ignore_ascii_case("accept-encoding") { + return normalize_upstream_accept_encoding(value); + } + + Some(value.to_string()) +} + pub fn build_passthrough_headers( headers: &http::HeaderMap, extra_headers: &BTreeMap, @@ -312,7 +322,8 @@ fn bearer_auth_value(secret: &str) -> String { mod tests { use super::{ build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth, - resolve_local_openai_bearer_auth, resolve_local_standard_auth, + build_openai_passthrough_headers, resolve_local_openai_bearer_auth, + resolve_local_standard_auth, }; use crate::snapshot::{ GatewayProviderTransportEndpoint, GatewayProviderTransportKey, @@ -417,6 +428,28 @@ mod tests { ); } + #[test] + fn passthrough_headers_preserve_supported_response_compression() { + let mut headers = http::HeaderMap::new(); + headers.insert( + http::header::ACCEPT_ENCODING, + http::HeaderValue::from_static("gzip, br"), + ); + + let built = build_openai_passthrough_headers( + &headers, + "authorization", + "Bearer upstream", + &BTreeMap::new(), + Some("application/json"), + ); + + assert_eq!( + built.get("accept-encoding").map(String::as_str), + Some("gzip") + ); + } + #[test] fn claude_passthrough_headers_preserve_explicit_anthropic_version_override() { let mut headers = http::HeaderMap::new(); diff --git a/crates/aether-provider-transport/src/conversion.rs b/crates/aether-provider-transport/src/conversion.rs index 193514fc9..48b7fb930 100644 --- a/crates/aether-provider-transport/src/conversion.rs +++ b/crates/aether-provider-transport/src/conversion.rs @@ -592,6 +592,52 @@ mod tests { )); } + #[test] + fn cross_format_pair_requires_provider_or_endpoint_enablement() { + let disabled = transport_snapshot("custom", "openai:responses", "bearer", false, None); + assert!(!request_conversion_enabled_for_transport( + &disabled, + "claude:messages", + "openai:responses", + )); + assert_eq!( + candidate_transport_pair_skip_reason(&disabled, "claude:messages"), + Some("format_conversion_disabled") + ); + + let provider_enabled = + transport_snapshot("custom", "openai:responses", "bearer", true, None); + assert!(request_conversion_enabled_for_transport( + &provider_enabled, + "claude:messages", + "openai:responses", + )); + assert_eq!( + candidate_transport_pair_skip_reason(&provider_enabled, "claude:messages"), + None + ); + + let endpoint_enabled = transport_snapshot( + "custom", + "openai:responses", + "bearer", + false, + Some(json!({ + "enabled": true, + "accept_formats": ["claude:messages"], + })), + ); + assert!(request_conversion_enabled_for_transport( + &endpoint_enabled, + "claude:messages", + "openai:responses", + )); + assert_eq!( + candidate_transport_pair_skip_reason(&endpoint_enabled, "claude:messages"), + None + ); + } + #[test] fn vertex_gemini_transport_supports_cross_format_conversion_with_query_auth() { let transport = transport_snapshot( diff --git a/crates/aether-provider-transport/src/headers.rs b/crates/aether-provider-transport/src/headers.rs index 83c43b283..e3d105b34 100644 --- a/crates/aether-provider-transport/src/headers.rs +++ b/crates/aether-provider-transport/src/headers.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use aether_contracts::USAGE_SERVER_NOW_UNIX_MS_HEADER; pub fn should_skip_request_header(name: &str) -> bool { @@ -42,7 +44,6 @@ pub fn should_skip_upstream_passthrough_header(name: &str) -> bool { | "content-length" | "transfer-encoding" | "connection" - | "accept-encoding" | "content-encoding" | "x-real-ip" | "x-real-proto" @@ -68,7 +69,6 @@ pub(crate) fn should_skip_upstream_complete_passthrough_header(name: &str) -> bo | "content-length" | "transfer-encoding" | "connection" - | "accept-encoding" | "content-encoding" | "x-real-ip" | "x-real-proto" @@ -80,13 +80,102 @@ pub(crate) fn should_skip_upstream_complete_passthrough_header(name: &str) -> bo ) || should_skip_request_header(name) } +pub fn normalize_upstream_accept_encoding(value: &str) -> Option { + let mut accepted = Vec::new(); + let mut wildcard_allowed = false; + let mut gzip_disabled = false; + let mut deflate_disabled = false; + let mut identity_disabled = false; + + for item in value.split(',') { + let Some((token, normalized_item, enabled)) = parse_accept_encoding_item(item) else { + continue; + }; + match token.as_str() { + "gzip" if enabled => accepted.push(normalized_item), + "gzip" => gzip_disabled = true, + "deflate" if enabled => accepted.push(normalized_item), + "deflate" => deflate_disabled = true, + "identity" if enabled => accepted.push(normalized_item), + "identity" => identity_disabled = true, + "*" if enabled => wildcard_allowed = true, + _ => {} + } + } + + if !accepted.is_empty() { + return Some(accepted.join(", ")); + } + + if wildcard_allowed && !gzip_disabled { + Some("gzip".to_string()) + } else if wildcard_allowed && !deflate_disabled { + Some("deflate".to_string()) + } else if wildcard_allowed && !identity_disabled { + Some("identity".to_string()) + } else { + None + } +} + +fn parse_accept_encoding_item(raw_item: &str) -> Option<(String, String, bool)> { + let mut parts = raw_item.trim().split(';'); + let token = parts.next()?.trim().to_ascii_lowercase(); + if token.is_empty() { + return None; + } + + let mut enabled = true; + let mut normalized = token.clone(); + for raw_param in parts { + let param = raw_param.trim(); + if param.is_empty() { + continue; + } + let Some((name, value)) = param.split_once('=') else { + continue; + }; + if name.trim().eq_ignore_ascii_case("q") { + let value = value.trim(); + if q_value_is_zero(value) { + enabled = false; + continue; + } + normalized.push_str(";q="); + normalized.push_str(value); + } + } + + Some((token, normalized, enabled)) +} + +fn q_value_is_zero(value: &str) -> bool { + value + .trim_matches('"') + .parse::() + .is_ok_and(|q| q <= 0.0) +} + +pub fn force_identity_accept_encoding(headers: &mut BTreeMap) { + if let Some(existing_key) = headers + .keys() + .find(|key| key.eq_ignore_ascii_case("accept-encoding")) + .cloned() + { + headers.remove(&existing_key); + } + headers.insert("accept-encoding".to_string(), "identity".to_string()); +} + #[cfg(test)] mod tests { use super::{ + force_identity_accept_encoding, normalize_upstream_accept_encoding, should_skip_request_header, should_skip_upstream_complete_passthrough_header, should_skip_upstream_passthrough_header, }; use aether_contracts::USAGE_SERVER_NOW_UNIX_MS_HEADER; + use std::collections::BTreeMap; #[test] fn strips_all_stainless_headers() { @@ -111,6 +200,60 @@ mod tests { } } + #[test] + fn accept_encoding_is_not_classified_as_hop_by_hop_passthrough_skip() { + assert!(!should_skip_upstream_passthrough_header("accept-encoding")); + assert!(!should_skip_upstream_complete_passthrough_header( + "accept-encoding" + )); + } + + #[test] + fn normalizes_accept_encoding_to_supported_upstream_codecs() { + assert_eq!( + normalize_upstream_accept_encoding("gzip, br").as_deref(), + Some("gzip") + ); + assert_eq!( + normalize_upstream_accept_encoding("br, deflate").as_deref(), + Some("deflate") + ); + assert_eq!( + normalize_upstream_accept_encoding("identity").as_deref(), + Some("identity") + ); + assert_eq!( + normalize_upstream_accept_encoding("gzip;q=0.5, br").as_deref(), + Some("gzip;q=0.5") + ); + assert_eq!( + normalize_upstream_accept_encoding("gzip;q=0, br, deflate").as_deref(), + Some("deflate") + ); + assert_eq!( + normalize_upstream_accept_encoding("gzip;q=0, br").as_deref(), + None + ); + assert_eq!( + normalize_upstream_accept_encoding("*").as_deref(), + Some("gzip") + ); + assert_eq!(normalize_upstream_accept_encoding("br"), None); + } + + #[test] + fn force_identity_accept_encoding_replaces_existing_casing() { + let mut headers = BTreeMap::from([("Accept-Encoding".to_string(), "gzip".to_string())]); + + force_identity_accept_encoding(&mut headers); + + assert_eq!( + headers.get("accept-encoding").map(String::as_str), + Some("identity") + ); + assert!(!headers.contains_key("Accept-Encoding")); + } + #[test] fn strips_anthropic_and_claude_cli_identity_headers() { let anthropic = [ diff --git a/crates/aether-provider-transport/src/lib.rs b/crates/aether-provider-transport/src/lib.rs index f8a9d081a..79a60d527 100644 --- a/crates/aether-provider-transport/src/lib.rs +++ b/crates/aether-provider-transport/src/lib.rs @@ -112,14 +112,16 @@ pub use rules::{ }; pub use same_format_provider::{ build_same_format_provider_headers, build_same_format_provider_request_body, + build_same_format_provider_request_body_with_compatibility_report, build_same_format_provider_upstream_url, classify_same_format_provider_request_behavior, resolve_same_format_provider_direct_auth, same_format_provider_transport_supported, same_format_provider_transport_unsupported_reason, same_format_provider_transport_unsupported_reason_for_trace, - should_try_same_format_provider_oauth_auth, SameFormatProviderFamily, + should_try_same_format_provider_oauth_auth, SameFormatProviderCompatibilityEdit, + SameFormatProviderCompatibilityEditAction, SameFormatProviderFamily, SameFormatProviderHeadersInput, SameFormatProviderRequestBehavior, SameFormatProviderRequestBehaviorParams, SameFormatProviderRequestBodyInput, - SameFormatProviderUpstreamUrlParams, + SameFormatProviderRequestBodyOutput, SameFormatProviderUpstreamUrlParams, }; pub use snapshot::{ read_provider_transport_snapshot, GatewayProviderTransportSnapshot, diff --git a/crates/aether-provider-transport/src/same_format_provider/mod.rs b/crates/aether-provider-transport/src/same_format_provider/mod.rs index fcffc2dd0..5faaa9032 100644 --- a/crates/aether-provider-transport/src/same_format_provider/mod.rs +++ b/crates/aether-provider-transport/src/same_format_provider/mod.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; +use serde::Serialize; use serde_json::Value; use crate::antigravity::is_antigravity_provider_transport; @@ -75,6 +76,29 @@ pub struct SameFormatProviderRequestBodyInput<'a> { pub enable_model_directives: bool, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct SameFormatProviderRequestBodyOutput { + pub body: Value, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub compatibility_edits: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct SameFormatProviderCompatibilityEdit { + pub field: String, + pub action: SameFormatProviderCompatibilityEditAction, + pub detail: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum SameFormatProviderCompatibilityEditAction { + RuntimeRewrite, + ProviderCompatibilityRewrite, + ProviderEnvelope, + OperatorRule, +} + #[derive(Debug, Clone, Copy)] pub struct SameFormatProviderUpstreamUrlParams<'a> { pub provider_api_format: &'a str, @@ -158,15 +182,41 @@ pub fn classify_same_format_provider_request_behavior( pub fn build_same_format_provider_request_body( input: SameFormatProviderRequestBodyInput<'_>, +) -> Option { + build_same_format_provider_request_body_inner(input, None) +} + +pub fn build_same_format_provider_request_body_with_compatibility_report( + input: SameFormatProviderRequestBodyInput<'_>, +) -> Option { + let mut compatibility_edits = Vec::new(); + let body = + build_same_format_provider_request_body_inner(input, Some(&mut compatibility_edits))?; + Some(SameFormatProviderRequestBodyOutput { + body, + compatibility_edits, + }) +} + +fn build_same_format_provider_request_body_inner( + input: SameFormatProviderRequestBodyInput<'_>, + mut compatibility_edits: Option<&mut Vec>, ) -> Option { if let Some(kiro_auth_config) = input.kiro_auth_config { - return build_kiro_provider_request_body( + let body = build_kiro_provider_request_body( input.body_json, input.mapped_model, kiro_auth_config, input.body_rules, input.request_headers, + )?; + record_compatibility_edit( + &mut compatibility_edits, + "$", + SameFormatProviderCompatibilityEditAction::ProviderEnvelope, + "wrapped same-format request in Kiro provider envelope", ); + return Some(body); } if embedding_multimodal_input_requires_aliyun_provider( @@ -188,41 +238,82 @@ pub fn build_same_format_provider_request_body( .map(|(key, value)| (key.clone(), value.clone())), ) } else { - aether_ai_formats::convert_request( + aether_ai_formats::convert_request_pure( input.client_api_format, input.provider_api_format, input.body_json, - &aether_ai_formats::FormatContext::default().with_mapped_model(input.mapped_model), ) .ok()? + .value .as_object()? .clone() }; match input.family { SameFormatProviderFamily::Standard => { + let previous_model = provider_request_body.get("model").cloned(); provider_request_body.insert( "model".to_string(), Value::String(input.mapped_model.to_string()), ); + if previous_model != Some(Value::String(input.mapped_model.to_string())) { + record_compatibility_edit( + &mut compatibility_edits, + "model", + SameFormatProviderCompatibilityEditAction::RuntimeRewrite, + "rewrote request model to mapped upstream model", + ); + } } SameFormatProviderFamily::Gemini => { - provider_request_body.remove("model"); + if provider_request_body.remove("model").is_some() { + record_compatibility_edit( + &mut compatibility_edits, + "model", + SameFormatProviderCompatibilityEditAction::RuntimeRewrite, + "removed top-level model because Gemini model is carried by the upstream URL", + ); + } } } let mut provider_request_body = Value::Object(provider_request_body); if input.is_claude_code { + let before = compatibility_edits + .is_some() + .then(|| provider_request_body.clone()); crate::claude_code::sanitize_claude_code_request_body(&mut provider_request_body); + if before.is_some_and(|before| before != provider_request_body) { + record_compatibility_edit( + &mut compatibility_edits, + "body", + SameFormatProviderCompatibilityEditAction::ProviderCompatibilityRewrite, + "sanitized Claude Code request body for provider compatibility", + ); + } } if input.enable_model_directives { if let Some(source_model) = input.source_model { + let before = compatibility_edits + .is_some() + .then(|| provider_request_body.clone()); aether_ai_formats::apply_model_directive_overrides_from_model( &mut provider_request_body, input.provider_api_format, input.mapped_model, source_model, ); + if before.is_some_and(|before| before != provider_request_body) { + record_compatibility_edit( + &mut compatibility_edits, + "model_directives", + SameFormatProviderCompatibilityEditAction::RuntimeRewrite, + "applied model directive request body overrides", + ); + } } } + let before_body_rules = compatibility_edits + .is_some() + .then(|| provider_request_body.clone()); if !apply_local_body_rules_with_request_headers( &mut provider_request_body, input.body_rules, @@ -231,28 +322,73 @@ pub fn build_same_format_provider_request_body( ) { return None; } + if before_body_rules.is_some_and(|before| before != provider_request_body) { + record_compatibility_edit( + &mut compatibility_edits, + "body_rules", + SameFormatProviderCompatibilityEditAction::OperatorRule, + "applied configured provider body rules", + ); + } if matches!(input.family, SameFormatProviderFamily::Gemini) && aether_ai_formats::api_format_alias_matches( input.provider_api_format, "gemini:generate_content", ) { - strip_gemini_function_response_ids(&mut provider_request_body); + let stripped = strip_gemini_function_response_ids(&mut provider_request_body); + if stripped > 0 { + record_compatibility_edit( + &mut compatibility_edits, + "contents[].parts[].functionResponse.id", + SameFormatProviderCompatibilityEditAction::ProviderCompatibilityRewrite, + format!( + "stripped {stripped} Gemini functionResponse id field(s) rejected by upstreams" + ), + ); + } } let require_body_stream_field = input.force_body_stream_field || input .body_json .as_object() .is_some_and(|object| object.contains_key("stream")); + let previous_stream = provider_request_body.get("stream").cloned(); aether_ai_formats::enforce_request_body_stream_field( &mut provider_request_body, input.provider_api_format, input.upstream_is_stream, require_body_stream_field, ); + if previous_stream != provider_request_body.get("stream").cloned() { + record_compatibility_edit( + &mut compatibility_edits, + "stream", + SameFormatProviderCompatibilityEditAction::RuntimeRewrite, + format!( + "enforced provider stream policy with upstream_is_stream={}", + input.upstream_is_stream + ), + ); + } Some(provider_request_body) } +fn record_compatibility_edit( + edits: &mut Option<&mut Vec>, + field: impl Into, + action: SameFormatProviderCompatibilityEditAction, + detail: impl Into, +) { + if let Some(edits) = edits.as_mut() { + edits.push(SameFormatProviderCompatibilityEdit { + field: field.into(), + action, + detail: detail.into(), + }); + } +} + fn embedding_multimodal_input_requires_aliyun_provider( client_api_format: &str, provider_api_format: &str, @@ -278,25 +414,31 @@ fn embedding_content_is_multimodal(value: &Value) -> bool { }) } -fn strip_gemini_function_response_ids(value: &mut Value) { +fn strip_gemini_function_response_ids(value: &mut Value) -> usize { match value { Value::Object(object) => { + let mut stripped = 0; for key in ["functionResponse", "function_response"] { if let Some(function_response) = object.get_mut(key).and_then(Value::as_object_mut) { - function_response.remove("id"); + if function_response.remove("id").is_some() { + stripped += 1; + } } } for child in object.values_mut() { - strip_gemini_function_response_ids(child); + stripped += strip_gemini_function_response_ids(child); } + stripped } Value::Array(items) => { + let mut stripped = 0; for item in items { - strip_gemini_function_response_ids(item); + stripped += strip_gemini_function_response_ids(item); } + stripped } - _ => {} + _ => 0, } } @@ -882,6 +1024,61 @@ mod tests { assert_eq!(body.get("stream"), Some(&json!(true))); } + #[test] + fn same_format_standard_body_preserves_fields_that_cross_format_would_block() { + let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput { + body_json: &json!({ + "model": "client-model", + "messages": [{"role": "user", "content": "hello"}], + "n": 2, + "reasoning_effort": "max", + "unknown_vendor_field": {"keep": true} + }), + mapped_model: "client-model", + client_api_format: "openai:chat", + provider_api_format: "/v1/chat/completions", + source_model: Some("client-model"), + family: SameFormatProviderFamily::Standard, + body_rules: None, + request_headers: None, + upstream_is_stream: false, + force_body_stream_field: false, + kiro_auth_config: None, + is_claude_code: false, + enable_model_directives: false, + }) + .expect("same-format body should bypass canonical conversion"); + + assert_eq!(body["n"], 2); + assert_eq!(body["reasoning_effort"], "max"); + assert_eq!(body["unknown_vendor_field"]["keep"], true); + } + + #[test] + fn cross_format_standard_body_fails_closed_for_lossy_chat_fields() { + let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput { + body_json: &json!({ + "model": "client-model", + "messages": [{"role": "user", "content": "hello"}], + "n": 2 + }), + mapped_model: "upstream-model", + client_api_format: "openai:chat", + provider_api_format: "openai:responses", + source_model: Some("client-model"), + family: SameFormatProviderFamily::Standard, + body_rules: None, + request_headers: None, + upstream_is_stream: false, + force_body_stream_field: false, + kiro_auth_config: None, + is_claude_code: false, + enable_model_directives: false, + }); + + assert!(body.is_none()); + } + #[test] fn same_format_embedding_body_rejects_multimodal_for_openai_like_provider() { let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput { @@ -1073,6 +1270,77 @@ mod tests { assert_eq!(function_response["name"], "lookup_snake"); } + #[test] + fn same_format_body_report_records_provider_compatibility_edits() { + let output = build_same_format_provider_request_body_with_compatibility_report( + SameFormatProviderRequestBodyInput { + body_json: &json!({ + "model": "client-model", + "contents": [ + { + "role": "user", + "parts": [ + { + "functionResponse": { + "id": "call_123", + "name": "lookup", + "response": {"ok": true} + } + }, + { + "function_response": { + "id": "call_456", + "name": "lookup_snake", + "response": {"ok": true} + } + } + ] + } + ], + "stream": true + }), + mapped_model: "gemini-upstream", + client_api_format: "gemini:generate_content", + provider_api_format: "gemini:generate_content", + source_model: Some("client-model"), + family: SameFormatProviderFamily::Gemini, + body_rules: None, + request_headers: None, + upstream_is_stream: false, + force_body_stream_field: false, + kiro_auth_config: None, + is_claude_code: false, + enable_model_directives: false, + }, + ) + .expect("same-format body should build"); + + assert!(output.body.get("model").is_none()); + assert!(output.body.get("stream").is_none()); + assert!(output + .body + .pointer("/contents/0/parts/0/functionResponse/id") + .is_none()); + assert!(output + .body + .pointer("/contents/0/parts/1/function_response/id") + .is_none()); + assert!(output.compatibility_edits.iter().any(|edit| { + edit.field == "model" + && edit.action == SameFormatProviderCompatibilityEditAction::RuntimeRewrite + })); + assert!(output.compatibility_edits.iter().any(|edit| { + edit.field == "stream" + && edit.action == SameFormatProviderCompatibilityEditAction::RuntimeRewrite + })); + assert!(output.compatibility_edits.iter().any(|edit| { + edit.field == "contents[].parts[].functionResponse.id" + && edit.action + == SameFormatProviderCompatibilityEditAction::ProviderCompatibilityRewrite + && edit.detail.contains("2") + })); + } + #[test] fn same_format_stream_policy_wins_after_body_rules() { let body_rules = json!([ diff --git a/crates/aether-provider-transport/src/standard/mod.rs b/crates/aether-provider-transport/src/standard/mod.rs index 6ec4c15b2..27a0c1518 100644 --- a/crates/aether-provider-transport/src/standard/mod.rs +++ b/crates/aether-provider-transport/src/standard/mod.rs @@ -6,6 +6,7 @@ use crate::auth::{ build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth, build_openai_passthrough_headers, build_passthrough_headers, ensure_upstream_auth_header, }; +use crate::headers::force_identity_accept_encoding; use crate::rules::{ apply_local_body_rules, apply_local_body_rules_with_request_headers, apply_local_header_rules_with_request_headers, @@ -146,6 +147,10 @@ pub fn build_standard_plan_fallback_headers( } } + if input.upstream_is_stream { + force_identity_accept_encoding(&mut headers); + } + headers } @@ -272,6 +277,7 @@ pub fn build_standard_provider_request_headers( headers .entry("accept".to_string()) .or_insert_with(|| "text/event-stream".to_string()); + force_identity_accept_encoding(&mut headers); } Some(StandardProviderRequestHeaders { @@ -360,6 +366,10 @@ mod tests { fn builds_same_format_headers_with_complete_passthrough_and_stream_accept() { let mut request_headers = HeaderMap::new(); request_headers.insert("x-client", "demo".parse().expect("header")); + request_headers.insert( + http::header::ACCEPT_ENCODING, + "gzip, br".parse().expect("header"), + ); let transport = sample_transport("openai:chat"); let resolved = build_standard_provider_request_headers(StandardProviderRequestHeadersInput { @@ -387,9 +397,43 @@ mod tests { resolved.headers.get("accept"), Some(&"text/event-stream".to_string()) ); + assert_eq!( + resolved.headers.get("accept-encoding"), + Some(&"identity".to_string()) + ); assert_eq!(resolved.headers.get("x-client"), Some(&"demo".to_string())); } + #[test] + fn builds_sync_headers_preserving_supported_accept_encoding() { + let mut request_headers = HeaderMap::new(); + request_headers.insert( + http::header::ACCEPT_ENCODING, + "gzip, br".parse().expect("header"), + ); + let transport = sample_transport("openai:chat"); + let resolved = + build_standard_provider_request_headers(StandardProviderRequestHeadersInput { + transport: &transport, + provider_api_format: "openai:chat", + same_format: true, + headers: &request_headers, + auth_header: "authorization", + auth_value: "Bearer secret", + extra_headers: &BTreeMap::new(), + header_rules: None, + provider_request_body: &json!({"model":"gpt-5"}), + original_request_body: &json!({"model":"gpt-5"}), + upstream_is_stream: false, + }) + .expect("headers should build"); + + assert_eq!( + resolved.headers.get("accept-encoding"), + Some(&"gzip".to_string()) + ); + } + #[test] fn applies_header_rules_after_base_headers() { let transport = sample_transport("claude:messages"); @@ -473,6 +517,10 @@ mod tests { fn stream_fallback_headers_treat_wildcard_accept_as_absent() { let mut request_headers = HeaderMap::new(); request_headers.insert(http::header::ACCEPT, "*/*".parse().expect("header")); + request_headers.insert( + http::header::ACCEPT_ENCODING, + "gzip, br".parse().expect("header"), + ); let headers = build_standard_plan_fallback_headers(StandardPlanFallbackHeadersInput { request_headers: &request_headers, @@ -492,6 +540,10 @@ mod tests { headers.get("accept"), Some(&"text/event-stream".to_string()) ); + assert_eq!( + headers.get("accept-encoding"), + Some(&"identity".to_string()) + ); } #[test] diff --git a/crates/aether-usage-runtime/src/request_metadata.rs b/crates/aether-usage-runtime/src/request_metadata.rs index eb60f906d..554572be3 100644 --- a/crates/aether-usage-runtime/src/request_metadata.rs +++ b/crates/aether-usage-runtime/src/request_metadata.rs @@ -123,6 +123,7 @@ fn copy_allowed_metadata_fields(source: &Map, target: &mut Map, target: &mut Map remove_number(&mut source, target, "provider_request_body_base64_bytes"); remove_number(&mut source, target, "provider_response_body_base64_bytes"); remove_number(&mut source, target, "client_response_body_base64_bytes"); + remove_non_null_value(&mut source, target, "body_size"); remove_number(&mut source, target, "client_response_status_code"); remove_non_null_value(&mut source, target, "billing_snapshot"); remove_non_empty_string(&mut source, target, "billing_snapshot_schema_version"); @@ -491,6 +493,11 @@ mod tests { "provider_request_body_base64_bytes": 512, "provider_response_body_base64_bytes": 1024, "client_response_body_base64_bytes": 2048, + "body_size": { + "client_request_body": "1 KB", + "provider_request_body": "4 KB", + "provider_over_client": "4x" + }, "billing_snapshot": {"status": "complete"}, "billing_snapshot_schema_version": "2.0", "billing_snapshot_status": "complete", @@ -524,6 +531,11 @@ mod tests { "provider_request_body_base64_bytes": 512, "provider_response_body_base64_bytes": 1024, "client_response_body_base64_bytes": 2048, + "body_size": { + "client_request_body": "1 KB", + "provider_request_body": "4 KB", + "provider_over_client": "4x" + }, "billing_snapshot": {"status": "complete"}, "billing_snapshot_schema_version": "2.0", "billing_snapshot_status": "complete", diff --git a/crates/aether-usage-runtime/src/write.rs b/crates/aether-usage-runtime/src/write.rs index 3fbc2a37b..875e76f1b 100644 --- a/crates/aether-usage-runtime/src/write.rs +++ b/crates/aether-usage-runtime/src/write.rs @@ -720,8 +720,11 @@ pub fn build_terminal_usage_context_seed( }, body_states: request_capture.body_states, request_metadata: merge_usage_request_metadata_owned( - build_usage_request_metadata_seed(plan, context), - build_plan_body_capture_metadata(plan.body.body_bytes_b64.as_deref()), + merge_usage_request_metadata_owned( + build_usage_request_metadata_seed(plan, context), + build_plan_body_capture_metadata(plan.body.body_bytes_b64.as_deref()), + ), + build_runtime_body_size_request_metadata(plan, context), ), } } @@ -1561,6 +1564,7 @@ fn build_usage_event_data_seed_with_detail( .and_then(infer_endpoint_kind) .map(ToOwned::to_owned); let request_metadata = build_runtime_request_metadata_seed_from_parts( + plan, context, request_capture.request_body.is_some(), request_capture.request_body_ref.as_deref(), @@ -1859,6 +1863,7 @@ fn build_runtime_request_metadata_seed( context_has_inline_body(context, "provider_request_body") || plan_has_inline_json_body_for_usage(plan); let mut metadata = build_runtime_request_metadata_seed_from_parts( + plan, context, request_has_inline_body, request_body_ref.as_deref(), @@ -1889,6 +1894,7 @@ fn build_runtime_request_metadata_seed( } fn build_runtime_request_metadata_seed_from_parts( + plan: &ExecutionPlan, context: Option<&Map>, request_has_inline_body: bool, request_body_ref: Option<&str>, @@ -1943,10 +1949,128 @@ fn build_runtime_request_metadata_seed_from_parts( &mut metadata, provider_request_body_base64, ); + if let Some(body_size) = build_runtime_body_size_metadata(plan, context) { + metadata.insert("body_size".to_string(), body_size); + } (!metadata.is_empty()).then_some(Value::Object(metadata)) } +fn build_runtime_body_size_metadata( + plan: &ExecutionPlan, + context: Option<&Map>, +) -> Option { + let client_body_bytes = + context_value_ref(context, "original_request_body").and_then(captured_body_size_bytes); + let provider_body_bytes = plan_body_size_bytes(plan); + if client_body_bytes.is_none() && provider_body_bytes.is_none() { + return None; + } + + let mut metadata = Map::new(); + if let Some(bytes) = client_body_bytes { + metadata.insert( + "client_request_body".to_string(), + Value::String(format_data_size(bytes)), + ); + } + if let Some(bytes) = provider_body_bytes { + metadata.insert( + "provider_request_body".to_string(), + Value::String(format_data_size(bytes)), + ); + } + if let (Some(client_bytes), Some(provider_bytes)) = (client_body_bytes, provider_body_bytes) { + if let Some(ratio) = format_size_ratio(provider_bytes, client_bytes) { + metadata.insert("provider_over_client".to_string(), Value::String(ratio)); + } + } + metadata.insert( + "basis".to_string(), + Value::String("serialized gateway request bodies after normalization".to_string()), + ); + + Some(Value::Object(metadata)) +} + +fn build_runtime_body_size_request_metadata( + plan: &ExecutionPlan, + context: Option<&Map>, +) -> Option { + let body_size = build_runtime_body_size_metadata(plan, context)?; + let mut metadata = Map::new(); + metadata.insert("body_size".to_string(), body_size); + Some(Value::Object(metadata)) +} + +fn captured_body_size_bytes(value: &Value) -> Option { + if value.is_null() { + return None; + } + if let Some(body_base64) = value + .as_object() + .and_then(|object| object.get("body_bytes_b64")) + .and_then(Value::as_str) + { + return decoded_base64_len_hint(body_base64); + } + json_serialized_len(value) +} + +fn plan_body_size_bytes(plan: &ExecutionPlan) -> Option { + if let Some(body_base64) = plan.body.body_bytes_b64.as_deref() { + return decoded_base64_len_hint(body_base64); + } + plan.body.json_body.as_ref().and_then(json_serialized_len) +} + +fn json_serialized_len(value: &Value) -> Option { + serde_json::to_vec(value) + .ok() + .and_then(|bytes| u64::try_from(bytes.len()).ok()) +} + +fn format_data_size(bytes: u64) -> String { + const KIB: f64 = 1024.0; + const MIB: f64 = KIB * 1024.0; + const GIB: f64 = MIB * 1024.0; + + let bytes = bytes as f64; + let (value, unit) = if bytes >= GIB { + (bytes / GIB, "GB") + } else if bytes >= MIB { + (bytes / MIB, "MB") + } else { + (bytes / KIB, "KB") + }; + format!("{} {}", format_compact_decimal(value), unit) +} + +fn format_size_ratio(numerator: u64, denominator: u64) -> Option { + if denominator == 0 { + return None; + } + Some(format!( + "{}x", + format_compact_decimal(numerator as f64 / denominator as f64) + )) +} + +fn format_compact_decimal(value: f64) -> String { + let digits = if value >= 100.0 { + 0 + } else if value >= 10.0 { + 1 + } else { + 2 + }; + let formatted = format!("{value:.digits$}"); + formatted + .trim_end_matches('0') + .trim_end_matches('.') + .to_string() +} + fn capture_usage_storage_value(value: Value) -> Value { if usage_capture_within_limits(&value) { return value; @@ -3354,7 +3478,25 @@ mod tests { record.local_execution_runtime_miss_reason.as_deref(), Some("all_candidates_skipped") ); - assert_eq!(record.request_metadata, None); + let metadata = record + .request_metadata + .as_ref() + .and_then(Value::as_object) + .expect("pending usage should only keep lightweight request metadata"); + assert_eq!(metadata.len(), 1); + let body_size = metadata + .get("body_size") + .and_then(Value::as_object) + .expect("pending usage should keep request body size metadata"); + assert_eq!( + body_size.get("basis"), + Some(&json!( + "serialized gateway request bodies after normalization" + )) + ); + assert!(body_size.get("client_request_body").is_some()); + assert!(body_size.get("provider_request_body").is_some()); + assert!(body_size.get("provider_over_client").is_some()); } #[test] @@ -3392,14 +3534,27 @@ mod tests { ) .expect("pending usage should build"); + let metadata = record + .request_metadata + .as_ref() + .and_then(Value::as_object) + .expect("pending usage should keep request metadata"); + assert_eq!(metadata.get("api_key_is_standalone"), Some(&json!(true))); + assert_eq!(metadata.get("client_ip"), Some(&json!("203.0.113.8"))); + assert_eq!(metadata.get("user_agent"), Some(&json!("Claude-Code/1.0"))); + let body_size = metadata + .get("body_size") + .and_then(Value::as_object) + .expect("pending usage should keep request body size metadata"); assert_eq!( - record.request_metadata, - Some(json!({ - "api_key_is_standalone": true, - "client_ip": "203.0.113.8", - "user_agent": "Claude-Code/1.0" - })) + body_size.get("basis"), + Some(&json!( + "serialized gateway request bodies after normalization" + )) ); + assert!(body_size.get("provider_request_body").is_some()); + assert!(body_size.get("client_request_body").is_none()); + assert!(body_size.get("provider_over_client").is_none()); } #[test] @@ -5380,6 +5535,95 @@ mod tests { .is_none_or(|value| { value.get("provider_request_body_ref").is_none() })); } + #[test] + fn sync_terminal_usage_records_human_readable_request_body_sizes_in_metadata() { + let client_body_bytes = vec![b'c'; 1024]; + let provider_body_bytes = vec![b'p'; 4096]; + let client_body_base64 = + base64::engine::general_purpose::STANDARD.encode(client_body_bytes); + let provider_body_base64 = + base64::engine::general_purpose::STANDARD.encode(provider_body_bytes); + let plan = ExecutionPlan { + request_id: "req-sync-body-size-1".to_string(), + candidate_id: Some("cand-sync-body-size-1".to_string()), + provider_name: Some("OpenAI".to_string()), + provider_id: "provider-1".to_string(), + endpoint_id: "endpoint-1".to_string(), + key_id: "key-1".to_string(), + method: "POST".to_string(), + url: "https://example.com/v1/responses".to_string(), + headers: BTreeMap::new(), + content_type: Some("application/json".to_string()), + content_encoding: None, + body: RequestBody { + json_body: None, + body_bytes_b64: Some(provider_body_base64), + body_ref: None, + }, + stream: false, + client_api_format: "openai:chat".to_string(), + provider_api_format: "openai:responses".to_string(), + model_name: Some("gpt-5.4".to_string()), + proxy: None, + transport_profile: None, + timeouts: None, + }; + let payload = GatewaySyncReportRequest { + trace_id: "trace-sync-body-size-1".to_string(), + report_kind: "openai_chat_sync_success".to_string(), + report_context: Some(json!({ + "client_api_format": "openai:chat", + "provider_api_format": "openai:responses", + "needs_conversion": true, + "original_request_body": { + "body_bytes_b64": client_body_base64 + } + })), + status_code: 200, + headers: BTreeMap::new(), + body_json: Some(json!({"id": "resp_123"})), + client_body_json: None, + body_base64: None, + telemetry: None, + }; + + let context_seed = + build_terminal_usage_context_seed(&plan, payload.report_context.as_ref()); + assert_request_body_size_metadata( + context_seed.request_metadata.as_ref(), + "context seed should include body size metadata", + ); + + let payload_seed = build_sync_terminal_usage_payload_seed(&payload); + let seed_event = build_terminal_usage_event_from_seed(build_sync_terminal_usage_seed( + context_seed, + payload_seed, + )) + .expect("seed usage event should build"); + assert_request_body_size_metadata( + seed_event.data.request_metadata.as_ref(), + "seed event should include body size metadata", + ); + + let wrapper_event = + build_sync_terminal_usage_event(&plan, payload.report_context.as_ref(), &payload) + .expect("wrapper usage event should build"); + assert_request_body_size_metadata( + wrapper_event.data.request_metadata.as_ref(), + "wrapper event should include body size metadata", + ); + } + + fn assert_request_body_size_metadata(metadata: Option<&Value>, message: &str) { + let body_size = metadata + .and_then(|metadata| metadata.get("body_size")) + .unwrap_or_else(|| panic!("{message}: {metadata:?}")); + + assert_eq!(body_size.get("client_request_body"), Some(&json!("1 KB"))); + assert_eq!(body_size.get("provider_request_body"), Some(&json!("4 KB"))); + assert_eq!(body_size.get("provider_over_client"), Some(&json!("4x"))); + } + #[test] fn stream_terminal_usage_records_base64_response_sizes_in_metadata() { let provider_bytes = @@ -5591,6 +5835,16 @@ mod tests { "input": [{"role": "user", "content": "provider-side compiled body"}], })) ); + let body_size = event + .data + .request_metadata + .as_ref() + .and_then(|metadata| metadata.get("body_size")) + .and_then(Value::as_object) + .expect("provider body size metadata should exist"); + assert!(body_size.get("provider_request_body").is_some()); + assert!(body_size.get("client_request_body").is_none()); + assert!(body_size.get("provider_over_client").is_none()); } #[test] diff --git a/docs/api/format-conversion-audit.md b/docs/api/format-conversion-audit.md new file mode 100644 index 000000000..c279d4f1f --- /dev/null +++ b/docs/api/format-conversion-audit.md @@ -0,0 +1,236 @@ +# Format Conversion Audit + +Last audited: 2026-06-03 + +This audit tracks `source format -> Canonical -> target format` behavior. It is intentionally stricter than historical best-effort conversion. + +Statuses: + +- `native`: emitted as a target-native field without semantic change. +- `mapped`: converted through canonical/provider-specific mapping. +- `extension-preserved`: preserved in same-format canonical roundtrip or target-approved extension namespace. +- `unaudited`: rejected because the source field is not in the audited provider schema inventory for cross-format conversion. +- `unsupported`: rejected because the request/field shape is outside the supported conversion surface, independent of schema drift. +- `lossy-blocked`: conversion fails closed. +- `invalid-enum`: conversion fails closed because a provider enum value is not valid for the target mapping. + +Full schema field coverage is tracked in `docs/api/format-field-coverage-matrix.md`. That matrix is generated from the schema inventory in `docs/api/provider-interface-definitions.md` by `python3 docs/api/generate_format_field_coverage.py` and gives every documented OpenAI, Claude, and Gemini schema field a handling status. “Handled” means mapped, same-format/native preserved, extension-preserved, blocked with a structured error, or explicitly marked outside the canonical conversion surface. + +Provider schema refresh is not a runtime dependency. Same-format runtime paths do not use this matrix; they bypass canonical conversion. Same-format canonical roundtrip must preserve unrecognized provider fields through provider extension namespaces. Cross-format conversion is capability-based: only explicitly mapped fields are emitted, and newly discovered or unknown provider fields fail closed with `UnauditedField` until a lossless mapping is audited. + +## Implemented Boundary Changes + +| Area | Current behavior | +| --- | --- | +| Pure conversion API | `convert_request_pure` and response equivalents do not apply model override or stream policy. | +| Legacy conversion API | `convert_request` / `convert_response` are retained for migration and may still use legacy context behavior. | +| Same-format provider path | Bypasses canonical conversion and copies the parsed JSON object before transport edits. | +| Cross-format same-format-provider path | Uses `convert_request_pure`, then applies model/body/stream edits in transport. | +| Conversion errors | Added `UnauditedField`, `UnsupportedField`, `InvalidEnumValue`, `LossyConversionBlocked`, and `InvalidTargetField`. | +| Reporting | Added `ConversionReport` with field statuses. Runtime reports remain conversion-operation oriented; exhaustive nested schema coverage is enforced by `format-field-coverage-matrix.md`. | +| Source schema coverage | Cross-format request conversion rejects unknown source root fields before emit. Every documented schema field is covered by the field coverage matrix. | +| Schema drift handling | Official schema changes are detected by regenerating the inventory/matrix. Runtime same-format remains passthrough; cross-format unknowns return `UnauditedField` until deliberately mapped. | +| Tool schema roundtrip | Claude `input_schema` and Gemini `functionDeclarations.parameters` preserve raw same-format schema through provider-specific extensions. | +| Tool result ids | Chat `tool_call_id`, Responses `call_id`, Claude `tool_use_id`, and Gemini `functionResponse.id` are mapped through canonical tool IDs. | + +## OpenAI Chat -> OpenAI Responses + +| Chat field | Canonical handling | Responses output | Status | +| --- | --- | --- | --- | +| `model` | request identity | `model` | native | +| `messages` | canonical messages/instructions | `input`, `instructions` | mapped | +| `max_tokens` | generation max tokens | `max_output_tokens` | mapped | +| `max_completion_tokens` | generation max tokens | `max_output_tokens` | mapped | +| `temperature` | generation | `temperature` | native | +| `top_p` | generation | `top_p` | native | +| `top_logprobs` | generation | `top_logprobs` | native | +| `n` | generation but no Responses equivalent | none | lossy-blocked | +| `stop` | generation but no Responses equivalent | none | lossy-blocked | +| `presence_penalty` | generation but no Responses equivalent | none | lossy-blocked | +| `frequency_penalty` | generation but no Responses equivalent | none | lossy-blocked | +| `seed` | generation but no Responses equivalent | none | lossy-blocked | +| `logprobs` | generation but no Responses equivalent | none | lossy-blocked | +| `stream` | OpenAI extension | `stream` | mapped if explicit | +| `stream_options` | Chat-specific extension | none | lossy-blocked | +| `tools[].function.name` | canonical tool | `tools[].name` | mapped | +| `tools[].function.description` | canonical tool | `tools[].description` | mapped | +| `tools[].function.parameters` | canonical tool | `tools[].parameters` | mapped | +| `tools[].function.strict` | canonical tool strict | `tools[].strict` | mapped, implemented | +| assistant `tool_calls[].id` | canonical tool use id | `function_call.call_id` | mapped, implemented | +| tool message `tool_call_id` | canonical tool result id | `function_call_output.call_id` | mapped, implemented | +| `tool_choice` | canonical tool choice | `tool_choice` | mapped | +| `parallel_tool_calls` | canonical bool | `parallel_tool_calls` | native | +| `metadata` | canonical metadata | `metadata` | native | +| `response_format` | canonical response format | `text.format` | mapped | +| `reasoning_effort` | OpenAI enum | `reasoning.effort` | mapped; invalid enum blocked | +| `verbosity` | OpenAI extension | `text.verbosity` | mapped | +| `store` | OpenAI extension | `store` | extension-preserved | +| `service_tier` | OpenAI extension | `service_tier` | extension-preserved | +| `safety_identifier` | OpenAI extension | `safety_identifier` | extension-preserved | +| `prompt_cache_key` | OpenAI extension | `prompt_cache_key` | extension-preserved | +| `user` | legacy Chat user field | none | lossy-blocked | +| unknown top-level fields | source schema guard | none | unaudited | + +## OpenAI Responses -> OpenAI Chat + +| Responses field | Canonical handling | Chat output | Status | +| --- | --- | --- | --- | +| `model` | request identity | `model` | native | +| `input` | canonical messages/content/tool I/O | `messages` | mapped | +| `instructions` | canonical instruction/system | `messages` system/developer | mapped | +| `max_output_tokens` | generation max tokens | `max_completion_tokens` | mapped | +| `temperature` | generation | `temperature` | native | +| `top_p` | generation | `top_p` | native | +| `top_logprobs` | generation | `top_logprobs` | native | +| `metadata` | canonical metadata | `metadata` | native | +| `parallel_tool_calls` | canonical bool | `parallel_tool_calls` | native | +| `text.format` | canonical response format | `response_format` | mapped | +| `text.verbosity` | Responses extension | `verbosity` | mapped | +| `tools[].type=function` | canonical tool | `tools[].type=function` | mapped | +| `tools[].name` | canonical tool | `tools[].function.name` | mapped | +| `tools[].parameters` | canonical tool | `tools[].function.parameters` | mapped | +| `tools[].strict` | canonical tool strict | `tools[].function.strict` | mapped, implemented | +| `function_call.call_id` | canonical tool use id | `tool_calls[].id` | mapped, implemented | +| `function_call_output.call_id` | canonical tool result id | tool message `tool_call_id` | mapped, implemented | +| `tools[].type=custom` | raw Responses tool | none | lossy-blocked to Chat | +| `tools[].type=web_search*` | raw Responses tool | none | lossy-blocked to Chat | +| `tool_choice` | canonical tool choice | `tool_choice` | mapped | +| `reasoning.effort` | OpenAI enum | `reasoning_effort` | mapped; invalid enum blocked | +| `reasoning.summary` | Responses-only | none | lossy-blocked | +| `reasoning.budget_tokens` | Responses-only | none | lossy-blocked | +| `stream` | Responses request transport policy | none | lossy-blocked; target stream policy is transport-owned | +| `include` | Responses-only | none | lossy-blocked; legacy emitter no longer leaks | +| `previous_response_id` | Responses-only | none | lossy-blocked; legacy emitter no longer leaks | +| `truncation` | Responses-only | none | lossy-blocked | +| `prompt` | Responses-only | none | lossy-blocked | +| `conversation` | Responses-only | none | lossy-blocked | +| `background` | Responses-only | none | lossy-blocked | +| `max_tool_calls` | Responses-only | none | lossy-blocked | +| unknown top-level fields | source schema guard | none | unaudited | + +## Claude Messages <-> OpenAI Chat / Responses + +Claude to OpenAI Chat, Claude to OpenAI Responses, and the reverse directions are included in the field coverage matrix. Runtime strict guards cover request root fields, provider extension namespaces, thinking/cache/tool-result hazards, and target generation-field gaps. Fields without a lossless target equivalent fail closed instead of being dropped. + +High-risk fields: + +| Claude field | OpenAI target risk | Required status | +| --- | --- | --- | +| `system` with cache blocks | Chat/Responses system instructions | same-format preserved; cross-format `cache_control` loss is blocked | +| `thinking` | OpenAI reasoning | Claude request-level thinking config maps to OpenAI reasoning; message-level thinking blocks are blocked for Responses | +| `cache_control` | OpenAI content/tool extensions | same-format preserved; cross-format blocked when no target equivalent exists | +| `tools[].input_schema` | OpenAI tool parameters | mapped; raw same-format schema preservation implemented | +| `tool_choice.disable_parallel_tool_use` | OpenAI `parallel_tool_calls` | mapped, implemented | +| `tool_result` multi-block content | OpenAI tool output/content | same-format preserved; cross-format to Chat/Responses is lossy-blocked | +| `metadata` | OpenAI metadata | mapped when the target has metadata | +| `container`, `inference_geo`, `service_tier` | OpenAI target has no audited equivalent | lossy-blocked unless a target-approved mapping is added | + +## Gemini GenerateContent <-> OpenAI Chat / Responses / Claude + +Gemini to OpenAI Chat, Gemini to OpenAI Responses, Gemini to Claude, and reverse generation paths are included in the field coverage matrix. Gemini-only request fields are preserved same-format and blocked cross-format unless the target mapping is explicitly audited. + +High-risk fields: + +| Gemini field | Target risk | Required status | +| --- | --- | --- | +| `contents[].parts[].thoughtSignature` | OpenAI/Claude thinking | Chat/Claude preserve; Responses cross-format is lossy-blocked | +| `tools[].functionDeclarations` | OpenAI/Claude tool schema | mapped; raw same-format `parameters` preservation implemented | +| `toolConfig.functionCallingConfig.allowedFunctionNames` | OpenAI/Claude tool choice | single-name mapping implemented; multi-name input is lossy-blocked | +| `toolConfig.functionCallingConfig.mode` | OpenAI/Claude tool choice enum | valid enum required; invalid values fail with `InvalidEnumValue` | +| `generationConfig.thinkingConfig.thinkingLevel` | OpenAI/Claude reasoning effort | low/medium/high mapping implemented; invalid values fail closed | +| `safetySettings` | OpenAI/Claude no direct equivalent | lossy-blocked | +| `cachedContent` | OpenAI/Claude no direct equivalent | lossy-blocked | +| `codeExecution` | OpenAI/Claude tool/builtin mismatch | lossy-blocked | +| `generationConfig.responseModalities` | OpenAI/Claude modality mismatch | lossy-blocked | +| `functionResponse.id` | tool result id | conversion preserves id; Gemini upstream cleanup is transport-layer edit only | + +## Embedding And Rerank + +Embedding and rerank request parse/emit capability and strict target guards are implemented. Provider schema fields outside these canonical conversion surfaces are marked `not-in-conversion-surface` in the field coverage matrix instead of being left implicit. + +Embedding source capability: + +| Source format | Parsed request shape | Canonical fields | Status | +| --- | --- | --- | --- | +| OpenAI Embedding | `model`, `input`, `encoding_format`, `dimensions`, `user`, `parameters`, `task` | OpenAI-like embedding | mapped | +| Jina Embedding | OpenAI-like plus provider extension namespace | OpenAI-like embedding | mapped | +| Doubao Embedding | OpenAI-like `model` + text `input` | OpenAI-like embedding | mapped | +| Gemini Embedding | single `content.parts[].text` or batch `requests[]` | text input, `dimensions`, `task` | mapped | +| Aliyun Multimodal Embedding | `input.contents[]`, `parameters.dimension` | text/multimodal input, `dimensions`, `parameters` | mapped | + +Embedding target guards: + +| Target format | Accepted canonical fields | Blocked fields/cases | Status | +| --- | --- | --- | --- | +| OpenAI Embedding | text or token input, `encoding_format`, `dimensions`, `user` | multimodal input, `task`, generic `parameters` | lossy-blocked | +| Jina Embedding | text input, `dimensions`, `task`, `parameters` | token/multimodal input, `encoding_format`, `user` | lossy-blocked | +| Gemini Embedding | text input, `dimensions`, valid `taskType` | token/multimodal input, `encoding_format`, `user`, generic `parameters`, invalid `taskType` | lossy-blocked / invalid-enum | +| Doubao Embedding | text input, `dimensions` | token/multimodal input, `encoding_format`, `user`, `task`, generic `parameters` | lossy-blocked | +| Aliyun Multimodal Embedding | text or multimodal input, `dimensions`, `parameters` | token input, `encoding_format`, `user`, `task` | lossy-blocked | + +Cross-format embedding invariants: + +- Embedding formats can only convert to embedding formats. +- Unknown provider-specific embedding extension namespaces are blocked cross-format unless the namespace matches the target. +- Aliyun `parameters.dimension` maps to canonical `dimensions` and is not treated as generic `parameters`. +- Gemini batch embedding parse requires every batch item to share the same model, dimensions, and task. + +Rerank first pass: + +| Area | Current behavior | Status | +| --- | --- | --- | +| Source formats | OpenAI Rerank and Jina Rerank parse OpenAI-like `model`, `query`, `documents`, `top_n`, `return_documents` | mapped | +| Target formats | OpenAI Rerank and Jina Rerank emit OpenAI-like rerank bodies | mapped | +| Boundary | Rerank formats can only convert to rerank formats | lossy-blocked | +| Validation | Empty query/documents and `top_n=0` fail closed | invalid-target-field | +| Extensions | Unknown provider-specific rerank extension namespaces are blocked cross-format | unsupported | + +## Sync Response Conversion + +Cross-format sync response conversion now validates source stop/finish/status +enums before emitting a target body. Same-format runtime response passthrough is +still outside canonical conversion. + +| Source field | Target risk | Current behavior | Status | +| --- | --- | --- | --- | +| Same-format response raw stop/status fields | canonical emitters would otherwise normalize unknown enum/status to default target stop values | raw OpenAI Chat `finish_reason`, OpenAI Responses `status`, Claude `stop_reason`/`stop_sequence`, and Gemini `finishReason` are preserved through provider extension metadata | extension-preserved | +| OpenAI Chat `choices[].finish_reason` | unknown value would otherwise emit as target normal stop | valid Chat enum required; unknown values fail with `InvalidEnumValue` | invalid-enum | +| OpenAI Responses `status` | `queued`, `in_progress`, and `cancelled` have no sync target equivalent | non-terminal valid states fail with `LossyConversionBlocked`; invalid states fail with `InvalidEnumValue` | lossy-blocked / invalid-enum | +| OpenAI Responses `incomplete_details.reason=content_filter` | previously mapped to max tokens/`length` | maps to canonical content filter and emits Chat `content_filter` / Claude `content_filtered` / Gemini `SAFETY` | mapped | +| Claude `stop_reason` | unknown value would otherwise emit as target normal stop | valid known stop enum required for cross-format conversion | invalid-enum | +| Gemini `candidates[].finishReason` | known-but-unmappable reasons would otherwise emit as target normal stop | mappable safety/max/stop reasons convert; known unmappable values such as `OTHER`, `MALFORMED_FUNCTION_CALL`, `UNEXPECTED_TOOL_CALL`, `MISSING_THOUGHT_SIGNATURE`, and `MALFORMED_RESPONSE` fail with `LossyConversionBlocked`; future unknown values fail with `InvalidEnumValue` | lossy-blocked / invalid-enum | +| Canonical `Unknown` stop reason | target emitters default to normal stop values | cross-format response conversion blocks canonical unknown stop reasons | lossy-blocked | + +## Stream Conversion + +Sixth batch first pass is implemented for unknown event handling and runtime +same-format boundaries. Sync response finish/status parity has a first strict +pass; stream finish-reason guardrails are implemented for unknown/unmappable +terminal reasons. Stream event schema fields are covered in the field coverage +matrix; provider-by-provider fixtures cover the runtime event behavior. + +Current stream behavior: + +| Area | Current behavior | Status | +| --- | --- | --- | +| Provider parsers | OpenAI Chat, OpenAI Responses, Claude, and Gemini unknown stream payloads become `CanonicalStreamEvent::UnknownEvent` | mapped | +| Cross-format stream matrix | Unknown canonical stream events emit a target-format error SSE with `unsupported_stream_event` and terminate conversion | lossy-blocked | +| Stream finish reason guard | Unknown OpenAI finish reasons, unknown Claude `stop_reason`, and Gemini known-but-unmappable `finishReason` values such as `OTHER` are preserved as raw canonical finish strings, then blocked by the matrix with `unsupported_finish_reason` | lossy-blocked | +| OpenAI Responses stream target | Canonical `length` and `content_filter` terminal reasons emit `response.incomplete` with `incomplete_details.reason=max_output_tokens` or `content_filter` instead of `response.completed` | mapped | +| Terminal observer | Unknown provider stream events increment `unknown_event_count`; OpenAI Responses failed events mark terminal error state | mapped | +| Stream -> sync aggregate | Unknown OpenAI Chat, OpenAI Responses, Claude, and Gemini stream events make the runtime finalize checked path return an error and block `body_json` fallback; legacy public aggregate helpers keep `Option` compatibility | lossy-blocked | +| Runtime strict fallback guard | `UnauditedField`, `InvalidEnumValue`, `UnsupportedField`, `LossyConversionBlocked`, and `InvalidTargetField` from registry response conversion are not allowed to fall through legacy conversion helpers | lossy-blocked | +| Runtime same-format stream | Same-format stream passthrough remains outside canonical conversion; stream policy edits are transport-layer only | native | + +Stream fixture coverage: + +| Provider stream | Covered fixture areas | +| --- | --- | +| OpenAI Chat | sync aggregation for text, tool call IDs/names/argument deltas, finish reason, and usage; cross-format unknown finish/event blocking | +| OpenAI Responses | text snapshot de-duplication, multi-part messages, reasoning/items, function calls, image generation calls, same-family stream sync, unknown event blocking | +| Claude Messages | thinking signatures, tool input deltas, cache/usage aggregation, media emission, unknown stop/event blocking | +| Gemini GenerateContent | text/media/signature aggregation, function calls/results, safety finish mapping, unknown parts/events, and unmappable finish reason blocking | + +Matrix-level interception remains the authoritative runtime path for cross-format +unknown events; direct client emitters are covered only as provider/client +building blocks. diff --git a/docs/api/format-enum-mapping.md b/docs/api/format-enum-mapping.md new file mode 100644 index 000000000..1d493efab --- /dev/null +++ b/docs/api/format-enum-mapping.md @@ -0,0 +1,145 @@ +# Format Enum Mapping + +Last audited: 2026-06-03 + +Status values used below: + +- `native`: same semantic value exists in the target format. +- `mapped`: explicit provider-specific mapping is required. +- `blocked`: no lossless target value; conversion must fail closed. +- `preserve-same-format`: unknown/raw values are preserved only when source and target format are the same. + +## OpenAI Reasoning Effort + +Provider-specific types: `OpenAiChatReasoningEffort` for Chat `reasoning_effort`, and `OpenAiResponsesReasoningEffort` for Responses `reasoning.effort`. They are intentionally separate even when their current value sets overlap; a value accepted by one field is not treated as valid for the other unless that field's own enum accepts it. + +| Source field | Source value | Target field | Target value | Status | +| --- | --- | --- | --- | --- | +| Chat `reasoning_effort` | `none` | Responses `reasoning.effort` | `none` | native | +| Chat `reasoning_effort` | `minimal` | Responses `reasoning.effort` | `minimal` | native | +| Chat `reasoning_effort` | `low` | Responses `reasoning.effort` | `low` | native | +| Chat `reasoning_effort` | `medium` | Responses `reasoning.effort` | `medium` | native | +| Chat `reasoning_effort` | `high` | Responses `reasoning.effort` | `high` | native | +| Chat `reasoning_effort` | `xhigh` | Responses `reasoning.effort` | `xhigh` | native | +| Chat `reasoning_effort` | `max` | Responses `reasoning.effort` | none | blocked, invalid OpenAI enum | +| Responses `reasoning.effort` | `none` | Chat `reasoning_effort` | `none` | native | +| Responses `reasoning.effort` | `minimal` | Chat `reasoning_effort` | `minimal` | native | +| Responses `reasoning.effort` | `low` | Chat `reasoning_effort` | `low` | native | +| Responses `reasoning.effort` | `medium` | Chat `reasoning_effort` | `medium` | native | +| Responses `reasoning.effort` | `high` | Chat `reasoning_effort` | `high` | native | +| Responses `reasoning.effort` | `xhigh` | Chat `reasoning_effort` | `xhigh` | native | +| Responses `reasoning.effort` | `max` | Chat `reasoning_effort` | none | blocked, invalid Responses enum | +| Responses `reasoning.summary` | any | Chat | none | blocked | +| Responses `reasoning.budget_tokens` | any | Chat | none | blocked | + +Internal model directive values: + +| Internal value | OpenAI Chat | OpenAI Responses | Claude output effort | Gemini thinking level | Notes | +| --- | --- | --- | --- | --- | --- | +| `none` | `none` | `none` | `low` | `low` | Budget maps to `0`. | +| `minimal` | `minimal` | `minimal` | `low` | `low` | Budget maps to `512`. | +| `low` | `low` | `low` | `low` | `low` | Budget maps to `1280`. | +| `medium` | `medium` | `medium` | `medium` | `medium` | Budget maps to `2048`. | +| `high` | `high` | `high` | `high` | `high` | Budget maps to `4096`. | +| `xhigh` | `xhigh` | `xhigh` | `xhigh` | `high` | Budget maps to `8192`. | +| `max` | `xhigh` | `xhigh` | `max` | `high` | Internal directive only; not accepted as raw OpenAI input. | + +## Tool Choice + +| Canonical | OpenAI Chat | OpenAI Responses | Claude Messages | Gemini GenerateContent | +| --- | --- | --- | --- | --- | +| auto | `"auto"` | `"auto"` | `{"type":"auto"}` | unset / function calling config auto | +| none | `"none"` | `"none"` | `{"type":"none"}` | mode none | +| required | `"required"` | `"required"` | `{"type":"any"}` | mode any | +| named function | `{"type":"function","function":{"name":...}}` | `{"type":"function","name":...}` | `{"type":"tool","name":...}` | allowed function name | + +Implemented guardrails: + +- Claude `output_config.effort=max` maps to OpenAI `xhigh`; unknown Claude effort enums are blocked cross-format. +- Claude `tool_choice.disable_parallel_tool_use` maps inversely to OpenAI `parallel_tool_calls`. +- Gemini `allowedFunctionNames` maps to canonical named tool choice and emits back to `allowedFunctionNames`. +- Gemini `thinkingLevel` maps `low|medium|high` to OpenAI reasoning effort `low|medium|high`; unknown values are blocked cross-format. +- Responses `custom`, `web_search*`, and other built-in tools are blocked when converting to Chat unless a target raw passthrough is explicitly added. + +## Tool Definition Kind + +| Source | Target | Mapping | +| --- | --- | --- | +| OpenAI Chat `tools[].function.name` | Responses `tools[].name` | mapped | +| OpenAI Chat `tools[].function.parameters` | Responses `tools[].parameters` | mapped | +| OpenAI Chat `tools[].function.strict` | Responses `tools[].strict` | mapped, implemented | +| Responses `tools[].strict` | Chat `tools[].function.strict` | mapped, implemented | +| OpenAI Chat assistant `tool_calls[].id` | Responses `function_call.call_id` | mapped, implemented | +| OpenAI Chat tool `tool_call_id` | Responses `function_call_output.call_id` | mapped, implemented | +| Responses `function_call.call_id` | Chat `tool_calls[].id` | mapped, implemented | +| Responses `function_call_output.call_id` | Chat tool `tool_call_id` | mapped, implemented | +| Gemini `functionCall.id` | OpenAI/Claude canonical tool use id | mapped, implemented | +| Gemini `functionResponse.id` | OpenAI `tool_call_id` / Responses `call_id` / Claude `tool_use_id` | mapped, implemented | +| Claude `tools[].input_schema` | OpenAI `parameters` | mapped; raw same-format schema preserved | +| Gemini `functionDeclarations[].parameters` | OpenAI `parameters` | mapped; raw same-format schema preserved | + +## Roles + +| Canonical role | OpenAI Chat | OpenAI Responses | Claude Messages | Gemini | +| --- | --- | --- | --- | --- | +| system | `system` | `instructions` or system input item | top-level `system` | `systemInstruction` | +| developer | `developer` | `instructions` or developer input item | extension-preserved | systemInstruction extension | +| user | `user` | `message.role=user` | `user` | `user` | +| assistant | `assistant` | `message.role=assistant` / output item | `assistant` | `model` | +| tool | `tool` | `function_call_output` | `tool_result` inside user message | `functionResponse` | + +Known lossy risks: + +- Multiple system/developer instruction ordering needs full golden fixtures. +- Provider-specific role extensions must be preserved in same-format roundtrip and blocked cross-format if no target equivalent exists. + +## Finish Reasons + +| Canonical | OpenAI Chat | OpenAI Responses | Claude | Gemini | +| --- | --- | --- | --- | --- | +| stop | `stop` | completed output | `end_turn` | `STOP` | +| length | `length` | `status=incomplete`, `incomplete_details.reason=max_output_tokens` | `max_tokens` | `MAX_TOKENS` | +| tool calls | `tool_calls` | output contains `function_call` | `tool_use` | `functionCall` part, usually with `STOP` | +| content filter/safety | `content_filter` | `status=incomplete`, `incomplete_details.reason=content_filter` | `content_filtered` or refusal-compatible stops | `SAFETY`, `RECITATION`, `LANGUAGE`, `BLOCKLIST`, `PROHIBITED_CONTENT`, `SPII`, image safety/recitation stops | +| unknown | preserve-same-format | preserve-same-format | preserve-same-format | preserve-same-format | + +Implemented response guardrails: + +- Cross-format sync response conversion validates source finish/status enums before emitting the target response. +- Same-format canonical response roundtrip preserves raw OpenAI Chat `finish_reason`, OpenAI Responses `status`, Claude `stop_reason`, and Gemini `finishReason` values through provider extension metadata. +- OpenAI Chat unknown `choices[].finish_reason` fails with `InvalidEnumValue`. +- OpenAI Responses non-terminal `status` values (`queued`, `in_progress`, `cancelled`) are valid provider states but are blocked for sync response conversion because target sync formats cannot represent them losslessly. +- Runtime sync finalize does not fall back to legacy conversion when registry response conversion reports strict errors such as invalid enums, unsupported fields, lossy blocks, or invalid target fields. +- Stream terminal reasons now follow the same strict policy: unknown OpenAI / Claude raw finish reasons and Gemini known-but-unmappable values such as `OTHER` surface as `unsupported_finish_reason`, while OpenAI Responses `length` and `content_filter` stream finals emit `response.incomplete`. +- Gemini known but unmappable finish reasons such as `OTHER`, `MALFORMED_FUNCTION_CALL`, `UNEXPECTED_TOOL_CALL`, `MISSING_THOUGHT_SIGNATURE`, and `MALFORMED_RESPONSE` are blocked with `LossyConversionBlocked`; unknown future Gemini values fail with `InvalidEnumValue`. +- Stream finish reason guardrails are covered with provider-specific fixtures for usage, tool calls, reasoning signatures, media, and unknown payloads. Unknown provider stream events are not mapped as finish reasons; cross-format runtime conversion emits a target-format `unsupported_stream_event` error and terminates. + +## Embedding Task Types + +Provider-specific type: `GeminiEmbeddingTaskType`. + +Gemini embedding task values are stored in canonical `embedding.task` only after +source parsing. They are emitted to Gemini as `taskType` and validated before +cross-format conversion to a Gemini target. + +| Canonical task input | Gemini `taskType` output | Status | +| --- | --- | --- | +| `QUERY` | `RETRIEVAL_QUERY` | mapped alias | +| `RETRIEVAL_QUERY` | `RETRIEVAL_QUERY` | native | +| `DOCUMENT` | `RETRIEVAL_DOCUMENT` | mapped alias | +| `RETRIEVAL_DOCUMENT` | `RETRIEVAL_DOCUMENT` | native | +| `TEXT_MATCHING` | `SEMANTIC_SIMILARITY` | mapped alias | +| `SEMANTIC_SIMILARITY` | `SEMANTIC_SIMILARITY` | native | +| `CLASSIFICATION` | `CLASSIFICATION` | native | +| `CLUSTERING` | `CLUSTERING` | native | +| `QUESTION_ANSWERING` | `QUESTION_ANSWERING` | native | +| `FACT_VERIFICATION` | `FACT_VERIFICATION` | native | +| `CODE_RETRIEVAL_QUERY` | `CODE_RETRIEVAL_QUERY` | native | +| `TASK_TYPE_UNSPECIFIED` | `TASK_TYPE_UNSPECIFIED` | native | +| unknown value | none | blocked with `InvalidEnumValue` when targeting Gemini | + +Cross-provider rules: + +- Gemini `taskType` to OpenAI Embedding is blocked because OpenAI has no equivalent task field. +- Gemini `taskType` to Doubao/Aliyun is blocked for the same reason. +- Jina `task` may carry through canonical and emit as Jina `task`; when targeting Gemini it must match the valid Gemini task set above. diff --git a/docs/api/format-field-coverage-matrix.md b/docs/api/format-field-coverage-matrix.md new file mode 100644 index 000000000..84e862ddd --- /dev/null +++ b/docs/api/format-field-coverage-matrix.md @@ -0,0 +1,2095 @@ +# Format Field Coverage Matrix + +Last generated: 2026-06-03 + +This file is generated from the schema inventory in `docs/api/provider-interface-definitions.md` and gives every documented schema field an explicit handling status. “处理到” here means the field is either mapped, preserved in same-format paths, rejected with a structured fail-closed error, or explicitly outside the current conversion surface. It does not mean every field can be cross-format converted. + +Provider schema updates do not require immediate conversion-code changes for runtime safety. Same-format runtime paths bypass canonical conversion, and same-format canonical roundtrip preserves provider extension fields. Cross-format conversion only enables fields with an audited semantic mapping; newly discovered or unknown provider fields default to structured fail-closed behavior until mapped. + +Regenerate with: `python3 docs/api/generate_format_field_coverage.py`. + +Statuses used in this matrix: `native`, `mapped`, `mapped/lossy-blocked`, `extension-preserved`, `unaudited`, `unsupported`, `invalid-enum`, `lossy-blocked`, `not-in-conversion-surface`. + +| Provider | Schema | Field | Required | Type | Surface | Same-Format Runtime | Canonical Roundtrip | Cross-Format | Notes | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| OpenAI | `AdditionalTools` | `id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `AdditionalTools` | `role` | 是 | `MessageRole` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `AdditionalTools` | `tools` | 是 | `array` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `AdditionalTools` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `AdditionalToolsItemParam` | `id` | 否 | `string \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `AdditionalToolsItemParam` | `role` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `AdditionalToolsItemParam` | `tools` | 是 | `array` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `AdditionalToolsItemParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchCreateFileOperation` | `diff` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchCreateFileOperation` | `path` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchCreateFileOperation` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchCreateFileOperationParam` | `diff` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchCreateFileOperationParam` | `path` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchCreateFileOperationParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchDeleteFileOperation` | `path` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchDeleteFileOperation` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchDeleteFileOperationParam` | `path` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchDeleteFileOperationParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCall` | `call_id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCall` | `created_by` | 否 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCall` | `id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCall` | `operation` | 是 | `ApplyPatchCreateFileOperation \| ApplyPatchDeleteFileOperation \| ApplyPatchUpdateFileOperation` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCall` | `status` | 是 | `ApplyPatchCallStatus` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCall` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallItemParam` | `call_id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallItemParam` | `id` | 否 | `string \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallItemParam` | `operation` | 是 | `ApplyPatchOperationParam` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallItemParam` | `status` | 是 | `ApplyPatchCallStatusParam` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallItemParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallOutput` | `call_id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallOutput` | `created_by` | 否 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallOutput` | `id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallOutput` | `output` | 否 | `string \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallOutput` | `status` | 是 | `ApplyPatchCallOutputStatus` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallOutput` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallOutputItemParam` | `call_id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallOutputItemParam` | `id` | 否 | `string \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallOutputItemParam` | `output` | 否 | `string \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallOutputItemParam` | `status` | 是 | `ApplyPatchCallOutputStatusParam` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolCallOutputItemParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchToolParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchUpdateFileOperation` | `diff` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchUpdateFileOperation` | `path` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchUpdateFileOperation` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchUpdateFileOperationParam` | `diff` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchUpdateFileOperationParam` | `path` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApplyPatchUpdateFileOperationParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ApproximateLocation` | `city` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ApproximateLocation` | `country` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ApproximateLocation` | `region` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ApproximateLocation` | `timezone` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ApproximateLocation` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `AutoCodeInterpreterToolParam` | `file_ids` | 否 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `AutoCodeInterpreterToolParam` | `memory_limit` | 否 | `ContainerMemoryLimit \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `AutoCodeInterpreterToolParam` | `network_policy` | 否 | `ContainerNetworkPolicyDisabledParam \| ContainerNetworkPolicyAllowlistParam` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `AutoCodeInterpreterToolParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionAllowedTools` | `mode` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionAllowedTools` | `tools` | 是 | `array` | openai:chat standard | native | mapped | mapped | function tools map; unsupported tool kinds fail closed | +| OpenAI | `ChatCompletionAllowedToolsChoice` | `allowed_tools` | 是 | `ChatCompletionAllowedTools` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionAllowedToolsChoice` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionFunctionCallOption` | `name` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionFunctions` | `description` | 否 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionFunctions` | `name` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionFunctions` | `parameters` | 否 | `FunctionParameters` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionMessageCustomToolCall` | `custom` | 是 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionMessageCustomToolCall` | `id` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionMessageCustomToolCall` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionMessageToolCall` | `function` | 是 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionMessageToolCall` | `id` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionMessageToolCall` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionMessageToolCallChunk` | `function` | 否 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionMessageToolCallChunk` | `id` | 否 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionMessageToolCallChunk` | `index` | 是 | `integer` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionMessageToolCallChunk` | `type` | 否 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionNamedToolChoice` | `function` | 是 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionNamedToolChoice` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionNamedToolChoiceCustom` | `custom` | 是 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionNamedToolChoiceCustom` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestAssistantMessage` | `audio` | 否 | `object \| null` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestAssistantMessage` | `content` | 否 | `string \| array \| null` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestAssistantMessage` | `function_call` | 否 | `object \| null` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestAssistantMessage` | `name` | 否 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestAssistantMessage` | `refusal` | 否 | `string \| null` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestAssistantMessage` | `role` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestAssistantMessage` | `tool_calls` | 否 | `ChatCompletionMessageToolCalls` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestDeveloperMessage` | `content` | 是 | `string \| array` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestDeveloperMessage` | `name` | 否 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestDeveloperMessage` | `role` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestFunctionMessage` | `content` | 是 | `string \| null` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestFunctionMessage` | `name` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestFunctionMessage` | `role` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestMessageContentPartAudio` | `input_audio` | 是 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestMessageContentPartAudio` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestMessageContentPartFile` | `file` | 是 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestMessageContentPartFile` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestMessageContentPartImage` | `image_url` | 是 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestMessageContentPartImage` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestMessageContentPartRefusal` | `refusal` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestMessageContentPartRefusal` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestMessageContentPartText` | `text` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestMessageContentPartText` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestSystemMessage` | `content` | 是 | `string \| array` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestSystemMessage` | `name` | 否 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestSystemMessage` | `role` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestToolMessage` | `content` | 是 | `string \| array` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestToolMessage` | `role` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestToolMessage` | `tool_call_id` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestUserMessage` | `content` | 是 | `string \| array` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestUserMessage` | `name` | 否 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionRequestUserMessage` | `role` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionResponseMessage` | `annotations` | 否 | `array` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionResponseMessage` | `audio` | 否 | `object \| null` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionResponseMessage` | `content` | 是 | `string \| null` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionResponseMessage` | `function_call` | 否 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `ChatCompletionResponseMessage` | `refusal` | 是 | `string \| null` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionResponseMessage` | `role` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionResponseMessage` | `tool_calls` | 否 | `ChatCompletionMessageToolCalls` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionStreamResponseDelta` | `content` | 否 | `string \| null` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `ChatCompletionStreamResponseDelta` | `function_call` | 否 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `ChatCompletionStreamResponseDelta` | `refusal` | 否 | `string \| null` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `ChatCompletionStreamResponseDelta` | `role` | 否 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionStreamResponseDelta` | `tool_calls` | 否 | `array` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionTokenLogprob` | `bytes` | 是 | `array \| null` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionTokenLogprob` | `logprob` | 是 | `number` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionTokenLogprob` | `token` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionTokenLogprob` | `top_logprobs` | 是 | `array` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `ChatCompletionTool` | `function` | 是 | `FunctionObject` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ChatCompletionTool` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ClickParam` | `button` | 是 | `ClickButtonType` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ClickParam` | `keys` | 否 | `array \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ClickParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ClickParam` | `x` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ClickParam` | `y` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CodeInterpreterOutputImage` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CodeInterpreterOutputImage` | `url` | 是 | `string(uri)` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CodeInterpreterOutputLogs` | `logs` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CodeInterpreterOutputLogs` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CodeInterpreterTool` | `container` | 是 | `string \| AutoCodeInterpreterToolParam` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CodeInterpreterTool` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CodeInterpreterToolCall` | `code` | 是 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CodeInterpreterToolCall` | `container_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CodeInterpreterToolCall` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CodeInterpreterToolCall` | `outputs` | 是 | `array \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CodeInterpreterToolCall` | `status` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CodeInterpreterToolCall` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CompactResource` | `created_at` | 是 | `integer(unixtime)` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactResource` | `id` | 是 | `string` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactResource` | `object` | 是 | `string` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactResource` | `output` | 是 | `array` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactResource` | `usage` | 是 | `ResponseUsage` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactResponseMethodPublicBody` | `input` | 否 | `string \| array \| null` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactResponseMethodPublicBody` | `instructions` | 否 | `string \| null` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactResponseMethodPublicBody` | `model` | 是 | `ModelIdsCompaction` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactResponseMethodPublicBody` | `previous_response_id` | 否 | `string \| null` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactResponseMethodPublicBody` | `prompt_cache_key` | 否 | `string \| null` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactResponseMethodPublicBody` | `prompt_cache_retention` | 否 | `PromptCacheRetentionEnum \| null` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactResponseMethodPublicBody` | `service_tier` | 否 | `ServiceTierEnum \| null` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactionBody` | `created_by` | 否 | `string` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactionBody` | `encrypted_content` | 是 | `string` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactionBody` | `id` | 是 | `string` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactionBody` | `type` | 是 | `string` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactionSummaryItemParam` | `encrypted_content` | 是 | `string` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactionSummaryItemParam` | `id` | 否 | `string \| null` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactionSummaryItemParam` | `type` | 是 | `string` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CompactionTriggerItemParam` | `type` | 是 | `string` | openai:responses:compact native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ComparisonFilter` | `key` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComparisonFilter` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComparisonFilter` | `value` | 是 | `string \| number \| boolean \| array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CompletionUsage` | `completion_tokens` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CompletionUsage` | `completion_tokens_details` | 否 | `object` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CompletionUsage` | `prompt_tokens` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CompletionUsage` | `prompt_tokens_details` | 否 | `object` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CompletionUsage` | `total_tokens` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CompoundFilter` | `filters` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CompoundFilter` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerCallOutputItemParam` | `acknowledged_safety_checks` | 否 | `array \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerCallOutputItemParam` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerCallOutputItemParam` | `id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerCallOutputItemParam` | `output` | 是 | `ComputerScreenshotImage` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerCallOutputItemParam` | `status` | 否 | `FunctionCallItemStatus \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerCallOutputItemParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerCallSafetyCheckParam` | `code` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerCallSafetyCheckParam` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerCallSafetyCheckParam` | `message` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerScreenshotContent` | `detail` | 是 | `ImageDetail` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerScreenshotContent` | `file_id` | 是 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerScreenshotContent` | `image_url` | 是 | `string(uri) \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerScreenshotContent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerScreenshotImage` | `file_id` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerScreenshotImage` | `image_url` | 否 | `string(uri)` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerScreenshotImage` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerTool` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCall` | `action` | 否 | `ComputerAction` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCall` | `actions` | 否 | `ComputerActionList` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCall` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCall` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCall` | `pending_safety_checks` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCall` | `status` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCall` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutput` | `acknowledged_safety_checks` | 否 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutput` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutput` | `id` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutput` | `output` | 是 | `ComputerScreenshotImage` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutput` | `status` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutput` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutputResource` | `acknowledged_safety_checks` | 否 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutputResource` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutputResource` | `created_by` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutputResource` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutputResource` | `output` | 是 | `ComputerScreenshotImage` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutputResource` | `status` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerToolCallOutputResource` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerUsePreviewTool` | `display_height` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerUsePreviewTool` | `display_width` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerUsePreviewTool` | `environment` | 是 | `ComputerEnvironment` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ComputerUsePreviewTool` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerAutoParam` | `file_ids` | 否 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerAutoParam` | `memory_limit` | 否 | `ContainerMemoryLimit \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerAutoParam` | `network_policy` | 否 | `ContainerNetworkPolicyDisabledParam \| ContainerNetworkPolicyAllowlistParam` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerAutoParam` | `skills` | 否 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerAutoParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerFileCitationBody` | `container_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerFileCitationBody` | `end_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerFileCitationBody` | `file_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerFileCitationBody` | `filename` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerFileCitationBody` | `start_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerFileCitationBody` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerNetworkPolicyAllowlistParam` | `allowed_domains` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerNetworkPolicyAllowlistParam` | `domain_secrets` | 否 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerNetworkPolicyAllowlistParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerNetworkPolicyDisabledParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerNetworkPolicyDomainSecretParam` | `domain` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerNetworkPolicyDomainSecretParam` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerNetworkPolicyDomainSecretParam` | `value` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerReferenceParam` | `container_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerReferenceParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerReferenceResource` | `container_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContainerReferenceResource` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContextManagementParam` | `compact_threshold` | 否 | `integer \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ContextManagementParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Conversation-2` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ConversationParam-2` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CoordParam` | `x` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CoordParam` | `y` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateChatCompletionRequest` | `audio` | 否 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `CreateChatCompletionRequest` | `frequency_penalty` | 否 | `number` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `CreateChatCompletionRequest` | `function_call` | 否 | `string \| ChatCompletionFunctionCallOption` | openai:chat standard | native | extension-preserved | lossy-blocked | Chat-only or provider-specific field has no audited lossless target equivalent | +| OpenAI | `CreateChatCompletionRequest` | `functions` | 否 | `array` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `CreateChatCompletionRequest` | `logit_bias` | 否 | `object/map` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `CreateChatCompletionRequest` | `logprobs` | 否 | `boolean` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `CreateChatCompletionRequest` | `max_completion_tokens` | 否 | `integer` | openai:chat standard | native | mapped | mapped | maps to canonical max_tokens; target-specific field names | +| OpenAI | `CreateChatCompletionRequest` | `max_tokens` | 否 | `integer` | openai:chat standard | native | mapped | mapped | maps to canonical max_tokens; target-specific field names | +| OpenAI | `CreateChatCompletionRequest` | `messages` | 是 | `array` | openai:chat standard | native | mapped | mapped | messages/content/tool ids map through canonical messages | +| OpenAI | `CreateChatCompletionRequest` | `metadata` | 否 | `Metadata` | openai:chat standard | native | native | native | metadata maps where target has metadata | +| OpenAI | `CreateChatCompletionRequest` | `modalities` | 否 | `ResponseModalities` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `CreateChatCompletionRequest` | `model` | 是 | `ModelIdsShared` | openai:chat standard | native | native | mapped | native provider model field; runtime model override is transport-only | +| OpenAI | `CreateChatCompletionRequest` | `n` | 否 | `integer` | openai:chat standard | native | mapped | lossy-blocked | maps to Gemini candidateCount; blocked for Responses/Claude | +| OpenAI | `CreateChatCompletionRequest` | `parallel_tool_calls` | 否 | `ParallelToolCalls` | openai:chat standard | native | mapped | mapped | maps directly or inverse disable_parallel_tool_use | +| OpenAI | `CreateChatCompletionRequest` | `prediction` | 否 | `PredictionContent` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `CreateChatCompletionRequest` | `presence_penalty` | 否 | `number` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `CreateChatCompletionRequest` | `prompt_cache_key` | 否 | `string` | openai:chat standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateChatCompletionRequest` | `prompt_cache_retention` | 否 | `string \| null` | openai:chat standard | native | mapped | mapped | Chat request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateChatCompletionRequest` | `reasoning_effort` | 否 | `ReasoningEffort` | openai:chat standard | native | mapped | mapped | provider enum validated and mapped to target reasoning/thinking | +| OpenAI | `CreateChatCompletionRequest` | `response_format` | 否 | `ResponseFormatText \| ResponseFormatJsonSchema \| ResponseFormatJsonObject` | openai:chat standard | native | mapped | mapped | Chat request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateChatCompletionRequest` | `safety_identifier` | 否 | `string` | openai:chat standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateChatCompletionRequest` | `seed` | 否 | `integer` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `CreateChatCompletionRequest` | `service_tier` | 否 | `ServiceTier` | openai:chat standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateChatCompletionRequest` | `stop` | 否 | `StopConfiguration` | openai:chat standard | native | mapped | lossy-blocked | maps to Claude/Gemini stop sequences; blocked for Responses | +| OpenAI | `CreateChatCompletionRequest` | `store` | 否 | `boolean` | openai:chat standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateChatCompletionRequest` | `stream` | 否 | `boolean` | openai:chat standard | native | extension-preserved | mapped | same-format runtime passthrough; target stream policy is transport-owned | +| OpenAI | `CreateChatCompletionRequest` | `stream_options` | 否 | `ChatCompletionStreamOptions` | openai:chat standard | native | extension-preserved | lossy-blocked | Chat stream_options has no Responses/Claude/Gemini request equivalent | +| OpenAI | `CreateChatCompletionRequest` | `temperature` | 否 | `number \| null` | openai:chat standard | native | mapped | mapped | Chat request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateChatCompletionRequest` | `tool_choice` | 否 | `ChatCompletionToolChoiceOption` | openai:chat standard | native | mapped | mapped | tool choice enum/name style maps provider-specifically | +| OpenAI | `CreateChatCompletionRequest` | `tools` | 否 | `array` | openai:chat standard | native | mapped | mapped | Chat request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateChatCompletionRequest` | `top_logprobs` | 否 | `integer \| null` | openai:chat standard | native | mapped | mapped | Chat request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateChatCompletionRequest` | `top_p` | 否 | `number \| null` | openai:chat standard | native | mapped | mapped | Chat request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateChatCompletionRequest` | `user` | 否 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | official Chat field has no audited lossless target equivalent outside same-format/OpenAI-compatible paths | +| OpenAI | `CreateChatCompletionRequest` | `verbosity` | 否 | `Verbosity` | openai:chat standard | native | mapped | mapped | maps to Responses text.verbosity; provider-only elsewhere | +| OpenAI | `CreateChatCompletionRequest` | `web_search_options` | 否 | `object` | openai:chat standard | native | extension-preserved | mapped | maps to Gemini googleSearch; blocked where target has no equivalent | +| OpenAI | `CreateChatCompletionResponse` | `choices` | 是 | `array` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateChatCompletionResponse` | `created` | 是 | `integer(unixtime)` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateChatCompletionResponse` | `id` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateChatCompletionResponse` | `model` | 是 | `string` | openai:chat standard | native | native | mapped | native provider model field; runtime model override is transport-only | +| OpenAI | `CreateChatCompletionResponse` | `object` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateChatCompletionResponse` | `service_tier` | 否 | `ServiceTier` | openai:chat standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateChatCompletionResponse` | `system_fingerprint` | 否 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateChatCompletionResponse` | `usage` | 否 | `CompletionUsage` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateChatCompletionStreamResponse` | `choices` | 是 | `array` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateChatCompletionStreamResponse` | `created` | 是 | `integer(unixtime)` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateChatCompletionStreamResponse` | `id` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateChatCompletionStreamResponse` | `model` | 是 | `string` | openai:chat standard | native | native | mapped | native provider model field; runtime model override is transport-only | +| OpenAI | `CreateChatCompletionStreamResponse` | `object` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateChatCompletionStreamResponse` | `service_tier` | 否 | `ServiceTier` | openai:chat standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateChatCompletionStreamResponse` | `system_fingerprint` | 否 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateChatCompletionStreamResponse` | `usage` | 否 | `CompletionUsage` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CreateEmbeddingRequest` | `dimensions` | 否 | `integer` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `CreateEmbeddingRequest` | `encoding_format` | 否 | `string` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `CreateEmbeddingRequest` | `input` | 是 | `string \| array \| array \| array>` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `CreateEmbeddingRequest` | `model` | 是 | `string \| string` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `CreateEmbeddingRequest` | `user` | 否 | `string` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `CreateEmbeddingResponse` | `data` | 是 | `array` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `CreateEmbeddingResponse` | `model` | 是 | `string` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `CreateEmbeddingResponse` | `object` | 是 | `string` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `CreateEmbeddingResponse` | `usage` | 是 | `object` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `CreateImageEditRequest` | `background` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `image` | 是 | `string(binary) \| array` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `input_fidelity` | 否 | `InputFidelity \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `mask` | 否 | `string(binary)` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `model` | 否 | `string \| string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `n` | 否 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `output_compression` | 否 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `output_format` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `partial_images` | 否 | `PartialImages` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `prompt` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `quality` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `response_format` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `size` | 否 | `string \| string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `stream` | 否 | `boolean` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageEditRequest` | `user` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `background` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `model` | 否 | `string \| string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `moderation` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `n` | 否 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `output_compression` | 否 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `output_format` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `partial_images` | 否 | `PartialImages` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `prompt` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `quality` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `response_format` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `size` | 否 | `string \| string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `stream` | 否 | `boolean` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `style` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageRequest` | `user` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageVariationRequest` | `image` | 是 | `string(binary)` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageVariationRequest` | `model` | 否 | `string \| string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageVariationRequest` | `n` | 否 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageVariationRequest` | `response_format` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageVariationRequest` | `size` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateImageVariationRequest` | `user` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `CreateModelResponseProperties` | `metadata` | 否 | `Metadata` | openai:responses standard | native | native | native | metadata maps where target has metadata | +| OpenAI | `CreateModelResponseProperties` | `prompt_cache_key` | 否 | `string` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateModelResponseProperties` | `prompt_cache_retention` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateModelResponseProperties` | `safety_identifier` | 否 | `string` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateModelResponseProperties` | `service_tier` | 否 | `ServiceTier` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateModelResponseProperties` | `temperature` | 否 | `number \| null` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateModelResponseProperties` | `top_logprobs` | 否 | `integer \| null` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateModelResponseProperties` | `top_p` | 否 | `number \| null` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateModelResponseProperties` | `user` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `CreateResponse` | `background` | 否 | `boolean \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `CreateResponse` | `context_management` | 否 | `array \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `CreateResponse` | `conversation` | 否 | `ConversationParam \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `CreateResponse` | `include` | 否 | `array \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `CreateResponse` | `input` | 否 | `InputParam` | openai:responses standard | native | mapped | mapped | maps to canonical messages/content/tool I/O | +| OpenAI | `CreateResponse` | `instructions` | 否 | `string \| null` | openai:responses standard | native | mapped | mapped | Responses request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateResponse` | `max_output_tokens` | 否 | `integer \| null` | openai:responses standard | native | mapped | mapped | Responses request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateResponse` | `max_tool_calls` | 否 | `integer \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `CreateResponse` | `metadata` | 否 | `Metadata` | openai:responses standard | native | native | native | metadata maps where target has metadata | +| OpenAI | `CreateResponse` | `model` | 否 | `ModelIdsResponses` | openai:responses standard | native | native | mapped | native provider model field; runtime model override is transport-only | +| OpenAI | `CreateResponse` | `parallel_tool_calls` | 否 | `boolean \| null` | openai:responses standard | native | mapped | mapped | Responses request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateResponse` | `previous_response_id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `CreateResponse` | `prompt` | 否 | `Prompt` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `CreateResponse` | `prompt_cache_key` | 否 | `string` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateResponse` | `prompt_cache_retention` | 否 | `string \| null` | openai:responses standard | native | mapped | mapped | Responses request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateResponse` | `reasoning` | 否 | `Reasoning \| null` | openai:responses standard | native | mapped | mapped | Responses request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateResponse` | `safety_identifier` | 否 | `string` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateResponse` | `service_tier` | 否 | `ServiceTier` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `CreateResponse` | `store` | 否 | `boolean \| null` | openai:responses standard | native | mapped | mapped | Responses request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateResponse` | `stream` | 否 | `boolean \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | target stream policy is transport-owned; cross-format conversion does not emit provider stream flags | +| OpenAI | `CreateResponse` | `stream_options` | 否 | `ResponseStreamOptions` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `CreateResponse` | `temperature` | 否 | `number \| null` | openai:responses standard | native | mapped | mapped | Responses request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateResponse` | `text` | 否 | `ResponseTextParam` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `CreateResponse` | `tool_choice` | 否 | `ToolChoiceParam` | openai:responses standard | native | mapped | mapped | tool choice enum/name style maps provider-specifically | +| OpenAI | `CreateResponse` | `tools` | 否 | `ToolsArray` | openai:responses standard | native | mapped | mapped | function tools map; custom/built-in tools fail closed unless target supports equivalent | +| OpenAI | `CreateResponse` | `top_logprobs` | 否 | `integer \| null` | openai:responses standard | native | mapped | mapped | Responses request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateResponse` | `top_p` | 否 | `number \| null` | openai:responses standard | native | mapped | mapped | Responses request field maps provider-specifically; target-incompatible cases fail closed | +| OpenAI | `CreateResponse` | `truncation` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `CreateResponse` | `user` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `CustomGrammarFormatParam` | `definition` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomGrammarFormatParam` | `syntax` | 是 | `GrammarSyntax1` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomGrammarFormatParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomTextFormatParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCall` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCall` | `id` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCall` | `input` | 是 | `string` | openai:responses standard | native | mapped | mapped | maps to canonical messages/content/tool I/O | +| OpenAI | `CustomToolCall` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCall` | `namespace` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCall` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCallOutput` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCallOutput` | `id` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCallOutput` | `output` | 是 | `string \| array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCallOutput` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCallOutputResource` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCallOutputResource` | `created_by` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCallOutputResource` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCallOutputResource` | `output` | 是 | `string \| array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCallOutputResource` | `status` | 是 | `FunctionCallOutputStatusEnum` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolCallOutputResource` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolChatCompletions` | `custom` | 是 | `object` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolChatCompletions` | `type` | 是 | `string` | openai:chat standard | native | extension-preserved | lossy-blocked | nested Chat field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolParam` | `defer_loading` | 否 | `boolean` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolParam` | `description` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolParam` | `format` | 否 | `CustomTextFormatParam \| CustomGrammarFormatParam` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolParam` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `CustomToolParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `DoubleClickAction` | `keys` | 是 | `array \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `DoubleClickAction` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `DoubleClickAction` | `x` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `DoubleClickAction` | `y` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `DragParam` | `keys` | 否 | `array \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `DragParam` | `path` | 是 | `array` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `DragParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EasyInputMessage` | `content` | 是 | `string \| InputMessageContentList` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `EasyInputMessage` | `phase` | 否 | `MessagePhase \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `EasyInputMessage` | `role` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `EasyInputMessage` | `type` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `EditImageBodyJsonParam` | `background` | 否 | `string \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `images` | 是 | `array` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `input_fidelity` | 否 | `string \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `mask` | 否 | `ImageRefParam` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `model` | 否 | `string \| string \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `moderation` | 否 | `string \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `n` | 否 | `integer \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `output_compression` | 否 | `integer \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `output_format` | 否 | `string \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `partial_images` | 否 | `PartialImages` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `prompt` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `quality` | 否 | `string \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `size` | 否 | `string \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `stream` | 否 | `boolean \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `EditImageBodyJsonParam` | `user` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `Embedding` | `embedding` | 是 | `array` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `Embedding` | `index` | 是 | `integer` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `Embedding` | `object` | 是 | `string` | openai:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| OpenAI | `FileCitationBody` | `file_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FileCitationBody` | `filename` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FileCitationBody` | `index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FileCitationBody` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FilePath` | `file_id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FilePath` | `index` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FilePath` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FileSearchTool` | `filters` | 否 | `Filters \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FileSearchTool` | `max_num_results` | 否 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FileSearchTool` | `ranking_options` | 否 | `RankingOptions` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FileSearchTool` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FileSearchTool` | `vector_store_ids` | 是 | `array` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FileSearchToolCall` | `id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FileSearchToolCall` | `queries` | 是 | `array` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FileSearchToolCall` | `results` | 否 | `array \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FileSearchToolCall` | `status` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FileSearchToolCall` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `FunctionCallOutputItemParam` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionCallOutputItemParam` | `id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionCallOutputItemParam` | `output` | 是 | `string \| array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionCallOutputItemParam` | `status` | 否 | `FunctionCallItemStatus \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionCallOutputItemParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionObject` | `description` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionObject` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionObject` | `parameters` | 否 | `FunctionParameters` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionObject` | `strict` | 否 | `boolean \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellAction` | `commands` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellAction` | `max_output_length` | 是 | `integer \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellAction` | `timeout_ms` | 是 | `integer \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellActionParam` | `commands` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellActionParam` | `max_output_length` | 否 | `integer \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellActionParam` | `timeout_ms` | 否 | `integer \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCall` | `action` | 是 | `FunctionShellAction` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCall` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCall` | `created_by` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCall` | `environment` | 是 | `LocalEnvironmentResource \| ContainerReferenceResource \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCall` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCall` | `status` | 是 | `FunctionShellCallStatus` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCall` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallItemParam` | `action` | 是 | `FunctionShellActionParam` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallItemParam` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallItemParam` | `environment` | 否 | `LocalEnvironmentParam \| ContainerReferenceParam \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallItemParam` | `id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallItemParam` | `status` | 否 | `FunctionShellCallItemStatus \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallItemParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutput` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutput` | `created_by` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutput` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutput` | `max_output_length` | 是 | `integer \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutput` | `output` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutput` | `status` | 是 | `FunctionShellCallOutputStatusEnum` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutput` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputContent` | `created_by` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputContent` | `outcome` | 是 | `FunctionShellCallOutputTimeoutOutcome \| FunctionShellCallOutputExitOutcome` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputContent` | `stderr` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputContent` | `stdout` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputContentParam` | `outcome` | 是 | `FunctionShellCallOutputOutcomeParam` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputContentParam` | `stderr` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputContentParam` | `stdout` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputExitOutcome` | `exit_code` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputExitOutcome` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputExitOutcomeParam` | `exit_code` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputExitOutcomeParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputItemParam` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputItemParam` | `id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputItemParam` | `max_output_length` | 否 | `integer \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputItemParam` | `output` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputItemParam` | `status` | 否 | `FunctionShellCallItemStatus \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputItemParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputTimeoutOutcome` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellCallOutputTimeoutOutcomeParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellToolParam` | `environment` | 否 | `ContainerAutoParam \| LocalEnvironmentParam \| ContainerReferenceParam \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionShellToolParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionTool` | `defer_loading` | 否 | `boolean` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionTool` | `description` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionTool` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionTool` | `parameters` | 是 | `object/map \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionTool` | `strict` | 是 | `boolean \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionTool` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCall` | `arguments` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCall` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCall` | `id` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCall` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCall` | `namespace` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCall` | `status` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCall` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCallOutput` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCallOutput` | `id` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCallOutput` | `output` | 是 | `string \| array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCallOutput` | `status` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCallOutput` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCallOutputResource` | `call_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCallOutputResource` | `created_by` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCallOutputResource` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCallOutputResource` | `output` | 是 | `string \| array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCallOutputResource` | `status` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolCallOutputResource` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolParam` | `defer_loading` | 否 | `boolean` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolParam` | `description` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolParam` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolParam` | `parameters` | 否 | `EmptyModelParam \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolParam` | `strict` | 否 | `boolean \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `FunctionToolParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `HybridSearchOptions` | `embedding_weight` | 是 | `number` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `HybridSearchOptions` | `text_weight` | 是 | `number` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `Image` | `b64_json` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `Image` | `revised_prompt` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `Image` | `url` | 否 | `string(uri)` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditCompletedEvent` | `b64_json` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditCompletedEvent` | `background` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditCompletedEvent` | `created_at` | 是 | `integer(unixtime)` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditCompletedEvent` | `output_format` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditCompletedEvent` | `quality` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditCompletedEvent` | `size` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditCompletedEvent` | `type` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditCompletedEvent` | `usage` | 是 | `ImagesUsage` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditPartialImageEvent` | `b64_json` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditPartialImageEvent` | `background` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditPartialImageEvent` | `created_at` | 是 | `integer(unixtime)` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditPartialImageEvent` | `output_format` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditPartialImageEvent` | `partial_image_index` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditPartialImageEvent` | `quality` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditPartialImageEvent` | `size` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageEditPartialImageEvent` | `type` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenCompletedEvent` | `b64_json` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenCompletedEvent` | `background` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenCompletedEvent` | `created_at` | 是 | `integer(unixtime)` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenCompletedEvent` | `output_format` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenCompletedEvent` | `quality` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenCompletedEvent` | `size` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenCompletedEvent` | `type` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenCompletedEvent` | `usage` | 是 | `ImagesUsage` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenInputUsageDetails` | `image_tokens` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenInputUsageDetails` | `text_tokens` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenOutputTokensDetails` | `image_tokens` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenOutputTokensDetails` | `text_tokens` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenPartialImageEvent` | `b64_json` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenPartialImageEvent` | `background` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenPartialImageEvent` | `created_at` | 是 | `integer(unixtime)` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenPartialImageEvent` | `output_format` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenPartialImageEvent` | `partial_image_index` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenPartialImageEvent` | `quality` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenPartialImageEvent` | `size` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenPartialImageEvent` | `type` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `action` | 否 | `ImageGenActionEnum` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `background` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `input_fidelity` | 否 | `InputFidelity \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `input_image_mask` | 否 | `object` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `model` | 否 | `string \| string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `moderation` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `output_compression` | 否 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `output_format` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `partial_images` | 否 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `quality` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `size` | 否 | `string \| string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenTool` | `type` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenToolCall` | `id` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenToolCall` | `result` | 是 | `string \| null` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenToolCall` | `status` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenToolCall` | `type` | 是 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenUsage` | `input_tokens` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenUsage` | `input_tokens_details` | 是 | `ImageGenInputUsageDetails` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenUsage` | `output_tokens` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenUsage` | `output_tokens_details` | 否 | `ImageGenOutputTokensDetails` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageGenUsage` | `total_tokens` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageRefParam` | `file_id` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImageRefParam` | `image_url` | 否 | `string(uri)` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImagesResponse` | `background` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImagesResponse` | `created` | 是 | `integer(unixtime)` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImagesResponse` | `data` | 否 | `array` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImagesResponse` | `output_format` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImagesResponse` | `quality` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImagesResponse` | `size` | 否 | `string` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImagesResponse` | `usage` | 否 | `ImageGenUsage` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImagesUsage` | `input_tokens` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImagesUsage` | `input_tokens_details` | 是 | `object` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImagesUsage` | `output_tokens` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ImagesUsage` | `total_tokens` | 是 | `integer` | openai:image native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `InlineSkillParam` | `description` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `InlineSkillParam` | `name` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `InlineSkillParam` | `source` | 是 | `InlineSkillSourceParam` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `InlineSkillParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `InlineSkillSourceParam` | `data` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `InlineSkillSourceParam` | `media_type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `InlineSkillSourceParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `InputFileContent` | `detail` | 否 | `FileInputDetail` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputFileContent` | `file_data` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputFileContent` | `file_id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputFileContent` | `file_url` | 否 | `string(uri)` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputFileContent` | `filename` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputFileContent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputFileContentParam` | `detail` | 否 | `FileDetailEnum` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputFileContentParam` | `file_data` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputFileContentParam` | `file_id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputFileContentParam` | `file_url` | 否 | `string(uri) \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputFileContentParam` | `filename` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputFileContentParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputImageContent` | `detail` | 是 | `ImageDetail` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputImageContent` | `file_id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputImageContent` | `image_url` | 否 | `string(uri) \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputImageContent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputImageContentParamAutoParam` | `detail` | 否 | `DetailEnum \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputImageContentParamAutoParam` | `file_id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputImageContentParamAutoParam` | `image_url` | 否 | `string(uri) \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputImageContentParamAutoParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputMessage` | `content` | 是 | `InputMessageContentList` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputMessage` | `role` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputMessage` | `status` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputMessage` | `type` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputTextContent` | `text` | 是 | `string` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `InputTextContent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `InputTextContentParam` | `text` | 是 | `string` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `InputTextContentParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ItemReferenceParam` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ItemReferenceParam` | `type` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `KeyPressAction` | `keys` | 是 | `array` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `KeyPressAction` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalEnvironmentParam` | `skills` | 否 | `array` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalEnvironmentParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalEnvironmentResource` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellExecAction` | `command` | 是 | `array` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellExecAction` | `env` | 是 | `object/map` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellExecAction` | `timeout_ms` | 否 | `integer \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellExecAction` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellExecAction` | `user` | 否 | `string \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellExecAction` | `working_directory` | 否 | `string \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellToolCall` | `action` | 是 | `LocalShellExecAction` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellToolCall` | `call_id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellToolCall` | `id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellToolCall` | `status` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellToolCall` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellToolCallOutput` | `id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellToolCallOutput` | `output` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellToolCallOutput` | `status` | 否 | `string \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellToolCallOutput` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalShellToolParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalSkillParam` | `description` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalSkillParam` | `name` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LocalSkillParam` | `path` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `LogProb` | `bytes` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `LogProb` | `logprob` | 是 | `number` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `LogProb` | `token` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `LogProb` | `top_logprobs` | 是 | `array` | openai:responses standard | native | native | lossy-blocked | OpenAI-family only; blocked to Claude/Gemini | +| OpenAI | `MCPApprovalRequest` | `arguments` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalRequest` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalRequest` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalRequest` | `server_label` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalRequest` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalResponse` | `approval_request_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalResponse` | `approve` | 是 | `boolean` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalResponse` | `id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalResponse` | `reason` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalResponse` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalResponseResource` | `approval_request_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalResponseResource` | `approve` | 是 | `boolean` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalResponseResource` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalResponseResource` | `reason` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPApprovalResponseResource` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPListTools` | `error` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPListTools` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPListTools` | `server_label` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPListTools` | `tools` | 是 | `array` | openai:responses standard | native | mapped | mapped | function tools map; custom/built-in tools fail closed unless target supports equivalent | +| OpenAI | `MCPListTools` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPListToolsTool` | `annotations` | 否 | `object \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPListToolsTool` | `description` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPListToolsTool` | `input_schema` | 是 | `object` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPListToolsTool` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPTool` | `allowed_tools` | 否 | `array \| MCPToolFilter \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPTool` | `authorization` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPTool` | `connector_id` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPTool` | `defer_loading` | 否 | `boolean` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPTool` | `headers` | 否 | `object/map \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPTool` | `require_approval` | 否 | `object \| string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPTool` | `server_description` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPTool` | `server_label` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPTool` | `server_url` | 否 | `string(uri)` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPTool` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPToolCall` | `approval_request_id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPToolCall` | `arguments` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPToolCall` | `error` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPToolCall` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPToolCall` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPToolCall` | `output` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPToolCall` | `server_label` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPToolCall` | `status` | 否 | `MCPToolCallStatus` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPToolCall` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPToolFilter` | `read_only` | 否 | `boolean` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `MCPToolFilter` | `tool_names` | 否 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Message` | `content` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Message` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Message` | `phase` | 否 | `MessagePhase-2 \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Message` | `role` | 是 | `MessageRole` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Message` | `status` | 是 | `MessageStatus` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Message` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ModelResponseProperties` | `metadata` | 否 | `Metadata` | openai:responses standard | native | native | native | metadata maps where target has metadata | +| OpenAI | `ModelResponseProperties` | `prompt_cache_key` | 否 | `string` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `ModelResponseProperties` | `prompt_cache_retention` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `ModelResponseProperties` | `safety_identifier` | 否 | `string` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `ModelResponseProperties` | `service_tier` | 否 | `ServiceTier` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `ModelResponseProperties` | `temperature` | 否 | `number \| null` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `ModelResponseProperties` | `top_logprobs` | 否 | `integer \| null` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `ModelResponseProperties` | `top_p` | 否 | `number \| null` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `ModelResponseProperties` | `user` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `MoveParam` | `keys` | 否 | `array \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `MoveParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `MoveParam` | `x` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `MoveParam` | `y` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `NamespaceToolParam` | `description` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `NamespaceToolParam` | `name` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `NamespaceToolParam` | `tools` | 是 | `array` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `NamespaceToolParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `OutputMessage` | `content` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `OutputMessage` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `OutputMessage` | `phase` | 否 | `MessagePhase \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `OutputMessage` | `role` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `OutputMessage` | `status` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `OutputMessage` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `OutputTextContent` | `annotations` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `OutputTextContent` | `logprobs` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `OutputTextContent` | `text` | 是 | `string` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `OutputTextContent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `PredictionContent` | `content` | 是 | `string \| array` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `PredictionContent` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `RankingOptions` | `hybrid_search` | 否 | `HybridSearchOptions` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `RankingOptions` | `ranker` | 否 | `RankerVersionType` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `RankingOptions` | `score_threshold` | 否 | `number` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `Reasoning` | `effort` | 否 | `ReasoningEffort` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Reasoning` | `generate_summary` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Reasoning` | `summary` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ReasoningItem` | `content` | 否 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ReasoningItem` | `encrypted_content` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ReasoningItem` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ReasoningItem` | `status` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ReasoningItem` | `summary` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ReasoningItem` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ReasoningTextContent` | `text` | 是 | `string` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `ReasoningTextContent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `RefusalContent` | `refusal` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `RefusalContent` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `Response` | `background` | 否 | `boolean \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `completed_at` | 否 | `number(unixtime) \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `conversation` | 否 | `Conversation-2 \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `created_at` | 是 | `number(unixtime)` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `error` | 是 | `ResponseError` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `incomplete_details` | 是 | `object \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `instructions` | 是 | `string \| array \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `max_output_tokens` | 否 | `integer \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `max_tool_calls` | 否 | `integer \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `metadata` | 是 | `Metadata` | openai:responses standard | native | native | native | metadata maps where target has metadata | +| OpenAI | `Response` | `model` | 是 | `ModelIdsResponses` | openai:responses standard | native | native | mapped | native provider model field; runtime model override is transport-only | +| OpenAI | `Response` | `object` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `output` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `output_text` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `parallel_tool_calls` | 是 | `boolean` | openai:responses standard | native | native | mapped | maps directly or inverse disable_parallel_tool_use | +| OpenAI | `Response` | `previous_response_id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `prompt` | 否 | `Prompt` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `Response` | `prompt_cache_key` | 否 | `string` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `Response` | `prompt_cache_retention` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `reasoning` | 否 | `Reasoning \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `safety_identifier` | 否 | `string` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `Response` | `service_tier` | 否 | `ServiceTier` | openai:responses standard | native | extension-preserved | mapped | OpenAI-family only; blocked to non-OpenAI targets | +| OpenAI | `Response` | `status` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `temperature` | 是 | `number \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `text` | 否 | `ResponseTextParam` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `Response` | `tool_choice` | 是 | `ToolChoiceParam` | openai:responses standard | native | mapped | mapped | tool choice enum/name style maps provider-specifically | +| OpenAI | `Response` | `tools` | 是 | `ToolsArray` | openai:responses standard | native | mapped | mapped | function tools map; custom/built-in tools fail closed unless target supports equivalent | +| OpenAI | `Response` | `top_logprobs` | 否 | `integer \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `top_p` | 是 | `number \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `truncation` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `usage` | 否 | `ResponseUsage` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `Response` | `user` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `ResponseAudioDeltaEvent` | `delta` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseAudioDeltaEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseAudioDeltaEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseAudioDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseAudioDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseAudioTranscriptDeltaEvent` | `delta` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseAudioTranscriptDeltaEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseAudioTranscriptDeltaEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseAudioTranscriptDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseAudioTranscriptDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCodeDeltaEvent` | `delta` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCodeDeltaEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCodeDeltaEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCodeDeltaEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCodeDeltaEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCodeDoneEvent` | `code` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCodeDoneEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCodeDoneEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCodeDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCodeDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCompletedEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCompletedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCompletedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallCompletedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallInProgressEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallInProgressEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallInProgressEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallInProgressEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallInterpretingEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallInterpretingEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallInterpretingEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCodeInterpreterCallInterpretingEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCompletedEvent` | `response` | 是 | `Response` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCompletedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCompletedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartAddedEvent` | `content_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartAddedEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartAddedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartAddedEvent` | `part` | 是 | `OutputContent` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartAddedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartAddedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartDoneEvent` | `content_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartDoneEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartDoneEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartDoneEvent` | `part` | 是 | `OutputContent` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseContentPartDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCreatedEvent` | `response` | 是 | `Response` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCreatedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCreatedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCustomToolCallInputDeltaEvent` | `delta` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCustomToolCallInputDeltaEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCustomToolCallInputDeltaEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCustomToolCallInputDeltaEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCustomToolCallInputDeltaEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCustomToolCallInputDoneEvent` | `input` | 是 | `string` | openai:responses standard | native | mapped | mapped | maps to canonical messages/content/tool I/O | +| OpenAI | `ResponseCustomToolCallInputDoneEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCustomToolCallInputDoneEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCustomToolCallInputDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseCustomToolCallInputDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseErrorEvent` | `code` | 是 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseErrorEvent` | `message` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseErrorEvent` | `param` | 是 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseErrorEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseErrorEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFailedEvent` | `response` | 是 | `Response` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFailedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFailedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallCompletedEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallCompletedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallCompletedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallCompletedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallInProgressEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallInProgressEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallInProgressEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallInProgressEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallSearchingEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallSearchingEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallSearchingEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFileSearchCallSearchingEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFormatJsonObject` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFormatJsonSchema` | `json_schema` | 是 | `object` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFormatJsonSchema` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFormatText` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFunctionCallArgumentsDeltaEvent` | `delta` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFunctionCallArgumentsDeltaEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFunctionCallArgumentsDeltaEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFunctionCallArgumentsDeltaEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFunctionCallArgumentsDeltaEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFunctionCallArgumentsDoneEvent` | `arguments` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFunctionCallArgumentsDoneEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFunctionCallArgumentsDoneEvent` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFunctionCallArgumentsDoneEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFunctionCallArgumentsDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseFunctionCallArgumentsDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallCompletedEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallCompletedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallCompletedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallCompletedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallGeneratingEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallGeneratingEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallGeneratingEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallGeneratingEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallInProgressEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallInProgressEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallInProgressEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallInProgressEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallPartialImageEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallPartialImageEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallPartialImageEvent` | `partial_image_b64` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallPartialImageEvent` | `partial_image_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallPartialImageEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseImageGenCallPartialImageEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseInProgressEvent` | `response` | 是 | `Response` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseInProgressEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseInProgressEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseIncompleteEvent` | `response` | 是 | `Response` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseIncompleteEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseIncompleteEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseLogProb` | `logprob` | 是 | `number` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseLogProb` | `token` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseLogProb` | `top_logprobs` | 否 | `array` | openai:responses standard | native | native | lossy-blocked | OpenAI-family only; blocked to Claude/Gemini | +| OpenAI | `ResponseMCPCallArgumentsDeltaEvent` | `delta` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallArgumentsDeltaEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallArgumentsDeltaEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallArgumentsDeltaEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallArgumentsDeltaEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallArgumentsDoneEvent` | `arguments` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallArgumentsDoneEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallArgumentsDoneEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallArgumentsDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallArgumentsDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallCompletedEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallCompletedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallCompletedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallCompletedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallFailedEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallFailedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallFailedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallFailedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallInProgressEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallInProgressEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallInProgressEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPCallInProgressEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsCompletedEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsCompletedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsCompletedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsCompletedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsFailedEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsFailedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsFailedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsFailedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsInProgressEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsInProgressEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsInProgressEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseMCPListToolsInProgressEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputItemAddedEvent` | `item` | 是 | `OutputItem` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputItemAddedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputItemAddedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputItemAddedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputItemDoneEvent` | `item` | 是 | `OutputItem` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputItemDoneEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputItemDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputItemDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputTextAnnotationAddedEvent` | `annotation` | 是 | `object` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputTextAnnotationAddedEvent` | `annotation_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputTextAnnotationAddedEvent` | `content_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputTextAnnotationAddedEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputTextAnnotationAddedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputTextAnnotationAddedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseOutputTextAnnotationAddedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseProperties` | `background` | 否 | `boolean \| null` | openai:responses standard | native | mapped | mapped | native provider model field; runtime model override is transport-only | +| OpenAI | `ResponseProperties` | `max_tool_calls` | 否 | `integer \| null` | openai:responses standard | native | mapped | mapped | native provider model field; runtime model override is transport-only | +| OpenAI | `ResponseProperties` | `model` | 否 | `ModelIdsResponses` | openai:responses standard | native | native | mapped | native provider model field; runtime model override is transport-only | +| OpenAI | `ResponseProperties` | `previous_response_id` | 否 | `string \| null` | openai:responses standard | native | mapped | mapped | native provider model field; runtime model override is transport-only | +| OpenAI | `ResponseProperties` | `prompt` | 否 | `Prompt` | openai:responses standard | native | extension-preserved | lossy-blocked | Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent | +| OpenAI | `ResponseProperties` | `reasoning` | 否 | `Reasoning \| null` | openai:responses standard | native | mapped | mapped | native provider model field; runtime model override is transport-only | +| OpenAI | `ResponseProperties` | `text` | 否 | `ResponseTextParam` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `ResponseProperties` | `tool_choice` | 否 | `ToolChoiceParam` | openai:responses standard | native | mapped | mapped | tool choice enum/name style maps provider-specifically | +| OpenAI | `ResponseProperties` | `tools` | 否 | `ToolsArray` | openai:responses standard | native | mapped | mapped | function tools map; custom/built-in tools fail closed unless target supports equivalent | +| OpenAI | `ResponseProperties` | `truncation` | 否 | `string \| null` | openai:responses standard | native | mapped | mapped | native provider model field; runtime model override is transport-only | +| OpenAI | `ResponseQueuedEvent` | `response` | 是 | `Response` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseQueuedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseQueuedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartAddedEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartAddedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartAddedEvent` | `part` | 是 | `object` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartAddedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartAddedEvent` | `summary_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartAddedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartDoneEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartDoneEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartDoneEvent` | `part` | 是 | `object` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartDoneEvent` | `summary_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryPartDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryTextDeltaEvent` | `delta` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryTextDeltaEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryTextDeltaEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryTextDeltaEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryTextDeltaEvent` | `summary_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryTextDeltaEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryTextDoneEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryTextDoneEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryTextDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryTextDoneEvent` | `summary_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningSummaryTextDoneEvent` | `text` | 是 | `string` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `ResponseReasoningSummaryTextDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningTextDeltaEvent` | `content_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningTextDeltaEvent` | `delta` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningTextDeltaEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningTextDeltaEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningTextDeltaEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningTextDeltaEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningTextDoneEvent` | `content_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningTextDoneEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningTextDoneEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningTextDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseReasoningTextDoneEvent` | `text` | 是 | `string` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `ResponseReasoningTextDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDeltaEvent` | `content_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDeltaEvent` | `delta` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDeltaEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDeltaEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDeltaEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDeltaEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDoneEvent` | `content_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDoneEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDoneEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDoneEvent` | `refusal` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseRefusalDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDeltaEvent` | `content_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDeltaEvent` | `delta` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDeltaEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDeltaEvent` | `logprobs` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDeltaEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDeltaEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDeltaEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDoneEvent` | `content_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDoneEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDoneEvent` | `logprobs` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDoneEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDoneEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextDoneEvent` | `text` | 是 | `string` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `ResponseTextDoneEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextParam` | `format` | 否 | `TextResponseFormatConfiguration` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseTextParam` | `verbosity` | 否 | `Verbosity` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseUsage` | `input_tokens` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseUsage` | `input_tokens_details` | 是 | `object` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseUsage` | `output_tokens` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseUsage` | `output_tokens_details` | 是 | `object` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseUsage` | `total_tokens` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallCompletedEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallCompletedEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallCompletedEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallCompletedEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallInProgressEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallInProgressEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallInProgressEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallInProgressEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallSearchingEvent` | `item_id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallSearchingEvent` | `output_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallSearchingEvent` | `sequence_number` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ResponseWebSearchCallSearchingEvent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ScreenshotParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ScrollParam` | `keys` | 否 | `array \| null` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ScrollParam` | `scroll_x` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ScrollParam` | `scroll_y` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ScrollParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ScrollParam` | `x` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `ScrollParam` | `y` | 是 | `integer` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `SkillReferenceParam` | `skill_id` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `SkillReferenceParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `SkillReferenceParam` | `version` | 否 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `SpecificApplyPatchParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `SpecificFunctionShellParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `SummaryTextContent` | `text` | 是 | `string` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `SummaryTextContent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `TextContent` | `text` | 是 | `string` | openai:responses standard | native | mapped | mapped | text.format and text.verbosity map provider-specifically | +| OpenAI | `TextContent` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `TextResponseFormatJsonSchema` | `description` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `TextResponseFormatJsonSchema` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `TextResponseFormatJsonSchema` | `schema` | 是 | `ResponseFormatJsonSchemaSchema` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `TextResponseFormatJsonSchema` | `strict` | 否 | `boolean \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `TextResponseFormatJsonSchema` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolChoiceAllowed` | `mode` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolChoiceAllowed` | `tools` | 是 | `array` | openai:responses standard | native | mapped | mapped | function tools map; custom/built-in tools fail closed unless target supports equivalent | +| OpenAI | `ToolChoiceAllowed` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolChoiceCustom` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolChoiceCustom` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolChoiceFunction` | `name` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolChoiceFunction` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolChoiceMCP` | `name` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolChoiceMCP` | `server_label` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolChoiceMCP` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolChoiceTypes` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCall` | `arguments` | 是 | `object/value` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCall` | `call_id` | 是 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCall` | `created_by` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCall` | `execution` | 是 | `ToolSearchExecutionType` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCall` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCall` | `status` | 是 | `FunctionCallStatus` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCall` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCallItemParam` | `arguments` | 是 | `EmptyModelParam` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCallItemParam` | `call_id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCallItemParam` | `execution` | 否 | `ToolSearchExecutionType` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCallItemParam` | `id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCallItemParam` | `status` | 否 | `FunctionCallItemStatus \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchCallItemParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchOutput` | `call_id` | 是 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchOutput` | `created_by` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchOutput` | `execution` | 是 | `ToolSearchExecutionType` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchOutput` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchOutput` | `status` | 是 | `FunctionCallOutputStatusEnum` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchOutput` | `tools` | 是 | `array` | openai:responses standard | native | mapped | mapped | function tools map; custom/built-in tools fail closed unless target supports equivalent | +| OpenAI | `ToolSearchOutput` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchOutputItemParam` | `call_id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchOutputItemParam` | `execution` | 否 | `ToolSearchExecutionType` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchOutputItemParam` | `id` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchOutputItemParam` | `status` | 否 | `FunctionCallItemStatus \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchOutputItemParam` | `tools` | 是 | `array` | openai:responses standard | native | mapped | mapped | function tools map; custom/built-in tools fail closed unless target supports equivalent | +| OpenAI | `ToolSearchOutputItemParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchToolParam` | `description` | 否 | `string \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchToolParam` | `execution` | 否 | `ToolSearchExecutionType` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchToolParam` | `parameters` | 否 | `EmptyModelParam \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `ToolSearchToolParam` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `TopLogProb` | `bytes` | 是 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `TopLogProb` | `logprob` | 是 | `number` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `TopLogProb` | `token` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `TypeParam` | `text` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `TypeParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `UrlCitationBody` | `end_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `UrlCitationBody` | `start_index` | 是 | `integer` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `UrlCitationBody` | `title` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `UrlCitationBody` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `UrlCitationBody` | `url` | 是 | `string(uri)` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WaitParam` | `type` | 是 | `string` | openai auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| OpenAI | `WebSearchActionFind` | `pattern` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchActionFind` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchActionFind` | `url` | 是 | `string(uri)` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchActionOpenPage` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchActionOpenPage` | `url` | 否 | `string(uri) \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchActionSearch` | `queries` | 否 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchActionSearch` | `query` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchActionSearch` | `sources` | 否 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchActionSearch` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchLocation` | `city` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchLocation` | `country` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchLocation` | `region` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchLocation` | `timezone` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchPreviewTool` | `search_content_types` | 否 | `array` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchPreviewTool` | `search_context_size` | 否 | `SearchContextSize` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchPreviewTool` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchPreviewTool` | `user_location` | 否 | `ApproximateLocation \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchTool` | `filters` | 否 | `object \| null` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchTool` | `search_context_size` | 否 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchTool` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchTool` | `user_location` | 否 | `WebSearchApproximateLocation` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchToolCall` | `action` | 是 | `WebSearchActionSearch \| WebSearchActionOpenPage \| WebSearchActionFind` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchToolCall` | `id` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchToolCall` | `status` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| OpenAI | `WebSearchToolCall` | `type` | 是 | `string` | openai:responses standard | native | extension-preserved | lossy-blocked | nested Responses field is preserved same-format; cross-format requires explicit parent mapping or fails closed | +| Claude | `Base64ImageSource` | `data` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Base64ImageSource` | `media_type` | 是 | `'image/jpeg' \| 'image/png' \| 'image/gif' \| 'image/webp'` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Base64ImageSource` | `type` | 是 | `'base64'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Base64PDFSource` | `data` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Base64PDFSource` | `media_type` | 是 | `'application/pdf'` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Base64PDFSource` | `type` | 是 | `'base64'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionOutputBlock` | `file_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `BashCodeExecutionOutputBlock` | `type` | 是 | `'bash_code_execution_output'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionOutputBlockParam` | `file_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `BashCodeExecutionOutputBlockParam` | `type` | 是 | `'bash_code_execution_output'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionResultBlock` | `content` | 是 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionResultBlock` | `return_code` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `BashCodeExecutionResultBlock` | `stderr` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `BashCodeExecutionResultBlock` | `stdout` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `BashCodeExecutionResultBlock` | `type` | 是 | `'bash_code_execution_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionResultBlockParam` | `content` | 是 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionResultBlockParam` | `return_code` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `BashCodeExecutionResultBlockParam` | `stderr` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `BashCodeExecutionResultBlockParam` | `stdout` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `BashCodeExecutionResultBlockParam` | `type` | 是 | `'bash_code_execution_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionToolResultBlock` | `content` | 是 | `BashCodeExecutionToolResultError \| BashCodeExecutionResultBlock` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionToolResultBlock` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `BashCodeExecutionToolResultBlock` | `type` | 是 | `'bash_code_execution_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionToolResultBlockParam` | `content` | 是 | `BashCodeExecutionToolResultErrorParam \| BashCodeExecutionResultBlockParam` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionToolResultBlockParam` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `BashCodeExecutionToolResultBlockParam` | `type` | 是 | `'bash_code_execution_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionToolResultBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `BashCodeExecutionToolResultError` | `error_code` | 是 | `BashCodeExecutionToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `BashCodeExecutionToolResultError` | `type` | 是 | `'bash_code_execution_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `BashCodeExecutionToolResultErrorParam` | `error_code` | 是 | `BashCodeExecutionToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `BashCodeExecutionToolResultErrorParam` | `type` | 是 | `'bash_code_execution_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CacheControlEphemeral` | `type` | 是 | `'ephemeral'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CacheControlEphemeral` | `ttl` | 否 | `'5m' \| '1h'` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CacheCreation` | `ephemeral_1h_input_tokens` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CacheCreation` | `ephemeral_5m_input_tokens` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationCharLocation` | `cited_text` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationCharLocation` | `document_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationCharLocation` | `document_title` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationCharLocation` | `end_char_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationCharLocation` | `file_id` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationCharLocation` | `start_char_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationCharLocation` | `type` | 是 | `'char_location'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationCharLocationParam` | `cited_text` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationCharLocationParam` | `document_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationCharLocationParam` | `document_title` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationCharLocationParam` | `end_char_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationCharLocationParam` | `start_char_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationCharLocationParam` | `type` | 是 | `'char_location'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationContentBlockLocation` | `cited_text` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationContentBlockLocation` | `document_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationContentBlockLocation` | `document_title` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationContentBlockLocation` | `end_block_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationContentBlockLocation` | `file_id` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationContentBlockLocation` | `start_block_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationContentBlockLocation` | `type` | 是 | `'content_block_location'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationContentBlockLocationParam` | `cited_text` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationContentBlockLocationParam` | `document_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationContentBlockLocationParam` | `document_title` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationContentBlockLocationParam` | `end_block_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationContentBlockLocationParam` | `start_block_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationContentBlockLocationParam` | `type` | 是 | `'content_block_location'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationPageLocation` | `cited_text` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationPageLocation` | `document_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationPageLocation` | `document_title` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationPageLocation` | `end_page_number` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationPageLocation` | `file_id` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationPageLocation` | `start_page_number` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationPageLocation` | `type` | 是 | `'page_location'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationPageLocationParam` | `cited_text` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationPageLocationParam` | `document_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationPageLocationParam` | `document_title` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationPageLocationParam` | `end_page_number` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationPageLocationParam` | `start_page_number` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationPageLocationParam` | `type` | 是 | `'page_location'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationSearchResultLocationParam` | `cited_text` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationSearchResultLocationParam` | `end_block_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationSearchResultLocationParam` | `search_result_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationSearchResultLocationParam` | `source` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationSearchResultLocationParam` | `start_block_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationSearchResultLocationParam` | `title` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationSearchResultLocationParam` | `type` | 是 | `'search_result_location'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationWebSearchResultLocationParam` | `cited_text` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationWebSearchResultLocationParam` | `encrypted_index` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationWebSearchResultLocationParam` | `title` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationWebSearchResultLocationParam` | `type` | 是 | `'web_search_result_location'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationWebSearchResultLocationParam` | `url` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationsConfig` | `enabled` | 是 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationsConfigParam` | `enabled` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationsDelta` | `citation` | 是 | `\| CitationCharLocation \| CitationPageLocation \| CitationContentBlockLocation \| CitationsWebSearchResultLocation \| CitationsSearchResultLocation` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationsDelta` | `type` | 是 | `'citations_delta'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationsSearchResultLocation` | `cited_text` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationsSearchResultLocation` | `end_block_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationsSearchResultLocation` | `search_result_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationsSearchResultLocation` | `source` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationsSearchResultLocation` | `start_block_index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationsSearchResultLocation` | `title` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationsSearchResultLocation` | `type` | 是 | `'search_result_location'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationsWebSearchResultLocation` | `cited_text` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationsWebSearchResultLocation` | `encrypted_index` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CitationsWebSearchResultLocation` | `title` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CitationsWebSearchResultLocation` | `type` | 是 | `'web_search_result_location'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CitationsWebSearchResultLocation` | `url` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CodeExecutionOutputBlock` | `file_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionOutputBlock` | `type` | 是 | `'code_execution_output'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionOutputBlockParam` | `file_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionOutputBlockParam` | `type` | 是 | `'code_execution_output'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionResultBlock` | `content` | 是 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionResultBlock` | `return_code` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CodeExecutionResultBlock` | `stderr` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CodeExecutionResultBlock` | `stdout` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CodeExecutionResultBlock` | `type` | 是 | `'code_execution_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionResultBlockParam` | `content` | 是 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionResultBlockParam` | `return_code` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CodeExecutionResultBlockParam` | `stderr` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CodeExecutionResultBlockParam` | `stdout` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CodeExecutionResultBlockParam` | `type` | 是 | `'code_execution_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionTool20250522` | `name` | 是 | `'code_execution'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionTool20250522` | `type` | 是 | `'code_execution_20250522'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionTool20250522` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionTool20250522` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionTool20250522` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionTool20250522` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionTool20250825` | `name` | 是 | `'code_execution'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionTool20250825` | `type` | 是 | `'code_execution_20250825'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionTool20250825` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionTool20250825` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionTool20250825` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionTool20250825` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionTool20260120` | `name` | 是 | `'code_execution'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionTool20260120` | `type` | 是 | `'code_execution_20260120'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionTool20260120` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionTool20260120` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionTool20260120` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionTool20260120` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionToolResultBlock` | `content` | 是 | `CodeExecutionToolResultBlockContent` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionToolResultBlock` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CodeExecutionToolResultBlock` | `type` | 是 | `'code_execution_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionToolResultBlockParam` | `content` | 是 | `CodeExecutionToolResultBlockParamContent` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionToolResultBlockParam` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CodeExecutionToolResultBlockParam` | `type` | 是 | `'code_execution_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionToolResultBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `CodeExecutionToolResultError` | `error_code` | 是 | `CodeExecutionToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CodeExecutionToolResultError` | `type` | 是 | `'code_execution_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `CodeExecutionToolResultErrorParam` | `error_code` | 是 | `CodeExecutionToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `CodeExecutionToolResultErrorParam` | `type` | 是 | `'code_execution_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Container` | `id` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Container` | `expires_at` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ContainerUploadBlock` | `file_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ContainerUploadBlock` | `type` | 是 | `'container_upload'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ContainerUploadBlockParam` | `file_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ContainerUploadBlockParam` | `type` | 是 | `'container_upload'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ContainerUploadBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ContentBlockSource` | `content` | 是 | `string \| Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ContentBlockSource` | `type` | 是 | `'content'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `DirectCaller` | `type` | 是 | `'direct'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `DocumentBlock` | `citations` | 是 | `CitationsConfig \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `DocumentBlock` | `source` | 是 | `Base64PDFSource \| PlainTextSource` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `DocumentBlock` | `title` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `DocumentBlock` | `type` | 是 | `'document'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `DocumentBlockParam` | `source` | 是 | `Base64PDFSource \| PlainTextSource \| ContentBlockSource \| URLPDFSource` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `DocumentBlockParam` | `type` | 是 | `'document'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `DocumentBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `DocumentBlockParam` | `citations` | 否 | `CitationsConfigParam \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `DocumentBlockParam` | `context` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `DocumentBlockParam` | `title` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `EncryptedCodeExecutionResultBlock` | `content` | 是 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `EncryptedCodeExecutionResultBlock` | `encrypted_stdout` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `EncryptedCodeExecutionResultBlock` | `return_code` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `EncryptedCodeExecutionResultBlock` | `stderr` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `EncryptedCodeExecutionResultBlock` | `type` | 是 | `'encrypted_code_execution_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `EncryptedCodeExecutionResultBlockParam` | `content` | 是 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `EncryptedCodeExecutionResultBlockParam` | `encrypted_stdout` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `EncryptedCodeExecutionResultBlockParam` | `return_code` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `EncryptedCodeExecutionResultBlockParam` | `stderr` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `EncryptedCodeExecutionResultBlockParam` | `type` | 是 | `'encrypted_code_execution_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ImageBlockParam` | `source` | 是 | `Base64ImageSource \| URLImageSource` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ImageBlockParam` | `type` | 是 | `'image'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ImageBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `InputJSONDelta` | `partial_json` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `InputJSONDelta` | `type` | 是 | `'input_json_delta'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `JSONOutputFormat` | `schema` | 是 | `{ [key: string]: unknown }` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `JSONOutputFormat` | `type` | 是 | `'json_schema'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MemoryTool20250818` | `name` | 是 | `'memory'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MemoryTool20250818` | `type` | 是 | `'memory_20250818'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MemoryTool20250818` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `MemoryTool20250818` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `MemoryTool20250818` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `MemoryTool20250818` | `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `MemoryTool20250818` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `Message` | `id` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Message` | `container` | 是 | `Container \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `Message` | `content` | 是 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Message` | `model` | 是 | `Model` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Message` | `role` | 是 | `'assistant'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Message` | `stop_details` | 是 | `RefusalStopDetails \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Message` | `stop_reason` | 是 | `StopReason \| null` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Message` | `stop_sequence` | 是 | `string \| null` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Message` | `type` | 是 | `'message'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Message` | `usage` | 是 | `Usage` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCountTokensParams` | `messages` | 是 | `Array` | claude:messages/count_tokens native-only | native | not-in-conversion-surface | not-in-conversion-surface | count_tokens schemas are provider-native and outside canonical generation conversion | +| Claude | `MessageCountTokensParams` | `model` | 是 | `Model` | claude:messages/count_tokens native-only | native | not-in-conversion-surface | not-in-conversion-surface | count_tokens schemas are provider-native and outside canonical generation conversion | +| Claude | `MessageCountTokensParams` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages/count_tokens native-only | native | not-in-conversion-surface | not-in-conversion-surface | count_tokens schemas are provider-native and outside canonical generation conversion | +| Claude | `MessageCountTokensParams` | `output_config` | 否 | `OutputConfig` | claude:messages/count_tokens native-only | native | not-in-conversion-surface | not-in-conversion-surface | count_tokens schemas are provider-native and outside canonical generation conversion | +| Claude | `MessageCountTokensParams` | `system` | 否 | `string \| Array` | claude:messages/count_tokens native-only | native | not-in-conversion-surface | not-in-conversion-surface | count_tokens schemas are provider-native and outside canonical generation conversion | +| Claude | `MessageCountTokensParams` | `thinking` | 否 | `ThinkingConfigParam` | claude:messages/count_tokens native-only | native | not-in-conversion-surface | not-in-conversion-surface | count_tokens schemas are provider-native and outside canonical generation conversion | +| Claude | `MessageCountTokensParams` | `tool_choice` | 否 | `ToolChoice` | claude:messages/count_tokens native-only | native | not-in-conversion-surface | not-in-conversion-surface | count_tokens schemas are provider-native and outside canonical generation conversion | +| Claude | `MessageCountTokensParams` | `tools` | 否 | `Array` | claude:messages/count_tokens native-only | native | not-in-conversion-surface | not-in-conversion-surface | count_tokens schemas are provider-native and outside canonical generation conversion | +| Claude | `MessageCreateParamsBase` | `max_tokens` | 是 | `number` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `messages` | 是 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `model` | 是 | `Model` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `MessageCreateParamsBase` | `container` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `MessageCreateParamsBase` | `inference_geo` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `MessageCreateParamsBase` | `metadata` | 否 | `Metadata` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `output_config` | 否 | `OutputConfig` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `service_tier` | 否 | `'auto' \| 'standard_only'` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `MessageCreateParamsBase` | `stop_sequences` | 否 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `stream` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `MessageCreateParamsBase` | `system` | 否 | `string \| Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `temperature` | 否 | `number` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `thinking` | 否 | `ThinkingConfigParam` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `tool_choice` | 否 | `ToolChoice` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `tools` | 否 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `top_k` | 否 | `number` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsBase` | `top_p` | 否 | `number` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageCreateParamsNonStreaming` | `stream` | 否 | `false` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `MessageCreateParamsStreaming` | `stream` | 是 | `true` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `MessageDeltaUsage` | `cache_creation_input_tokens` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `MessageDeltaUsage` | `cache_read_input_tokens` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `MessageDeltaUsage` | `input_tokens` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `MessageDeltaUsage` | `output_tokens` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `MessageDeltaUsage` | `output_tokens_details` | 是 | `OutputTokensDetails \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `MessageDeltaUsage` | `server_tool_use` | 是 | `ServerToolUsage \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `MessageParam` | `content` | 是 | `string \| Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageParam` | `role` | 是 | `'user' \| 'assistant' \| 'system'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MessageTokensCount` | `input_tokens` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Metadata` | `user_id` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `MidConversationSystemBlockParam` | `content` | 是 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MidConversationSystemBlockParam` | `type` | 是 | `'mid_conv_system'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `MidConversationSystemBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `OutputConfig` | `effort` | 否 | `'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max' \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `OutputConfig` | `format` | 否 | `JSONOutputFormat \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `OutputTokensDetails` | `thinking_tokens` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `PlainTextSource` | `data` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `PlainTextSource` | `media_type` | 是 | `'text/plain'` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `PlainTextSource` | `type` | 是 | `'text'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `RawContentBlockDeltaEvent` | `delta` | 是 | `RawContentBlockDelta` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `RawContentBlockDeltaEvent` | `index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `RawContentBlockDeltaEvent` | `type` | 是 | `'content_block_delta'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `RawContentBlockStartEvent` | `content_block` | 是 | `\| TextBlock \| ThinkingBlock \| RedactedThinkingBlock \| ToolUseBlock \| ServerToolUseBlock \| WebSearchToolResultBlock \| WebFetchToolResultBlock \| CodeExecutionToolResultBlock \| BashCodeExecutionToolResultBlock \| TextEditorCodeExecutionToolResultBlock \| ToolSearchToolResultBlock \| ContainerUploadBlock` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `RawContentBlockStartEvent` | `index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `RawContentBlockStartEvent` | `type` | 是 | `'content_block_start'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `RawContentBlockStopEvent` | `index` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `RawContentBlockStopEvent` | `type` | 是 | `'content_block_stop'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `RawMessageDeltaEvent` | `delta` | 是 | `RawMessageDeltaEvent.Delta` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `RawMessageDeltaEvent` | `type` | 是 | `'message_delta'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `RawMessageDeltaEvent` | `usage` | 是 | `MessageDeltaUsage` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `RawMessageStartEvent` | `message` | 是 | `Message` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `RawMessageStartEvent` | `type` | 是 | `'message_start'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `RawMessageStopEvent` | `type` | 是 | `'message_stop'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `RedactedThinkingBlock` | `data` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `RedactedThinkingBlock` | `type` | 是 | `'redacted_thinking'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `RedactedThinkingBlockParam` | `data` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `RedactedThinkingBlockParam` | `type` | 是 | `'redacted_thinking'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `RefusalStopDetails` | `category` | 是 | `'cyber' \| 'bio' \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `RefusalStopDetails` | `explanation` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `RefusalStopDetails` | `type` | 是 | `'refusal'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `SearchResultBlockParam` | `content` | 是 | `Array` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `SearchResultBlockParam` | `source` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `SearchResultBlockParam` | `title` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `SearchResultBlockParam` | `type` | 是 | `'search_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `SearchResultBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `SearchResultBlockParam` | `citations` | 否 | `CitationsConfigParam` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ServerToolCaller` | `tool_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ServerToolCaller` | `type` | 是 | `'code_execution_20250825'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ServerToolCaller20260120` | `tool_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ServerToolCaller20260120` | `type` | 是 | `'code_execution_20260120'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ServerToolUsage` | `web_fetch_requests` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ServerToolUsage` | `web_search_requests` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ServerToolUseBlock` | `id` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ServerToolUseBlock` | `caller` | 是 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ServerToolUseBlock` | `input` | 是 | `unknown` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ServerToolUseBlock` | `name` | 是 | `\| 'web_search' \| 'web_fetch' \| 'code_execution' \| 'bash_code_execution' \| 'text_editor_code_execution' \| 'tool_search_tool_regex' \| 'tool_search_tool_bm25'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ServerToolUseBlock` | `type` | 是 | `'server_tool_use'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ServerToolUseBlockParam` | `id` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ServerToolUseBlockParam` | `input` | 是 | `unknown` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ServerToolUseBlockParam` | `name` | 是 | `\| 'web_search' \| 'web_fetch' \| 'code_execution' \| 'bash_code_execution' \| 'text_editor_code_execution' \| 'tool_search_tool_regex' \| 'tool_search_tool_bm25'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ServerToolUseBlockParam` | `type` | 是 | `'server_tool_use'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ServerToolUseBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ServerToolUseBlockParam` | `caller` | 否 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `SignatureDelta` | `signature` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `SignatureDelta` | `type` | 是 | `'signature_delta'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextBlock` | `citations` | 是 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `TextBlock` | `text` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextBlock` | `type` | 是 | `'text'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextBlockParam` | `text` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextBlockParam` | `type` | 是 | `'text'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `TextBlockParam` | `citations` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `TextDelta` | `text` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextDelta` | `type` | 是 | `'text_delta'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionCreateResultBlock` | `is_file_update` | 是 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionCreateResultBlock` | `type` | 是 | `'text_editor_code_execution_create_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionCreateResultBlockParam` | `is_file_update` | 是 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionCreateResultBlockParam` | `type` | 是 | `'text_editor_code_execution_create_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlock` | `lines` | 是 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlock` | `new_lines` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlock` | `new_start` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlock` | `old_lines` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlock` | `old_start` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlock` | `type` | 是 | `'text_editor_code_execution_str_replace_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlockParam` | `type` | 是 | `'text_editor_code_execution_str_replace_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlockParam` | `lines` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlockParam` | `new_lines` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlockParam` | `new_start` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlockParam` | `old_lines` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionStrReplaceResultBlockParam` | `old_start` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionToolResultBlock` | `content` | 是 | `\| TextEditorCodeExecutionToolResultError \| TextEditorCodeExecutionViewResultBlock \| TextEditorCodeExecutionCreateResultBlock \| TextEditorCodeExecutionStrReplaceResultBlock` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionToolResultBlock` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionToolResultBlock` | `type` | 是 | `'text_editor_code_execution_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionToolResultBlockParam` | `content` | 是 | `\| TextEditorCodeExecutionToolResultErrorParam \| TextEditorCodeExecutionViewResultBlockParam \| TextEditorCodeExecutionCreateResultBlockParam \| TextEditorCodeExecutionStrReplaceResultBlockParam` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionToolResultBlockParam` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionToolResultBlockParam` | `type` | 是 | `'text_editor_code_execution_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionToolResultBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `TextEditorCodeExecutionToolResultError` | `error_code` | 是 | `TextEditorCodeExecutionToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionToolResultError` | `error_message` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionToolResultError` | `type` | 是 | `'text_editor_code_execution_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionToolResultErrorParam` | `error_code` | 是 | `TextEditorCodeExecutionToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionToolResultErrorParam` | `type` | 是 | `'text_editor_code_execution_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionToolResultErrorParam` | `error_message` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlock` | `content` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlock` | `file_type` | 是 | `'text' \| 'image' \| 'pdf'` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlock` | `num_lines` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlock` | `start_line` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlock` | `total_lines` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlock` | `type` | 是 | `'text_editor_code_execution_view_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlockParam` | `content` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlockParam` | `file_type` | 是 | `'text' \| 'image' \| 'pdf'` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlockParam` | `type` | 是 | `'text_editor_code_execution_view_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlockParam` | `num_lines` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlockParam` | `start_line` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `TextEditorCodeExecutionViewResultBlockParam` | `total_lines` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ThinkingBlock` | `signature` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ThinkingBlock` | `thinking` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ThinkingBlock` | `type` | 是 | `'thinking'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ThinkingBlockParam` | `signature` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ThinkingBlockParam` | `thinking` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ThinkingBlockParam` | `type` | 是 | `'thinking'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ThinkingConfigAdaptive` | `type` | 是 | `'adaptive'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ThinkingConfigAdaptive` | `display` | 否 | `'summarized' \| 'omitted' \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ThinkingConfigDisabled` | `type` | 是 | `'disabled'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ThinkingConfigEnabled` | `budget_tokens` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ThinkingConfigEnabled` | `type` | 是 | `'enabled'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ThinkingConfigEnabled` | `display` | 否 | `'summarized' \| 'omitted' \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ThinkingDelta` | `thinking` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ThinkingDelta` | `type` | 是 | `'thinking_delta'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Tool` | `input_schema` | 是 | `Tool.InputSchema` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Tool` | `name` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Tool` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `Tool` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `Tool` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `Tool` | `description` | 否 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `Tool` | `eager_input_streaming` | 否 | `boolean \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Tool` | `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Tool` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `Tool` | `type` | 否 | `'custom' \| null` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolBash20250124` | `name` | 是 | `'bash'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolBash20250124` | `type` | 是 | `'bash_20250124'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolBash20250124` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolBash20250124` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolBash20250124` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolBash20250124` | `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolBash20250124` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolChoiceAny` | `type` | 是 | `'any'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolChoiceAny` | `disable_parallel_tool_use` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolChoiceAuto` | `type` | 是 | `'auto'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolChoiceAuto` | `disable_parallel_tool_use` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolChoiceNone` | `type` | 是 | `'none'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolChoiceTool` | `name` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolChoiceTool` | `type` | 是 | `'tool'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolChoiceTool` | `disable_parallel_tool_use` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolReferenceBlock` | `tool_name` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolReferenceBlock` | `type` | 是 | `'tool_reference'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolReferenceBlockParam` | `tool_name` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolReferenceBlockParam` | `type` | 是 | `'tool_reference'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolReferenceBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolResultBlockParam` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolResultBlockParam` | `type` | 是 | `'tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolResultBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolResultBlockParam` | `content` | 否 | `\| string \| Array< \| TextBlockParam \| ImageBlockParam \| SearchResultBlockParam \| DocumentBlockParam \| ToolReferenceBlockParam >` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolResultBlockParam` | `is_error` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolSearchToolBm25_20251119` | `name` | 是 | `'tool_search_tool_bm25'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolSearchToolBm25_20251119` | `type` | 是 | `'tool_search_tool_bm25_20251119' \| 'tool_search_tool_bm25'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolSearchToolBm25_20251119` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolSearchToolBm25_20251119` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolSearchToolBm25_20251119` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolSearchToolBm25_20251119` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolSearchToolRegex20251119` | `name` | 是 | `'tool_search_tool_regex'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolSearchToolRegex20251119` | `type` | 是 | `'tool_search_tool_regex_20251119' \| 'tool_search_tool_regex'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolSearchToolRegex20251119` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolSearchToolRegex20251119` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolSearchToolRegex20251119` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolSearchToolRegex20251119` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolSearchToolResultBlock` | `content` | 是 | `ToolSearchToolResultError \| ToolSearchToolSearchResultBlock` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolSearchToolResultBlock` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolSearchToolResultBlock` | `type` | 是 | `'tool_search_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolSearchToolResultBlockParam` | `content` | 是 | `ToolSearchToolResultErrorParam \| ToolSearchToolSearchResultBlockParam` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolSearchToolResultBlockParam` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolSearchToolResultBlockParam` | `type` | 是 | `'tool_search_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolSearchToolResultBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolSearchToolResultError` | `error_code` | 是 | `ToolSearchToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolSearchToolResultError` | `error_message` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolSearchToolResultError` | `type` | 是 | `'tool_search_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolSearchToolResultErrorParam` | `error_code` | 是 | `ToolSearchToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolSearchToolResultErrorParam` | `type` | 是 | `'tool_search_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolSearchToolSearchResultBlock` | `tool_references` | 是 | `Array` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolSearchToolSearchResultBlock` | `type` | 是 | `'tool_search_tool_search_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolSearchToolSearchResultBlockParam` | `tool_references` | 是 | `Array` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolSearchToolSearchResultBlockParam` | `type` | 是 | `'tool_search_tool_search_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolTextEditor20250124` | `name` | 是 | `'str_replace_editor'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolTextEditor20250124` | `type` | 是 | `'text_editor_20250124'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolTextEditor20250124` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolTextEditor20250124` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolTextEditor20250124` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolTextEditor20250124` | `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolTextEditor20250124` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolTextEditor20250429` | `name` | 是 | `'str_replace_based_edit_tool'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolTextEditor20250429` | `type` | 是 | `'text_editor_20250429'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolTextEditor20250429` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolTextEditor20250429` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolTextEditor20250429` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolTextEditor20250429` | `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolTextEditor20250429` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolTextEditor20250728` | `name` | 是 | `'str_replace_based_edit_tool'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolTextEditor20250728` | `type` | 是 | `'text_editor_20250728'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolTextEditor20250728` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolTextEditor20250728` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolTextEditor20250728` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolTextEditor20250728` | `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolTextEditor20250728` | `max_characters` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `ToolTextEditor20250728` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolUseBlock` | `id` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolUseBlock` | `caller` | 是 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolUseBlock` | `input` | 是 | `unknown` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolUseBlock` | `name` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolUseBlock` | `type` | 是 | `'tool_use'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolUseBlockParam` | `id` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolUseBlockParam` | `input` | 是 | `unknown` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolUseBlockParam` | `name` | 是 | `string` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolUseBlockParam` | `type` | 是 | `'tool_use'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `ToolUseBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `ToolUseBlockParam` | `caller` | 否 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `URLImageSource` | `type` | 是 | `'url'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `URLImageSource` | `url` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `URLPDFSource` | `type` | 是 | `'url'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `URLPDFSource` | `url` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Usage` | `cache_creation` | 是 | `CacheCreation \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `Usage` | `cache_creation_input_tokens` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `Usage` | `cache_read_input_tokens` | 是 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `Usage` | `inference_geo` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `Usage` | `input_tokens` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Usage` | `output_tokens` | 是 | `number` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Usage` | `output_tokens_details` | 是 | `OutputTokensDetails \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `Usage` | `server_tool_use` | 是 | `ServerToolUsage \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `Usage` | `service_tier` | 是 | `'standard' \| 'priority' \| 'batch' \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `UserLocation` | `type` | 是 | `'approximate'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `UserLocation` | `city` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `UserLocation` | `country` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `UserLocation` | `region` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `UserLocation` | `timezone` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchBlock` | `content` | 是 | `DocumentBlock` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchBlock` | `retrieved_at` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchBlock` | `type` | 是 | `'web_fetch_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchBlock` | `url` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchBlockParam` | `content` | 是 | `DocumentBlockParam` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchBlockParam` | `type` | 是 | `'web_fetch_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchBlockParam` | `url` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchBlockParam` | `retrieved_at` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchTool20250910` | `name` | 是 | `'web_fetch'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchTool20250910` | `type` | 是 | `'web_fetch_20250910'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchTool20250910` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20250910` | `allowed_domains` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20250910` | `blocked_domains` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20250910` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20250910` | `citations` | 否 | `CitationsConfigParam \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20250910` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20250910` | `max_content_tokens` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchTool20250910` | `max_uses` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20250910` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260209` | `name` | 是 | `'web_fetch'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchTool20260209` | `type` | 是 | `'web_fetch_20260209'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchTool20260209` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260209` | `allowed_domains` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260209` | `blocked_domains` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260209` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260209` | `citations` | 否 | `CitationsConfigParam \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260209` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260209` | `max_content_tokens` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchTool20260209` | `max_uses` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260209` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260309` | `name` | 是 | `'web_fetch'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchTool20260309` | `type` | 是 | `'web_fetch_20260309'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchTool20260309` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260309` | `allowed_domains` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260309` | `blocked_domains` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260309` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260309` | `citations` | 否 | `CitationsConfigParam \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260309` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260309` | `max_content_tokens` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchTool20260309` | `max_uses` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260309` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchTool20260309` | `use_cache` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchToolResultBlock` | `caller` | 是 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchToolResultBlock` | `content` | 是 | `WebFetchToolResultErrorBlock \| WebFetchBlock` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchToolResultBlock` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchToolResultBlock` | `type` | 是 | `'web_fetch_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchToolResultBlockParam` | `content` | 是 | `WebFetchToolResultErrorBlockParam \| WebFetchBlockParam` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchToolResultBlockParam` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchToolResultBlockParam` | `type` | 是 | `'web_fetch_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchToolResultBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchToolResultBlockParam` | `caller` | 否 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebFetchToolResultErrorBlock` | `error_code` | 是 | `WebFetchToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchToolResultErrorBlock` | `type` | 是 | `'web_fetch_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebFetchToolResultErrorBlockParam` | `error_code` | 是 | `WebFetchToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebFetchToolResultErrorBlockParam` | `type` | 是 | `'web_fetch_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchResultBlock` | `encrypted_content` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebSearchResultBlock` | `page_age` | 是 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebSearchResultBlock` | `title` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchResultBlock` | `type` | 是 | `'web_search_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchResultBlock` | `url` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebSearchResultBlockParam` | `encrypted_content` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebSearchResultBlockParam` | `title` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchResultBlockParam` | `type` | 是 | `'web_search_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchResultBlockParam` | `url` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebSearchResultBlockParam` | `page_age` | 否 | `string \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebSearchTool20250305` | `name` | 是 | `'web_search'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchTool20250305` | `type` | 是 | `'web_search_20250305'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchTool20250305` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20250305` | `allowed_domains` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20250305` | `blocked_domains` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20250305` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20250305` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20250305` | `max_uses` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20250305` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20250305` | `user_location` | 否 | `UserLocation \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20260209` | `name` | 是 | `'web_search'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchTool20260209` | `type` | 是 | `'web_search_20260209'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchTool20260209` | `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20260209` | `allowed_domains` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20260209` | `blocked_domains` | 否 | `Array \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20260209` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20260209` | `defer_loading` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20260209` | `max_uses` | 否 | `number \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20260209` | `strict` | 否 | `boolean` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchTool20260209` | `user_location` | 否 | `UserLocation \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchToolRequestError` | `error_code` | 是 | `WebSearchToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebSearchToolRequestError` | `type` | 是 | `'web_search_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchToolResultBlock` | `caller` | 是 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchToolResultBlock` | `content` | 是 | `WebSearchToolResultBlockContent` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchToolResultBlock` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebSearchToolResultBlock` | `type` | 是 | `'web_search_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchToolResultBlockParam` | `content` | 是 | `WebSearchToolResultBlockParamContent` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchToolResultBlockParam` | `tool_use_id` | 是 | `string` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebSearchToolResultBlockParam` | `type` | 是 | `'web_search_tool_result'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Claude | `WebSearchToolResultBlockParam` | `cache_control` | 否 | `CacheControlEphemeral \| null` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchToolResultBlockParam` | `caller` | 否 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent | +| Claude | `WebSearchToolResultError` | `error_code` | 是 | `WebSearchToolResultErrorCode` | claude:messages standard | native | extension-preserved | lossy-blocked | Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Claude | `WebSearchToolResultError` | `type` | 是 | `'web_search_tool_result_error'` | claude:messages standard | native | mapped | mapped/lossy-blocked | Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed | +| Gemini | `AttributionSourceId` | `groundingPassage` | 否 | `GroundingPassageId` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `AttributionSourceId` | `semanticRetrieverChunk` | 否 | `SemanticRetrieverChunk` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `AudioResponseFormat` | `bitRate` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `AudioResponseFormat` | `delivery` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `AudioResponseFormat` | `mimeType` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `AudioResponseFormat` | `sampleRate` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `BatchEmbedContentsRequest` | `requests` | 否 | `array` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `BatchEmbedContentsResponse` | `embeddings` | 否 | `array` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `BatchEmbedContentsResponse` | `usageMetadata` | 否 | `EmbeddingUsageMetadata` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `Blob` | `data` | 否 | `string(byte)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Blob` | `mimeType` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `avgLogprobs` | 否 | `number(double)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `citationMetadata` | 否 | `CitationMetadata` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `content` | 否 | `Content` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `finishMessage` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `finishReason` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `groundingAttributions` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `groundingMetadata` | 否 | `GroundingMetadata` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `index` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `logprobsResult` | 否 | `LogprobsResult` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `safetyRatings` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `tokenCount` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Candidate` | `urlContextMetadata` | 否 | `UrlContextMetadata` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `CitationMetadata` | `citationSources` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `CitationSource` | `endIndex` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `CitationSource` | `license` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `CitationSource` | `startIndex` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `CitationSource` | `uri` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `CodeExecutionResult` | `id` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `CodeExecutionResult` | `outcome` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `CodeExecutionResult` | `output` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ComputerUse` | `environment` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ComputerUse` | `excludedPredefinedFunctions` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Content` | `parts` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Content` | `role` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ContentEmbedding` | `shape` | 否 | `array` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `ContentEmbedding` | `values` | 否 | `array` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `CountTokensRequest` | `contents` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `CountTokensRequest` | `generateContentRequest` | 否 | `GenerateContentRequest` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `CountTokensResponse` | `cacheTokensDetails` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `CountTokensResponse` | `cachedContentTokenCount` | 否 | `integer(int32)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `CountTokensResponse` | `promptTokensDetails` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `CountTokensResponse` | `totalTokens` | 否 | `integer(int32)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `CreateFileRequest` | `file` | 否 | `File` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `CreateFileResponse` | `file` | 否 | `File` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `DynamicRetrievalConfig` | `dynamicThreshold` | 否 | `number(float)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `DynamicRetrievalConfig` | `mode` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `EmbedContentConfig` | `audioTrackExtraction` | 否 | `boolean` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentConfig` | `autoTruncate` | 否 | `boolean` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentConfig` | `documentOcr` | 否 | `boolean` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentConfig` | `outputDimensionality` | 否 | `integer(int32)` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentConfig` | `taskType` | 否 | `string` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentConfig` | `title` | 否 | `string` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentRequest` | `content` | 否 | `Content` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentRequest` | `embedContentConfig` | 否 | `EmbedContentConfig` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentRequest` | `model` | 否 | `string` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentRequest` | `outputDimensionality` | 否 | `integer(int32)` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentRequest` | `taskType` | 否 | `string` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentRequest` | `title` | 否 | `string` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentResponse` | `embedding` | 否 | `ContentEmbedding` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbedContentResponse` | `usageMetadata` | 否 | `EmbeddingUsageMetadata` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbeddingUsageMetadata` | `promptTokenCount` | 否 | `integer(int32)` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `EmbeddingUsageMetadata` | `promptTokenDetails` | 否 | `array` | gemini:embedding | native | mapped | mapped/lossy-blocked | embedding field maps only within embedding targets; incompatible task/input/provider extensions fail closed | +| Gemini | `ExecutableCode` | `code` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ExecutableCode` | `id` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ExecutableCode` | `language` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `File` | `createTime` | 否 | `string(google-datetime)` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `displayName` | 否 | `string` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `downloadUri` | 否 | `string` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `error` | 否 | `Status` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `expirationTime` | 否 | `string(google-datetime)` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `mimeType` | 否 | `string` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `name` | 否 | `string` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `sha256Hash` | 否 | `string(byte)` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `sizeBytes` | 否 | `string(int64)` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `source` | 否 | `string` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `state` | 否 | `string` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `updateTime` | 否 | `string(google-datetime)` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `uri` | 否 | `string` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `File` | `videoMetadata` | 否 | `VideoFileMetadata` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `FileData` | `fileUri` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FileData` | `mimeType` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FileSearch` | `fileSearchStoreNames` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `FileSearch` | `metadataFilter` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `FileSearch` | `topK` | 否 | `integer(int32)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `FunctionCall` | `args` | 否 | `object/map` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionCall` | `id` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionCall` | `name` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionCallingConfig` | `allowedFunctionNames` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionCallingConfig` | `mode` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionDeclaration` | `behavior` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionDeclaration` | `description` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionDeclaration` | `name` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionDeclaration` | `parameters` | 否 | `Schema` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionDeclaration` | `parametersJsonSchema` | 否 | `any` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionDeclaration` | `response` | 否 | `Schema` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionDeclaration` | `responseJsonSchema` | 否 | `any` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionResponse` | `id` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionResponse` | `name` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionResponse` | `parts` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionResponse` | `response` | 否 | `object/map` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionResponse` | `scheduling` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionResponse` | `willContinue` | 否 | `boolean` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionResponseBlob` | `data` | 否 | `string(byte)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionResponseBlob` | `mimeType` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `FunctionResponsePart` | `inlineData` | 否 | `FunctionResponseBlob` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerateContentRequest` | `cachedContent` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini provider-only field has no lossless OpenAI/Claude target equivalent | +| Gemini | `GenerateContentRequest` | `contents` | 否 | `array` | gemini:generate_content standard | native | mapped | mapped | contents/parts map through canonical messages | +| Gemini | `GenerateContentRequest` | `generationConfig` | 否 | `GenerationConfig` | gemini:generate_content standard | native | mapped | mapped | supported generation fields map; unsupported nested fields fail closed | +| Gemini | `GenerateContentRequest` | `model` | 否 | `string` | gemini:generate_content standard | native | native | mapped | native provider model field or URL path model | +| Gemini | `GenerateContentRequest` | `safetySettings` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini provider-only field has no lossless OpenAI/Claude target equivalent | +| Gemini | `GenerateContentRequest` | `serviceTier` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini provider-only field has no lossless OpenAI/Claude target equivalent | +| Gemini | `GenerateContentRequest` | `store` | 否 | `boolean` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini provider-only field has no lossless OpenAI/Claude target equivalent | +| Gemini | `GenerateContentRequest` | `systemInstruction` | 否 | `Content` | gemini:generate_content standard | native | mapped | mapped | maps to target system/instructions | +| Gemini | `GenerateContentRequest` | `toolConfig` | 否 | `ToolConfig` | gemini:generate_content standard | native | mapped | mapped | functionCallingConfig mode and single allowedFunctionNames map | +| Gemini | `GenerateContentRequest` | `tools` | 否 | `array` | gemini:generate_content standard | native | mapped | mapped | functionDeclarations map; built-in tools require explicit target mapping | +| Gemini | `GenerateContentResponse` | `candidates` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerateContentResponse` | `modelStatus` | 否 | `ModelStatus` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerateContentResponse` | `modelVersion` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerateContentResponse` | `promptFeedback` | 否 | `PromptFeedback` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerateContentResponse` | `responseId` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerateContentResponse` | `usageMetadata` | 否 | `UsageMetadata` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `_responseJsonSchema` | 否 | `any` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `candidateCount` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `enableEnhancedCivicAnswers` | 否 | `boolean` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `frequencyPenalty` | 否 | `number(float)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `imageConfig` | 否 | `ImageConfig` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `logprobs` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `maxOutputTokens` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `mediaResolution` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `presencePenalty` | 否 | `number(float)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `responseFormat` | 否 | `ResponseFormatConfig` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `responseJsonSchema` | 否 | `any` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `responseLogprobs` | 否 | `boolean` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `responseMimeType` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `responseModalities` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `responseSchema` | 否 | `Schema` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `seed` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `speechConfig` | 否 | `SpeechConfig` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `stopSequences` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `temperature` | 否 | `number(float)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `thinkingConfig` | 否 | `ThinkingConfig` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `topK` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GenerationConfig` | `topP` | 否 | `number(float)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GoogleAiGenerativelanguageV1betaGroundingSupport` | `confidenceScores` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GoogleAiGenerativelanguageV1betaGroundingSupport` | `groundingChunkIndices` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GoogleAiGenerativelanguageV1betaGroundingSupport` | `renderedParts` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GoogleAiGenerativelanguageV1betaGroundingSupport` | `segment` | 否 | `GoogleAiGenerativelanguageV1betaSegment` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GoogleAiGenerativelanguageV1betaSegment` | `endIndex` | 否 | `integer(int32)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GoogleAiGenerativelanguageV1betaSegment` | `partIndex` | 否 | `integer(int32)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GoogleAiGenerativelanguageV1betaSegment` | `startIndex` | 否 | `integer(int32)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GoogleAiGenerativelanguageV1betaSegment` | `text` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GoogleMaps` | `enableWidget` | 否 | `boolean` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GoogleSearch` | `searchTypes` | 否 | `SearchTypes` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GoogleSearch` | `timeRangeFilter` | 否 | `Interval` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GoogleSearchRetrieval` | `dynamicRetrievalConfig` | 否 | `DynamicRetrievalConfig` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingAttribution` | `content` | 否 | `Content` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GroundingAttribution` | `sourceId` | 否 | `AttributionSourceId` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GroundingChunk` | `image` | 否 | `Image` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingChunk` | `maps` | 否 | `Maps` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingChunk` | `retrievedContext` | 否 | `RetrievedContext` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingChunk` | `web` | 否 | `Web` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingChunkCustomMetadata` | `key` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GroundingChunkCustomMetadata` | `numericValue` | 否 | `number(float)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GroundingChunkCustomMetadata` | `stringListValue` | 否 | `GroundingChunkStringList` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GroundingChunkCustomMetadata` | `stringValue` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GroundingChunkStringList` | `values` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GroundingMetadata` | `googleMapsWidgetContextToken` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingMetadata` | `groundingChunks` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingMetadata` | `groundingSupports` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingMetadata` | `imageSearchQueries` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingMetadata` | `retrievalMetadata` | 否 | `RetrievalMetadata` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingMetadata` | `searchEntryPoint` | 否 | `SearchEntryPoint` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingMetadata` | `webSearchQueries` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `GroundingPassageId` | `partIndex` | 否 | `integer(int32)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `GroundingPassageId` | `passageId` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Image` | `domain` | 否 | `string` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Image` | `imageUri` | 否 | `string` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Image` | `sourceUri` | 否 | `string` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Image` | `title` | 否 | `string` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ImageConfig` | `aspectRatio` | 否 | `string` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ImageConfig` | `imageSize` | 否 | `string` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ImageResponseFormat` | `aspectRatio` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ImageResponseFormat` | `delivery` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ImageResponseFormat` | `imageSize` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ImageResponseFormat` | `mimeType` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Interval` | `endTime` | 否 | `string(google-datetime)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Interval` | `startTime` | 否 | `string(google-datetime)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `LatLng` | `latitude` | 否 | `number(double)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `LatLng` | `longitude` | 否 | `number(double)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ListFilesResponse` | `files` | 否 | `array` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ListFilesResponse` | `nextPageToken` | 否 | `string` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `LogprobsResult` | `chosenCandidates` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `LogprobsResult` | `logProbabilitySum` | 否 | `number(float)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `LogprobsResult` | `topCandidates` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `LogprobsResultCandidate` | `logProbability` | 否 | `number(float)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `LogprobsResultCandidate` | `token` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `LogprobsResultCandidate` | `tokenId` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Maps` | `placeAnswerSources` | 否 | `PlaceAnswerSources` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Maps` | `placeId` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Maps` | `text` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Maps` | `title` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Maps` | `uri` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `McpServer` | `name` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `McpServer` | `streamableHttpTransport` | 否 | `StreamableHttpTransport` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ModalityTokenCount` | `modality` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ModalityTokenCount` | `tokenCount` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ModelStatus` | `message` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ModelStatus` | `modelStage` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ModelStatus` | `retirementTime` | 否 | `string(google-datetime)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `MultiSpeakerVoiceConfig` | `speakerVoiceConfigs` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Operation` | `done` | 否 | `boolean` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Operation` | `error` | 否 | `Status` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Operation` | `metadata` | 否 | `object/map` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Operation` | `name` | 否 | `string` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Operation` | `response` | 否 | `object/map` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Part` | `codeExecutionResult` | 否 | `CodeExecutionResult` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `executableCode` | 否 | `ExecutableCode` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `fileData` | 否 | `FileData` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `functionCall` | 否 | `FunctionCall` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `functionResponse` | 否 | `FunctionResponse` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `inlineData` | 否 | `Blob` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `mediaResolution` | 否 | `MediaResolution` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `partMetadata` | 否 | `object/map` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `text` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `thought` | 否 | `boolean` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `thoughtSignature` | 否 | `string(byte)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `toolCall` | 否 | `ToolCall` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `toolResponse` | 否 | `ToolResponse` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Part` | `videoMetadata` | 否 | `VideoMetadata` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `PlaceAnswerSources` | `reviewSnippets` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `PrebuiltVoiceConfig` | `voiceName` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `PredictLongRunningRequest` | `instances` | 否 | `array` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `PredictLongRunningRequest` | `parameters` | 否 | `any` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `PromptFeedback` | `blockReason` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `PromptFeedback` | `safetyRatings` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ResponseFormatConfig` | `audio` | 否 | `AudioResponseFormat` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ResponseFormatConfig` | `image` | 否 | `ImageResponseFormat` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ResponseFormatConfig` | `text` | 否 | `TextResponseFormat` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `RetrievalConfig` | `languageCode` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `RetrievalConfig` | `latLng` | 否 | `LatLng` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `RetrievalMetadata` | `googleSearchDynamicRetrievalScore` | 否 | `number(float)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `RetrievedContext` | `customMetadata` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `RetrievedContext` | `fileSearchStore` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `RetrievedContext` | `mediaId` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `RetrievedContext` | `pageNumber` | 否 | `integer(int32)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `RetrievedContext` | `text` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `RetrievedContext` | `title` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `RetrievedContext` | `uri` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ReviewSnippet` | `googleMapsUri` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ReviewSnippet` | `reviewId` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ReviewSnippet` | `title` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `SafetyRating` | `blocked` | 否 | `boolean` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `SafetyRating` | `category` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `SafetyRating` | `probability` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `SafetySetting` | `category` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `SafetySetting` | `threshold` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `anyOf` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `default` | 否 | `any` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `description` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `enum` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `example` | 否 | `any` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `format` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `items` | 否 | `Schema` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `maxItems` | 否 | `string(int64)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `maxLength` | 否 | `string(int64)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `maxProperties` | 否 | `string(int64)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `maximum` | 否 | `number(double)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `minItems` | 否 | `string(int64)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `minLength` | 否 | `string(int64)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `minProperties` | 否 | `string(int64)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `minimum` | 否 | `number(double)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `nullable` | 否 | `boolean` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `pattern` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `properties` | 否 | `object/map` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `propertyOrdering` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `required` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `title` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Schema` | `type` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `SearchEntryPoint` | `renderedContent` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `SearchEntryPoint` | `sdkBlob` | 否 | `string(byte)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `SearchTypes` | `imageSearch` | 否 | `ImageSearch` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `SearchTypes` | `webSearch` | 否 | `WebSearch` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `SemanticRetrieverChunk` | `chunk` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `SemanticRetrieverChunk` | `source` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `SpeakerVoiceConfig` | `speaker` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `SpeakerVoiceConfig` | `voiceConfig` | 否 | `VoiceConfig` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `SpeechConfig` | `languageCode` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `SpeechConfig` | `multiSpeakerVoiceConfig` | 否 | `MultiSpeakerVoiceConfig` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `SpeechConfig` | `voiceConfig` | 否 | `VoiceConfig` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Status` | `code` | 否 | `integer(int32)` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Status` | `details` | 否 | `array>` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Status` | `message` | 否 | `string` | gemini:files native-only | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `StreamableHttpTransport` | `headers` | 否 | `object/map` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `StreamableHttpTransport` | `sseReadTimeout` | 否 | `string(google-duration)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `StreamableHttpTransport` | `terminateOnClose` | 否 | `boolean` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `StreamableHttpTransport` | `timeout` | 否 | `string(google-duration)` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `StreamableHttpTransport` | `url` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `TextResponseFormat` | `mimeType` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `TextResponseFormat` | `schema` | 否 | `any` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ThinkingConfig` | `includeThoughts` | 否 | `boolean` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ThinkingConfig` | `thinkingBudget` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ThinkingConfig` | `thinkingLevel` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Tool` | `codeExecution` | 否 | `CodeExecution` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Tool` | `computerUse` | 否 | `ComputerUse` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Tool` | `fileSearch` | 否 | `FileSearch` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Tool` | `functionDeclarations` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Tool` | `googleMaps` | 否 | `GoogleMaps` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Tool` | `googleSearch` | 否 | `GoogleSearch` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Tool` | `googleSearchRetrieval` | 否 | `GoogleSearchRetrieval` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Tool` | `mcpServers` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `Tool` | `urlContext` | 否 | `UrlContext` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ToolCall` | `args` | 否 | `object/map` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ToolCall` | `id` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ToolCall` | `toolType` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ToolConfig` | `functionCallingConfig` | 否 | `FunctionCallingConfig` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ToolConfig` | `includeServerSideToolInvocations` | 否 | `boolean` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ToolConfig` | `retrievalConfig` | 否 | `RetrievalConfig` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `ToolResponse` | `id` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ToolResponse` | `response` | 否 | `object/map` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `ToolResponse` | `toolType` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `TopCandidates` | `candidates` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `UrlContextMetadata` | `urlMetadata` | 否 | `array` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `UrlMetadata` | `retrievedUrl` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `UrlMetadata` | `urlRetrievalStatus` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `UsageMetadata` | `cacheTokensDetails` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `UsageMetadata` | `cachedContentTokenCount` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `UsageMetadata` | `candidatesTokenCount` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `UsageMetadata` | `candidatesTokensDetails` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `UsageMetadata` | `promptTokenCount` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `UsageMetadata` | `promptTokensDetails` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `UsageMetadata` | `serviceTier` | 否 | `string` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini provider-only field has no lossless OpenAI/Claude target equivalent | +| Gemini | `UsageMetadata` | `thoughtsTokenCount` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `UsageMetadata` | `toolUsePromptTokenCount` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `UsageMetadata` | `toolUsePromptTokensDetails` | 否 | `array` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `UsageMetadata` | `totalTokenCount` | 否 | `integer(int32)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `VideoFileMetadata` | `videoDuration` | 否 | `string(google-duration)` | gemini:video/native operation | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `VideoMetadata` | `endOffset` | 否 | `string(google-duration)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `VideoMetadata` | `fps` | 否 | `number(double)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `VideoMetadata` | `startOffset` | 否 | `string(google-duration)` | gemini:generate_content standard | native | extension-preserved | lossy-blocked | Gemini nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed | +| Gemini | `VoiceConfig` | `prebuiltVoiceConfig` | 否 | `PrebuiltVoiceConfig` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Web` | `title` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | +| Gemini | `Web` | `uri` | 否 | `string` | gemini auxiliary / not-in-conversion-surface | native | not-in-conversion-surface | not-in-conversion-surface | not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly | + +Total covered schema fields: 2079. diff --git a/docs/api/format-passthrough-contract.md b/docs/api/format-passthrough-contract.md new file mode 100644 index 000000000..ea11ec13a --- /dev/null +++ b/docs/api/format-passthrough-contract.md @@ -0,0 +1,85 @@ +# Format Passthrough Contract + +Last audited: 2026-06-03 + +This document defines the boundary between runtime passthrough, canonical roundtrip tests, and cross-format conversion. + +## Runtime Same-Format Path + +Runtime same-format provider paths must not call canonical conversion. + +Current implementation: + +- `crates/aether-provider-transport/src/same_format_provider/mod.rs` checks `api_format_alias_matches(client_api_format, provider_api_format)`. +- When formats match, the provider body is built by copying the parsed JSON object field-for-field. +- When formats differ, the provider body is built through `aether_ai_formats::convert_request_pure`. +- Model override, body rules, model directives, Claude Code sanitization, Gemini function-response id stripping, and stream policy are applied only after the passthrough/conversion branch in provider transport. + +Important limitation: + +- The current transport helper receives `body_json: &serde_json::Value`, not raw request bytes. It therefore guarantees no canonical conversion and JSON value preservation at this layer, but it cannot preserve original whitespace or object key order by itself. +- True byte-level passthrough for requests with no transport edits requires a higher-level raw-body path that can forward the original bytes directly. Until that raw-body plumbing exists, tests should assert "conversion module not called" and JSON value equivalence for this helper, not byte-for-byte serialization equivalence. + +Provider schema drift does not change this rule. If OpenAI, Claude, or Gemini add a new field, same-format runtime routing must still forward it as part of the original provider body. The schema inventory and field coverage matrix are audit aids, not the runtime allowlist for same-format traffic. + +## Canonical Same-Format Roundtrip + +Canonical same-format roundtrip is only a test/audit mode: + +```text +source format -> Canonical -> same source format +``` + +Required behavior: + +- JSON-normalized equality, ignoring object field order and whitespace. +- Field values, array order, unknown fields, extension namespaces, and unknown enum strings must be preserved. +- This path may parse and emit; it is not the runtime path. +- Unknown provider fields are carried in provider extension namespaces and replayed when emitting the same provider format. + +## Cross-Format Conversion + +Cross-format conversion is strict: + +```text +source format -> Canonical -> target format +``` + +Required behavior: + +- Emit only fields valid for the target provider format. +- Map provider-specific enum values through explicit provider enum types. +- Preserve source fields only when the target has an equivalent field or documented extension passthrough. +- Fail closed with `FormatError::UnauditedField`, `FormatError::LossyConversionBlocked`, `FormatError::UnsupportedField`, `FormatError::InvalidEnumValue`, or `FormatError::InvalidTargetField` when no lossless mapping exists. +- Do not use `None` or silent omission to represent conversion failure. +- Newly added provider fields follow the same rule as other unknown fields: preserve same-format, fail closed cross-format with `UnauditedField`. A code change is required only when Aether intentionally supports a new cross-format semantic mapping. + +## Pure Conversion Interface + +Pure conversion lives in `crates/aether-ai-formats` and is limited to: + +- parse +- emit +- provider-specific field/enum mapping +- `ConversionReport` + +Pure conversion must not: + +- override `model` +- add, remove, or force `stream` +- apply body rules +- apply model directives +- read the original request body to patch missing target fields +- perform provider transport policy edits + +Current pure entrypoints: + +- `parse_request_pure` +- `emit_request_pure` +- `convert_request_pure` +- `convert_request_pure_with_context` +- `parse_response_pure` +- `emit_response_pure` +- `convert_response_pure` + +`convert_request` and `convert_response` remain legacy wrappers for existing callers that still need mapped model/report-context behavior during migration. diff --git a/docs/api/generate_format_field_coverage.py b/docs/api/generate_format_field_coverage.py new file mode 100644 index 000000000..644c1ff40 --- /dev/null +++ b/docs/api/generate_format_field_coverage.py @@ -0,0 +1,548 @@ +#!/usr/bin/env python3 +"""Generate the provider schema field coverage matrix. + +The input inventory is docs/api/provider-interface-definitions.md. Existing +coverage rows are reused so audited status/notes survive regeneration. Newly +introduced provider fields get conservative same-format/native and cross-format +fail-closed defaults until a human audits whether they deserve an explicit +mapping. +""" + +from __future__ import annotations + +import argparse +import dataclasses +from collections import Counter, defaultdict +from pathlib import Path +from typing import Iterable + + +ROOT = Path(__file__).resolve().parents[2] +DEFAULT_DEFINITIONS = ROOT / "docs/api/provider-interface-definitions.md" +DEFAULT_MATRIX = ROOT / "docs/api/format-field-coverage-matrix.md" + + +@dataclasses.dataclass(frozen=True) +class SourceField: + provider: str + schema: str + field: str + required: str + field_type: str + + +@dataclasses.dataclass(frozen=True) +class CoverageStatus: + surface: str + same_format_runtime: str + canonical_roundtrip: str + cross_format: str + notes: str + + +OPENAI_CHAT_MAPPED = { + "model", + "messages", + "max_tokens", + "max_completion_tokens", + "temperature", + "top_p", + "top_logprobs", + "tools", + "tool_choice", + "parallel_tool_calls", + "metadata", + "response_format", + "reasoning_effort", + "verbosity", + "store", + "service_tier", + "safety_identifier", + "prompt_cache_key", + "prompt_cache_retention", + "stream", +} + +OPENAI_CHAT_BLOCKED = { + "n", + "stop", + "presence_penalty", + "frequency_penalty", + "seed", + "logprobs", + "stream_options", + "user", + "function_call", + "functions", + "logit_bias", + "modalities", + "prediction", + "audio", + "web_search_options", +} + +OPENAI_RESPONSES_MAPPED = { + "model", + "input", + "instructions", + "max_output_tokens", + "temperature", + "top_p", + "top_logprobs", + "metadata", + "parallel_tool_calls", + "text", + "tools", + "tool_choice", + "reasoning", + "store", + "service_tier", + "safety_identifier", + "prompt_cache_key", + "prompt_cache_retention", +} + +OPENAI_RESPONSES_BLOCKED = { + "include", + "previous_response_id", + "truncation", + "prompt", + "conversation", + "background", + "max_tool_calls", + "user", + "context_management", + "stream", + "stream_options", +} + +CLAUDE_MAPPED_FIELDS = { + "id", + "type", + "role", + "text", + "content", + "source", + "name", + "description", + "input", + "input_schema", + "messages", + "model", + "max_tokens", + "system", + "temperature", + "top_p", + "top_k", + "stop_sequences", + "tool_choice", + "tools", + "metadata", + "thinking", + "output_config", + "usage", + "stop_reason", + "stop_sequence", +} + +CLAUDE_PROVIDER_ONLY_FIELDS = { + "cache_control", + "container", + "inference_geo", + "service_tier", + "allowed_callers", + "allowed_domains", + "blocked_domains", + "defer_loading", + "max_uses", + "strict", + "user_location", + "citations", + "context", + "title", + "file_id", + "document_index", + "document_title", + "cited_text", + "caller", +} + + +def split_markdown_row(line: str) -> list[str]: + cells: list[str] = [] + current: list[str] = [] + escaped = False + for char in line: + if char == "|" and not escaped: + cells.append("".join(current).strip()) + current.clear() + else: + current.append(char) + escaped = char == "\\" and not escaped + if escaped and char != "\\": + escaped = False + cells.append("".join(current).strip()) + return cells + + +def strip_markdown_code(value: str) -> str: + value = value.strip() + if value.startswith("`") and value.endswith("`"): + value = value[1:-1] + return value.replace("\\|", "|") + + +def escape_markdown_cell(value: str) -> str: + return value.replace("|", "\\|") + + +def parse_schema_heading(line: str) -> str | None: + if not line.startswith("### `"): + return None + rest = line[len("### `") :] + schema, _, _ = rest.partition("`") + return schema or None + + +def parse_provider_definition_fields(definitions: str) -> list[SourceField]: + provider: str | None = None + schema: str | None = None + fields: list[SourceField] = [] + + for line in definitions.splitlines(): + if line.startswith("## "): + if "OpenAI Schema" in line: + provider = "OpenAI" + elif "Claude / Anthropic TypeScript" in line: + provider = "Claude" + elif "Gemini Schema" in line: + provider = "Gemini" + else: + provider = None + schema = None + continue + + if provider is None: + continue + + if heading := parse_schema_heading(line): + schema = heading + continue + + if schema is None or not line.startswith("| `"): + continue + + cells = split_markdown_row(line) + if len(cells) < 4 or cells[2] not in {"是", "否"}: + continue + fields.append( + SourceField( + provider=provider, + schema=schema, + field=strip_markdown_code(cells[1]), + required=cells[2], + field_type=strip_markdown_code(cells[3]), + ) + ) + + return fields + + +def parse_existing_coverage( + matrix: str, +) -> tuple[dict[tuple[str, str, str], CoverageStatus], dict[tuple[str, str], list[CoverageStatus]]]: + existing: dict[tuple[str, str, str], CoverageStatus] = {} + profiles: dict[tuple[str, str], list[CoverageStatus]] = defaultdict(list) + + for line in matrix.splitlines(): + if not line.startswith("| "): + continue + cells = split_markdown_row(line) + if len(cells) < 11 or cells[1] not in {"OpenAI", "Claude", "Gemini"}: + continue + status = CoverageStatus( + surface=cells[6], + same_format_runtime=cells[7], + canonical_roundtrip=cells[8], + cross_format=cells[9], + notes=cells[10], + ) + provider = cells[1] + schema = strip_markdown_code(cells[2]) + field = strip_markdown_code(cells[3]) + existing[(provider, schema, field)] = status + profiles[(provider, schema)].append(status) + + return existing, profiles + + +def most_common(values: Iterable[str]) -> str | None: + values = list(values) + if not values: + return None + return Counter(values).most_common(1)[0][0] + + +def openai_surface(schema: str) -> str: + if "CreateChatCompletion" in schema or "ChatCompletion" in schema: + return "openai:chat standard" + if "CreateEmbedding" in schema or "Embedding" in schema: + return "openai:embedding" + if any(token in schema for token in ("CreateImage", "EditImage", "Image", "Images")): + return "openai:image native-only" + if any(token in schema for token in ("Compact", "Compaction")): + return "openai:responses:compact native-only" + if any( + token in schema + for token in ( + "Response", + "Input", + "Output", + "Tool", + "Reasoning", + "WebSearch", + "FileSearch", + "Computer", + "MCP", + "CodeInterpreter", + "Function", + "Custom", + "EasyInput", + "Prompt", + "Conversation", + "Annotation", + "Citation", + "LogProb", + "TopLogProb", + "Metadata", + "ServiceTier", + "Verbosity", + "TextResponse", + "ResponseFormat", + "Include", + "Modalities", + "ParallelToolCalls", + "StopConfiguration", + ) + ): + return "openai:responses standard" + return "openai auxiliary / not-in-conversion-surface" + + +def openai_default_status(field: SourceField, profile: list[CoverageStatus]) -> CoverageStatus: + surface = most_common(status.surface for status in profile) or openai_surface(field.schema) + if "not-in-conversion-surface" in surface or "native-only" in surface: + return CoverageStatus( + surface=surface, + same_format_runtime="native", + canonical_roundtrip="not-in-conversion-surface", + cross_format="not-in-conversion-surface", + notes="not part of current canonical cross-format conversion; same-format runtime path remains provider-native when routed directly", + ) + if field.schema == "CreateChatCompletionRequest": + if field.field in OPENAI_CHAT_MAPPED: + return CoverageStatus( + surface=surface, + same_format_runtime="native", + canonical_roundtrip="mapped", + cross_format="mapped", + notes="Chat request field maps provider-specifically; target-incompatible cases fail closed", + ) + if field.field in OPENAI_CHAT_BLOCKED: + return CoverageStatus( + surface=surface, + same_format_runtime="native", + canonical_roundtrip="extension-preserved", + cross_format="lossy-blocked", + notes="Chat-only or provider-specific field has no audited lossless target equivalent", + ) + if field.schema == "CreateResponse": + if field.field in OPENAI_RESPONSES_MAPPED: + return CoverageStatus( + surface=surface, + same_format_runtime="native", + canonical_roundtrip="mapped", + cross_format="mapped", + notes="Responses request field maps provider-specifically; target-incompatible cases fail closed", + ) + if field.field in OPENAI_RESPONSES_BLOCKED: + return CoverageStatus( + surface=surface, + same_format_runtime="native", + canonical_roundtrip="extension-preserved", + cross_format="lossy-blocked", + notes="Responses-only field has no audited lossless Chat/Claude/Gemini target equivalent", + ) + if profile: + cross_format = most_common(status.cross_format for status in profile) or "lossy-blocked" + return CoverageStatus( + surface=surface, + same_format_runtime="native", + canonical_roundtrip=most_common(status.canonical_roundtrip for status in profile) + or "extension-preserved", + cross_format=cross_format, + notes=next( + (status.notes for status in profile if status.cross_format == cross_format), + "schema-level handling inherited from audited sibling fields", + ), + ) + return CoverageStatus( + surface=surface, + same_format_runtime="native", + canonical_roundtrip="extension-preserved", + cross_format="lossy-blocked", + notes="OpenAI documented field is preserved same-format; cross-format requires explicit target mapping or fails closed", + ) + + +def claude_default_status(field: SourceField) -> CoverageStatus: + if "CountTokens" in field.schema: + return CoverageStatus( + surface="claude:messages/count_tokens native-only", + same_format_runtime="native", + canonical_roundtrip="not-in-conversion-surface", + cross_format="not-in-conversion-surface", + notes="count_tokens schemas are provider-native and outside canonical generation conversion", + ) + if field.field in CLAUDE_MAPPED_FIELDS: + return CoverageStatus( + surface="claude:messages standard", + same_format_runtime="native", + canonical_roundtrip="mapped", + cross_format="mapped/lossy-blocked", + notes="Claude field maps where canonical and target support an equivalent; otherwise conversion fails closed", + ) + if ( + field.field in CLAUDE_PROVIDER_ONLY_FIELDS + or field.field.endswith("_tokens_details") + or "cache" in field.field + ): + return CoverageStatus( + surface="claude:messages standard", + same_format_runtime="native", + canonical_roundtrip="extension-preserved", + cross_format="lossy-blocked", + notes="Claude provider-specific field is preserved same-format and blocked cross-format without an audited target equivalent", + ) + return CoverageStatus( + surface="claude:messages standard", + same_format_runtime="native", + canonical_roundtrip="extension-preserved", + cross_format="lossy-blocked", + notes="Claude nested/provider-specific field is same-format preserved; cross-format requires explicit mapping or fails closed", + ) + + +def gemini_default_status(field: SourceField, profile: list[CoverageStatus]) -> CoverageStatus: + if profile: + cross_format = most_common(status.cross_format for status in profile) or "lossy-blocked" + return CoverageStatus( + surface=most_common(status.surface for status in profile) + or "gemini:generate_content standard", + same_format_runtime="native", + canonical_roundtrip=most_common(status.canonical_roundtrip for status in profile) + or "extension-preserved", + cross_format=cross_format, + notes=next( + (status.notes for status in profile if status.cross_format == cross_format), + "Gemini field follows schema-level handling", + ), + ) + return CoverageStatus( + surface="gemini:generate_content standard", + same_format_runtime="native", + canonical_roundtrip="extension-preserved", + cross_format="lossy-blocked", + notes="Gemini documented field is preserved same-format; cross-format requires explicit mapping or fails closed", + ) + + +def default_status(field: SourceField, profile: list[CoverageStatus]) -> CoverageStatus: + if field.provider == "OpenAI": + return openai_default_status(field, profile) + if field.provider == "Claude": + return claude_default_status(field) + if field.provider == "Gemini": + return gemini_default_status(field, profile) + raise ValueError(f"unsupported provider: {field.provider}") + + +def render_matrix( + fields: list[SourceField], + existing: dict[tuple[str, str, str], CoverageStatus], + profiles: dict[tuple[str, str], list[CoverageStatus]], +) -> str: + rows: list[str] = [ + "# Format Field Coverage Matrix", + "", + "Last generated: 2026-06-03", + "", + "This file is generated from the schema inventory in `docs/api/provider-interface-definitions.md` and gives every documented schema field an explicit handling status. “处理到” here means the field is either mapped, preserved in same-format paths, rejected with a structured fail-closed error, or explicitly outside the current conversion surface. It does not mean every field can be cross-format converted.", + "", + "Provider schema updates do not require immediate conversion-code changes for runtime safety. Same-format runtime paths bypass canonical conversion, and same-format canonical roundtrip preserves provider extension fields. Cross-format conversion only enables fields with an audited semantic mapping; newly discovered or unknown provider fields default to structured fail-closed behavior until mapped.", + "", + "Regenerate with: `python3 docs/api/generate_format_field_coverage.py`.", + "", + "Statuses used in this matrix: `native`, `mapped`, `mapped/lossy-blocked`, `extension-preserved`, `unaudited`, `unsupported`, `invalid-enum`, `lossy-blocked`, `not-in-conversion-surface`.", + "", + "| Provider | Schema | Field | Required | Type | Surface | Same-Format Runtime | Canonical Roundtrip | Cross-Format | Notes |", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |", + ] + + for field in fields: + status = existing.get( + (field.provider, field.schema, field.field), + default_status(field, profiles[(field.provider, field.schema)]), + ) + rows.append( + "| " + + " | ".join( + [ + field.provider, + f"`{escape_markdown_cell(field.schema)}`", + f"`{escape_markdown_cell(field.field)}`", + field.required, + f"`{escape_markdown_cell(field.field_type)}`", + escape_markdown_cell(status.surface), + escape_markdown_cell(status.same_format_runtime), + escape_markdown_cell(status.canonical_roundtrip), + escape_markdown_cell(status.cross_format), + escape_markdown_cell(status.notes), + ] + ) + + " |" + ) + + rows.extend(["", f"Total covered schema fields: {len(fields)}."]) + return "\n".join(rows) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--definitions", type=Path, default=DEFAULT_DEFINITIONS) + parser.add_argument("--matrix", type=Path, default=DEFAULT_MATRIX) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + + definitions = args.definitions.read_text() + current_matrix = args.matrix.read_text() if args.matrix.exists() else "" + fields = parse_provider_definition_fields(definitions) + existing, profiles = parse_existing_coverage(current_matrix) + next_matrix = render_matrix(fields, existing, profiles) + + if args.check: + if current_matrix != next_matrix: + print( + f"{args.matrix} is not up to date; run " + "`python3 docs/api/generate_format_field_coverage.py`", + ) + return 1 + return 0 + + args.matrix.write_text(next_matrix) + print(f"wrote {len(fields)} field coverage rows to {args.matrix}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/api/provider-interface-definitions.md b/docs/api/provider-interface-definitions.md new file mode 100644 index 000000000..353a1b822 --- /dev/null +++ b/docs/api/provider-interface-definitions.md @@ -0,0 +1,7665 @@ +# OpenAI / Claude / Gemini 接口定义 + +生成日期:2026-06-03。 + +本文档整理 Aether 当前接入和转换矩阵实际涉及的三类 provider 接口面:OpenAI Chat Completions / Responses / Embeddings / Images,Claude Messages,以及 Gemini GenerateContent / Embeddings / Files / PredictLongRunning 相关接口。它不是三家公司所有管理类、训练类、账单类 API 的全集。 + +这是 schema inventory / audit input,不是运行时代码的字段 allowlist。Provider 官方新增字段时,同格式运行时路径仍按原始 body 透传;canonical same-format roundtrip 通过 provider extension 保留未映射字段;跨格式转换只有在存在显式语义映射时才开放,否则 fail closed。刷新本文档只用于更新审计基线和决定是否新增跨格式映射。 + +## 来源与范围 + +| Provider | 结构化来源 | 官方参考 | 本文档覆盖 | +| --- | --- | --- | --- | +| OpenAI | OpenAI OpenAPI `2.3.0` / `OpenAI API` | https://platform.openai.com/docs/api-reference | `/v1/chat/completions`, `/v1/responses`, `/v1/responses/compact`, `/v1/embeddings`, `/v1/images/*` | +| Claude / Anthropic | `anthropic-sdk-typescript` 中由 Anthropic OpenAPI 生成的 `messages.ts` | https://docs.anthropic.com/en/api/messages | `/v1/messages`, `/v1/messages/count_tokens`, Messages streaming events | +| Gemini | Google Generative Language Discovery `v1beta` / `Gemini API` | https://ai.google.dev/api | `generateContent`, `streamGenerateContent`, `embedContent`, `batchEmbedContents`, files, count tokens, predict long-running | + +结构化来源 URL: + +- OpenAI OpenAPI: https://app.stainless.com/api/spec/documented/openai/openapi.documented.yml +- Anthropic Messages SDK types: https://github.com/anthropics/anthropic-sdk-typescript/blob/main/src/resources/messages/messages.ts +- Gemini Discovery JSON: https://generativelanguage.googleapis.com/$discovery/rest?version=v1beta + +说明:字段表中的“必填”来自官方 schema 的 `required` 或 TypeScript `?` 标记;很多接口还会受到模型、账号权限、beta header、区域、Aether provider 配置和上游版本的约束。Aether 的 `/v1/rerank` 是 OpenAI/Jina compatible 兼容面,不是 OpenAI 官方 OpenAPI 中的 endpoint;它见 `docs/api/rerank.md`。 + +## Aether API Format 对应关系 + +| Aether format | Provider 原生接口 | 请求根 schema | 响应根 schema | +| --- | --- | --- | --- | +| `openai:chat` | `POST /v1/chat/completions` | `CreateChatCompletionRequest` | `CreateChatCompletionResponse` 或 `CreateChatCompletionStreamResponse` | +| `openai:responses` | `POST /v1/responses` | `CreateResponse` | `Response` 或 `ResponseStreamEvent` | +| `openai:responses:compact` | `POST /v1/responses/compact` | `CompactResponseMethodPublicBody` | `CompactResource` | +| `openai:embedding` | `POST /v1/embeddings` | `CreateEmbeddingRequest` | `CreateEmbeddingResponse` | +| `openai:image` | `POST /v1/images/generations`, `/edits`, `/variations` | `CreateImageRequest`, `CreateImageEditRequest`, `CreateImageVariationRequest` | `ImagesResponse` 或 image stream event | +| `claude:messages` | `POST /v1/messages` | `MessageCreateParams` | `Message` 或 `RawMessageStreamEvent` | +| `gemini:generate_content` | `models/{model}:generateContent` / `:streamGenerateContent` | `GenerateContentRequest` | `GenerateContentResponse` | +| `gemini:embedding` | `models/{model}:embedContent` / `:batchEmbedContents` | `EmbedContentRequest` / `BatchEmbedContentsRequest` | `EmbedContentResponse` / `BatchEmbedContentsResponse` | + +## OpenAI Endpoints + +| Method | Path | Request content type | Request schema | Response schema | +| --- | --- | --- | --- | --- | +| POST | `/chat/completions` | `application/json` | `CreateChatCompletionRequest` | `CreateChatCompletionResponse` / `CreateChatCompletionStreamResponse` | +| POST | `/responses` | `application/json` | `CreateResponse` | `Response` / `ResponseStreamEvent` | +| POST | `/responses/compact` | `application/json, application/x-www-form-urlencoded` | `CompactResponseMethodPublicBody` | `CompactResource` | +| POST | `/embeddings` | `application/json` | `CreateEmbeddingRequest` | `CreateEmbeddingResponse` | +| POST | `/images/generations` | `application/json` | `CreateImageRequest` | `ImagesResponse` / `ImageGenStreamEvent` | +| POST | `/images/edits` | `multipart/form-data, application/json` | `CreateImageEditRequest` / `EditImageBodyJsonParam` | `ImagesResponse` / `ImageEditStreamEvent` | +| POST | `/images/variations` | `multipart/form-data` | `CreateImageVariationRequest` | `ImagesResponse` | + +## OpenAI Schema 字段表 + +以下 schema 从上述 OpenAI endpoint 根 schema 递归引用得到,共 351 个。 + +### `AdditionalTools` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `id` | 是 | `string` | - | The unique ID of the additional tools item. | +| `role` | 是 | `MessageRole` | - | The role that provided the additional tools. | +| `tools` | 是 | `array` | - | The additional tool definitions made available at this item. | +| `type` | 是 | `string` | `additional_tools` | The type of the item. Always additional_tools. | + +### `AdditionalToolsItemParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `id` | 否 | `string \| null` | - | - | +| `role` | 是 | `string` | `developer` | The role that provided the additional tools. Only developer is supported. | +| `tools` | 是 | `array` | - | A list of additional tools made available at this item. | +| `type` | 是 | `string` | `additional_tools` | The item type. Always additional_tools. | + +### `Annotation` + +| 项 | 值 | +| --- | --- | +| 类型 | `FileCitationBody \| UrlCitationBody \| ContainerFileCitationBody \| FilePath` | +| 说明 | An annotation that applies to a span of output text. | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `FileCitationBody` | - | +| 2 | `UrlCitationBody` | - | +| 3 | `ContainerFileCitationBody` | - | +| 4 | `FilePath` | - | + +### `ApplyPatchCallOutputStatus` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `ApplyPatchCallOutputStatusParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | Outcome values reported for apply_patch tool call outputs. | + +### `ApplyPatchCallStatus` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `ApplyPatchCallStatusParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | Status values reported for apply_patch tool calls. | + +### `ApplyPatchCreateFileOperation` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Instruction describing how to create a file via the apply_patch tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `diff` | 是 | `string` | - | Diff to apply. | +| `path` | 是 | `string` | - | Path of the file to create. | +| `type` | 是 | `string` | `create_file` | Create a new file with the provided diff. | + +### `ApplyPatchCreateFileOperationParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Instruction for creating a new file via the apply_patch tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `diff` | 是 | `string` | - | Unified diff content to apply when creating the file. | +| `path` | 是 | `string` | - | Path of the file to create relative to the workspace root. | +| `type` | 是 | `string` | `create_file` | The operation type. Always create_file. | + +### `ApplyPatchDeleteFileOperation` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Instruction describing how to delete a file via the apply_patch tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `path` | 是 | `string` | - | Path of the file to delete. | +| `type` | 是 | `string` | `delete_file` | Delete the specified file. | + +### `ApplyPatchDeleteFileOperationParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Instruction for deleting an existing file via the apply_patch tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `path` | 是 | `string` | - | Path of the file to delete relative to the workspace root. | +| `type` | 是 | `string` | `delete_file` | The operation type. Always delete_file. | + +### `ApplyPatchOperationParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `ApplyPatchCreateFileOperationParam \| ApplyPatchDeleteFileOperationParam \| ApplyPatchUpdateFileOperationParam` | +| 说明 | One of the create_file, delete_file, or update_file operations supplied to the apply_patch tool. | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ApplyPatchCreateFileOperationParam` | - | +| 2 | `ApplyPatchDeleteFileOperationParam` | - | +| 3 | `ApplyPatchUpdateFileOperationParam` | - | + +### `ApplyPatchToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool call that applies file diffs by creating, deleting, or updating files. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | The unique ID of the apply patch tool call generated by the model. | +| `created_by` | 否 | `string` | - | The ID of the entity that created this tool call. | +| `id` | 是 | `string` | - | The unique ID of the apply patch tool call. Populated when this item is returned via API. | +| `operation` | 是 | `ApplyPatchCreateFileOperation \| ApplyPatchDeleteFileOperation \| ApplyPatchUpdateFileOperation` | - | One of the create_file, delete_file, or update_file operations applied via apply_patch. | +| `status` | 是 | `ApplyPatchCallStatus` | - | The status of the apply patch tool call. One of in_progress or completed. | +| `type` | 是 | `string` | `apply_patch_call` | The type of the item. Always apply_patch_call. | + +### `ApplyPatchToolCallItemParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool call representing a request to create, delete, or update files using diff patches. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | The unique ID of the apply patch tool call generated by the model. | +| `id` | 否 | `string \| null` | - | - | +| `operation` | 是 | `ApplyPatchOperationParam` | - | The specific create, delete, or update instruction for the apply_patch tool call. | +| `status` | 是 | `ApplyPatchCallStatusParam` | - | The status of the apply patch tool call. One of in_progress or completed. | +| `type` | 是 | `string` | `apply_patch_call` | The type of the item. Always apply_patch_call. | + +### `ApplyPatchToolCallOutput` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The output emitted by an apply patch tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | The unique ID of the apply patch tool call generated by the model. | +| `created_by` | 否 | `string` | - | The ID of the entity that created this tool call output. | +| `id` | 是 | `string` | - | The unique ID of the apply patch tool call output. Populated when this item is returned via API. | +| `output` | 否 | `string \| null` | - | - | +| `status` | 是 | `ApplyPatchCallOutputStatus` | - | The status of the apply patch tool call output. One of completed or failed. | +| `type` | 是 | `string` | `apply_patch_call_output` | The type of the item. Always apply_patch_call_output. | + +### `ApplyPatchToolCallOutputItemParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The streamed output emitted by an apply patch tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | The unique ID of the apply patch tool call generated by the model. | +| `id` | 否 | `string \| null` | - | - | +| `output` | 否 | `string \| null` | - | - | +| `status` | 是 | `ApplyPatchCallOutputStatusParam` | - | The status of the apply patch tool call output. One of completed or failed. | +| `type` | 是 | `string` | `apply_patch_call_output` | The type of the item. Always apply_patch_call_output. | + +### `ApplyPatchToolParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Allows the assistant to create, delete, or update files using unified diffs. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `apply_patch` | The type of the tool. Always apply_patch. | + +### `ApplyPatchUpdateFileOperation` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Instruction describing how to update a file via the apply_patch tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `diff` | 是 | `string` | - | Diff to apply. | +| `path` | 是 | `string` | - | Path of the file to update. | +| `type` | 是 | `string` | `update_file` | Update an existing file with the provided diff. | + +### `ApplyPatchUpdateFileOperationParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Instruction for updating an existing file via the apply_patch tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `diff` | 是 | `string` | - | Unified diff content to apply to the existing file. | +| `path` | 是 | `string` | - | Path of the file to update relative to the workspace root. | +| `type` | 是 | `string` | `update_file` | The operation type. Always update_file. | + +### `ApproximateLocation` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `city` | 否 | `string \| null` | - | - | +| `country` | 否 | `string \| null` | - | - | +| `region` | 否 | `string \| null` | - | - | +| `timezone` | 否 | `string \| null` | - | - | +| `type` | 是 | `string` | `approximate` | The type of location approximation. Always approximate. | + +### `AutoCodeInterpreterToolParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Configuration for a code interpreter container. Optionally specify the IDs of the files to run the code on. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `file_ids` | 否 | `array` | - | An optional list of uploaded files to make available to your code. | +| `memory_limit` | 否 | `ContainerMemoryLimit \| null` | - | - | +| `network_policy` | 否 | `ContainerNetworkPolicyDisabledParam \| ContainerNetworkPolicyAllowlistParam` | - | Network access policy for the container. | +| `type` | 是 | `string` | `auto` | Always auto. | + +### `ChatCompletionAllowedTools` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Constrains the tools available to the model to a pre-defined set. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `mode` | 是 | `string` | `auto`, `required` | Constrains the tools available to the model to a pre-defined set. auto allows the model to pick from among the allowed tools and generate a message. required requires the model to… | +| `tools` | 是 | `array` | - | A list of tool definitions that the model should be allowed to call. For the Chat Completions API, the list of tool definitions might look like: | + +### `ChatCompletionAllowedToolsChoice` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Constrains the tools available to the model to a pre-defined set. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `allowed_tools` | 是 | `ChatCompletionAllowedTools` | - | - | +| `type` | 是 | `string` | `allowed_tools` | Allowed tool configuration type. Always allowed_tools. | + +### `ChatCompletionFunctionCallOption` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Specifying a particular function via {"name": "my_function"} forces the model to call that function. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `name` | 是 | `string` | - | The name of the function to call. | + +### `ChatCompletionFunctions` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `description` | 否 | `string` | - | A description of what the function does, used by the model to choose when and how to call the function. | +| `name` | 是 | `string` | - | The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. | +| `parameters` | 否 | `FunctionParameters` | - | - | + +### `ChatCompletionMessageCustomToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A call to a custom tool created by the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `custom` | 是 | `object` | - | The custom tool that the model called. | +| `id` | 是 | `string` | - | The ID of the tool call. | +| `type` | 是 | `string` | `custom` | The type of the tool. Always custom. | + +### `ChatCompletionMessageToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A call to a function tool created by the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `function` | 是 | `object` | - | The function that the model called. | +| `id` | 是 | `string` | - | The ID of the tool call. | +| `type` | 是 | `string` | `function` | The type of the tool. Currently, only function is supported. | + +### `ChatCompletionMessageToolCallChunk` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `function` | 否 | `object` | - | - | +| `id` | 否 | `string` | - | The ID of the tool call. | +| `index` | 是 | `integer` | - | - | +| `type` | 否 | `string` | `function` | The type of the tool. Currently, only function is supported. | + +### `ChatCompletionMessageToolCalls` + +| 项 | 值 | +| --- | --- | +| 类型 | `array` | +| 说明 | The tool calls generated by the model, such as function calls. | + +### `ChatCompletionNamedToolChoice` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Specifies a tool the model should use. Use to force the model to call a specific function. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `function` | 是 | `object` | - | - | +| `type` | 是 | `string` | `function` | For function calling, the type is always function. | + +### `ChatCompletionNamedToolChoiceCustom` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Specifies a tool the model should use. Use to force the model to call a specific custom tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `custom` | 是 | `object` | - | - | +| `type` | 是 | `string` | `custom` | For custom tool calling, the type is always custom. | + +### `ChatCompletionRequestAssistantMessage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Messages sent by the model in response to user messages. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `audio` | 否 | `object \| null` | - | - | +| `content` | 否 | `string \| array \| null` | - | - | +| `function_call` | 否 | `object \| null` | - | - | +| `name` | 否 | `string` | - | An optional name for the participant. Provides the model information to differentiate between participants of the same role. | +| `refusal` | 否 | `string \| null` | - | - | +| `role` | 是 | `string` | `assistant` | The role of the messages author, in this case assistant. | +| `tool_calls` | 否 | `ChatCompletionMessageToolCalls` | - | - | + +### `ChatCompletionRequestAssistantMessageContentPart` + +| 项 | 值 | +| --- | --- | +| 类型 | `ChatCompletionRequestMessageContentPartText \| ChatCompletionRequestMessageContentPartRefusal` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ChatCompletionRequestMessageContentPartText` | - | +| 2 | `ChatCompletionRequestMessageContentPartRefusal` | - | + +### `ChatCompletionRequestDeveloperMessage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Developer-provided instructions that the model should follow, regardless of messages sent by the user. With o1 models and newer, developer messages replace the previous system mes… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 是 | `string \| array` | - | The contents of the developer message. | +| `name` | 否 | `string` | - | An optional name for the participant. Provides the model information to differentiate between participants of the same role. | +| `role` | 是 | `string` | `developer` | The role of the messages author, in this case developer. | + +### `ChatCompletionRequestFunctionMessage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 是 | `string \| null` | - | - | +| `name` | 是 | `string` | - | The name of the function to call. | +| `role` | 是 | `string` | `function` | The role of the messages author, in this case function. | + +### `ChatCompletionRequestMessage` + +| 项 | 值 | +| --- | --- | +| 类型 | `ChatCompletionRequestDeveloperMessage \| ChatCompletionRequestSystemMessage \| ChatCompletionRequestUserMessage \| ChatCompletionRequestAssistantMessage \| ChatCompletionRequestToolMessage \| ChatCompletionRequestFunctionMessage` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ChatCompletionRequestDeveloperMessage` | - | +| 2 | `ChatCompletionRequestSystemMessage` | - | +| 3 | `ChatCompletionRequestUserMessage` | - | +| 4 | `ChatCompletionRequestAssistantMessage` | - | +| 5 | `ChatCompletionRequestToolMessage` | - | +| 6 | `ChatCompletionRequestFunctionMessage` | - | + +### `ChatCompletionRequestMessageContentPartAudio` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Learn about [audio inputs](/docs/guides/audio). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `input_audio` | 是 | `object` | - | - | +| `type` | 是 | `string` | `input_audio` | The type of the content part. Always input_audio. | + +### `ChatCompletionRequestMessageContentPartFile` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Learn about [file inputs](/docs/guides/text) for text generation. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `file` | 是 | `object` | - | - | +| `type` | 是 | `string` | `file` | The type of the content part. Always file. | + +### `ChatCompletionRequestMessageContentPartImage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Learn about [image inputs](/docs/guides/vision). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `image_url` | 是 | `object` | - | - | +| `type` | 是 | `string` | `image_url` | The type of the content part. | + +### `ChatCompletionRequestMessageContentPartRefusal` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `refusal` | 是 | `string` | - | The refusal message generated by the model. | +| `type` | 是 | `string` | `refusal` | The type of the content part. | + +### `ChatCompletionRequestMessageContentPartText` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Learn about [text inputs](/docs/guides/text-generation). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `text` | 是 | `string` | - | The text content. | +| `type` | 是 | `string` | `text` | The type of the content part. | + +### `ChatCompletionRequestSystemMessage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Developer-provided instructions that the model should follow, regardless of messages sent by the user. With o1 models and newer, use developer messages for this purpose instead. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 是 | `string \| array` | - | The contents of the system message. | +| `name` | 否 | `string` | - | An optional name for the participant. Provides the model information to differentiate between participants of the same role. | +| `role` | 是 | `string` | `system` | The role of the messages author, in this case system. | + +### `ChatCompletionRequestSystemMessageContentPart` + +| 项 | 值 | +| --- | --- | +| 类型 | `ChatCompletionRequestMessageContentPartText` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ChatCompletionRequestMessageContentPartText` | - | + +### `ChatCompletionRequestToolMessage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 是 | `string \| array` | - | The contents of the tool message. | +| `role` | 是 | `string` | `tool` | The role of the messages author, in this case tool. | +| `tool_call_id` | 是 | `string` | - | Tool call that this message is responding to. | + +### `ChatCompletionRequestToolMessageContentPart` + +| 项 | 值 | +| --- | --- | +| 类型 | `ChatCompletionRequestMessageContentPartText` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ChatCompletionRequestMessageContentPartText` | - | + +### `ChatCompletionRequestUserMessage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Messages sent by an end user, containing prompts or additional context information. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 是 | `string \| array` | - | The contents of the user message. | +| `name` | 否 | `string` | - | An optional name for the participant. Provides the model information to differentiate between participants of the same role. | +| `role` | 是 | `string` | `user` | The role of the messages author, in this case user. | + +### `ChatCompletionRequestUserMessageContentPart` + +| 项 | 值 | +| --- | --- | +| 类型 | `ChatCompletionRequestMessageContentPartText \| ChatCompletionRequestMessageContentPartImage \| ChatCompletionRequestMessageContentPartAudio \| ChatCompletionRequestMessageContentPartFile` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ChatCompletionRequestMessageContentPartText` | - | +| 2 | `ChatCompletionRequestMessageContentPartImage` | - | +| 3 | `ChatCompletionRequestMessageContentPartAudio` | - | +| 4 | `ChatCompletionRequestMessageContentPartFile` | - | + +### `ChatCompletionResponseMessage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A chat completion message generated by the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `annotations` | 否 | `array` | - | Annotations for the message, when applicable, as when using the [web search tool](/docs/guides/tools-web-search?api-mode=chat). | +| `audio` | 否 | `object \| null` | - | - | +| `content` | 是 | `string \| null` | - | - | +| `function_call` | 否 | `object` | - | Deprecated and replaced by tool_calls. The name and arguments of a function that should be called, as generated by the model. | +| `refusal` | 是 | `string \| null` | - | - | +| `role` | 是 | `string` | `assistant` | The role of the author of this message. | +| `tool_calls` | 否 | `ChatCompletionMessageToolCalls` | - | - | + +### `ChatCompletionStreamOptions` + +| 项 | 值 | +| --- | --- | +| 类型 | `object \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `object` | Options for streaming response. Only set this when you set stream: true. | +| 2 | `null` | - | + +### `ChatCompletionStreamResponseDelta` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A chat completion delta generated by streamed model responses. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 否 | `string \| null` | - | - | +| `function_call` | 否 | `object` | - | Deprecated and replaced by tool_calls. The name and arguments of a function that should be called, as generated by the model. | +| `refusal` | 否 | `string \| null` | - | - | +| `role` | 否 | `string` | `developer`, `system`, `user`, `assistant`, `tool` | The role of the author of this message. | +| `tool_calls` | 否 | `array` | - | - | + +### `ChatCompletionTokenLogprob` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `bytes` | 是 | `array \| null` | - | - | +| `logprob` | 是 | `number` | - | The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value -9999.0 is used to signify that the token is very unlikely. | +| `token` | 是 | `string` | - | The token. | +| `top_logprobs` | 是 | `array` | - | List of the most likely tokens and their log probability, at this token position. The number of entries may be fewer than the requested top_logprobs. | + +### `ChatCompletionTool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A function tool that can be used to generate a response. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `function` | 是 | `FunctionObject` | - | - | +| `type` | 是 | `string` | `function` | The type of the tool. Currently, only function is supported. | + +### `ChatCompletionToolChoiceOption` + +| 项 | 值 | +| --- | --- | +| 类型 | `string \| ChatCompletionAllowedToolsChoice \| ChatCompletionNamedToolChoice \| ChatCompletionNamedToolChoiceCustom` | +| 说明 | Controls which (if any) tool is called by the model. none means the model will not call any tool and instead generates a message. auto means the model can pick between generating … | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `string` | none means the model will not call any tool and instead generates a message. auto means the model can pick between generating a message or calling one or more tools. required mean… | +| 2 | `ChatCompletionAllowedToolsChoice` | - | +| 3 | `ChatCompletionNamedToolChoice` | - | +| 4 | `ChatCompletionNamedToolChoiceCustom` | - | + +### `ClickButtonType` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `ClickParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A click action. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `button` | 是 | `ClickButtonType` | - | Indicates which mouse button was pressed during the click. One of left, right, wheel, back, or forward. | +| `keys` | 否 | `array \| null` | - | - | +| `type` | 是 | `string` | `click` | Specifies the event type. For a click action, this property is always click. | +| `x` | 是 | `integer` | - | The x-coordinate where the click occurred. | +| `y` | 是 | `integer` | - | The y-coordinate where the click occurred. | + +### `CodeInterpreterOutputImage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The image output from the code interpreter. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `image` | The type of the output. Always image. | +| `url` | 是 | `string(uri)` | - | The URL of the image output from the code interpreter. | + +### `CodeInterpreterOutputLogs` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The logs output from the code interpreter. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `logs` | 是 | `string` | - | The logs output from the code interpreter. | +| `type` | 是 | `string` | `logs` | The type of the output. Always logs. | + +### `CodeInterpreterTool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool that runs Python code to help generate a response to a prompt. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `container` | 是 | `string \| AutoCodeInterpreterToolParam` | - | The code interpreter container. Can be a container ID or an object that specifies uploaded file IDs to make available to your code, along with an optional memory_limit setting. | +| `type` | 是 | `string` | `code_interpreter` | The type of the code interpreter tool. Always code_interpreter. | + +### `CodeInterpreterToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool call to run code. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `code` | 是 | `string \| null` | - | - | +| `container_id` | 是 | `string` | - | The ID of the container used to run the code. | +| `id` | 是 | `string` | - | The unique ID of the code interpreter tool call. | +| `outputs` | 是 | `array \| null` | - | - | +| `status` | 是 | `string` | `in_progress`, `completed`, `incomplete`, `interpreting`, `failed` | The status of the code interpreter tool call. Valid values are in_progress, completed, incomplete, interpreting, and failed. | +| `type` | 是 | `string` | `code_interpreter_call` | The type of the code interpreter tool call. Always code_interpreter_call. | + +### `CompactResource` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `created_at` | 是 | `integer(unixtime)` | - | Unix timestamp (in seconds) when the compacted conversation was created. | +| `id` | 是 | `string` | - | The unique identifier for the compacted response. | +| `object` | 是 | `string` | `response.compaction` | The object type. Always response.compaction. | +| `output` | 是 | `array` | - | The compacted list of output items. | +| `usage` | 是 | `ResponseUsage` | - | Token accounting for the compaction pass, including cached, reasoning, and total tokens. | + +### `CompactResponseMethodPublicBody` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `input` | 否 | `string \| array \| null` | - | - | +| `instructions` | 否 | `string \| null` | - | - | +| `model` | 是 | `ModelIdsCompaction` | - | - | +| `previous_response_id` | 否 | `string \| null` | - | - | +| `prompt_cache_key` | 否 | `string \| null` | - | - | +| `prompt_cache_retention` | 否 | `PromptCacheRetentionEnum \| null` | - | - | +| `service_tier` | 否 | `ServiceTierEnum \| null` | - | - | + +### `CompactionBody` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A compaction item generated by the [v1/responses/compact API](/docs/api-reference/responses/compact). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `created_by` | 否 | `string` | - | The identifier of the actor that created the item. | +| `encrypted_content` | 是 | `string` | - | The encrypted content that was produced by compaction. | +| `id` | 是 | `string` | - | The unique ID of the compaction item. | +| `type` | 是 | `string` | `compaction` | The type of the item. Always compaction. | + +### `CompactionSummaryItemParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A compaction item generated by the [v1/responses/compact API](/docs/api-reference/responses/compact). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `encrypted_content` | 是 | `string` | - | The encrypted content of the compaction summary. | +| `id` | 否 | `string \| null` | - | - | +| `type` | 是 | `string` | `compaction` | The type of the item. Always compaction. | + +### `CompactionTriggerItemParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Compacts the current context. Must be the final input item. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `compaction_trigger` | The type of the item. Always compaction_trigger. | + +### `ComparisonFilter` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A filter used to compare a specified attribute key to a given value using a defined comparison operation. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `key` | 是 | `string` | - | The key to compare against the value. | +| `type` | 是 | `string` | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin` | Specifies the comparison operator: eq, ne, gt, gte, lt, lte, in, nin. - eq: equals - ne: not equal - gt: greater than - gte: greater than or equal - lt: less than - lte: less than… | +| `value` | 是 | `string \| number \| boolean \| array` | - | The value to compare against the attribute key; supports string, number, or boolean types. | + +### `CompletionUsage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Usage statistics for the completion request. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `completion_tokens` | 是 | `integer` | - | Number of tokens in the generated completion. | +| `completion_tokens_details` | 否 | `object` | - | Breakdown of tokens used in a completion. | +| `prompt_tokens` | 是 | `integer` | - | Number of tokens in the prompt. | +| `prompt_tokens_details` | 否 | `object` | - | Breakdown of tokens used in the prompt. | +| `total_tokens` | 是 | `integer` | - | Total number of tokens used in the request (prompt + completion). | + +### `CompoundFilter` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Combine multiple filters using and or or. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `filters` | 是 | `array` | - | Array of filters to combine. Items can be ComparisonFilter or CompoundFilter. | +| `type` | 是 | `string` | `and`, `or` | Type of operation: and or or. | + +### `ComputerAction` + +| 项 | 值 | +| --- | --- | +| 类型 | `ClickParam \| DoubleClickAction \| DragParam \| KeyPressAction \| MoveParam \| ScreenshotParam \| ScrollParam \| TypeParam … (+1)` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ClickParam` | - | +| 2 | `DoubleClickAction` | - | +| 3 | `DragParam` | - | +| 4 | `KeyPressAction` | - | +| 5 | `MoveParam` | - | +| 6 | `ScreenshotParam` | - | +| 7 | `ScrollParam` | - | +| 8 | `TypeParam` | - | +| 9 | `WaitParam` | - | + +### `ComputerActionList` + +| 项 | 值 | +| --- | --- | +| 类型 | `array` | +| 说明 | Flattened batched actions for computer_use. Each action includes an type discriminator and action-specific fields. | + +### `ComputerCallOutputItemParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The output of a computer tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `acknowledged_safety_checks` | 否 | `array \| null` | - | - | +| `call_id` | 是 | `string` | - | The ID of the computer tool call that produced the output. | +| `id` | 否 | `string \| null` | - | - | +| `output` | 是 | `ComputerScreenshotImage` | - | - | +| `status` | 否 | `FunctionCallItemStatus \| null` | - | - | +| `type` | 是 | `string` | `computer_call_output` | The type of the computer tool call output. Always computer_call_output. | + +### `ComputerCallOutputStatus` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `ComputerCallSafetyCheckParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A pending safety check for the computer call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `code` | 否 | `string \| null` | - | - | +| `id` | 是 | `string` | - | The ID of the pending safety check. | +| `message` | 否 | `string \| null` | - | - | + +### `ComputerEnvironment` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `ComputerScreenshotContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A screenshot of a computer. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `detail` | 是 | `ImageDetail` | - | The detail level of the screenshot image to be sent to the model. One of high, low, auto, or original. Defaults to auto. | +| `file_id` | 是 | `string \| null` | - | - | +| `image_url` | 是 | `string(uri) \| null` | - | - | +| `type` | 是 | `string` | `computer_screenshot` | Specifies the event type. For a computer screenshot, this property is always set to computer_screenshot. | + +### `ComputerScreenshotImage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A computer screenshot image used with the computer use tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `file_id` | 否 | `string` | - | The identifier of an uploaded file that contains the screenshot. | +| `image_url` | 否 | `string(uri)` | - | The URL of the screenshot image. | +| `type` | 是 | `string` | `computer_screenshot` | Specifies the event type. For a computer screenshot, this property is always set to computer_screenshot. | + +### `ComputerTool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool that controls a virtual computer. Learn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `computer` | The type of the computer tool. Always computer. | + +### `ComputerToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool call to a computer use tool. See the [computer use guide](/docs/guides/tools-computer-use) for more information. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `action` | 否 | `ComputerAction` | - | - | +| `actions` | 否 | `ComputerActionList` | - | - | +| `call_id` | 是 | `string` | - | An identifier used when responding to the tool call with output. | +| `id` | 是 | `string` | - | The unique ID of the computer call. | +| `pending_safety_checks` | 是 | `array` | - | The pending safety checks for the computer call. | +| `status` | 是 | `string` | `in_progress`, `completed`, `incomplete` | The status of the item. One of in_progress, completed, or incomplete. Populated when items are returned via API. | +| `type` | 是 | `string` | `computer_call` | The type of the computer call. Always computer_call. | + +### `ComputerToolCallOutput` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The output of a computer tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `acknowledged_safety_checks` | 否 | `array` | - | The safety checks reported by the API that have been acknowledged by the developer. | +| `call_id` | 是 | `string` | - | The ID of the computer tool call that produced the output. | +| `id` | 否 | `string` | - | The ID of the computer tool call output. | +| `output` | 是 | `ComputerScreenshotImage` | - | - | +| `status` | 否 | `string` | `in_progress`, `completed`, `incomplete` | The status of the message input. One of in_progress, completed, or incomplete. Populated when input items are returned via API. | +| `type` | 是 | `string` | `computer_call_output` | The type of the computer tool call output. Always computer_call_output. | + +### `ComputerToolCallOutputResource` + +| 项 | 值 | +| --- | --- | +| 类型 | `ComputerToolCallOutput & object` | +| 说明 | - | +| 组合 | `allOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ComputerToolCallOutput` | - | +| 2 | `object` | - | + +#### allOf 展开字段 + +| 字段 | 必填 | 类型 | 枚举/常量 | 来源 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `acknowledged_safety_checks` | 否 | `array` | - | `ComputerToolCallOutput` | The safety checks reported by the API that have been acknowledged by the developer. | +| `call_id` | 是 | `string` | - | `ComputerToolCallOutput` | The ID of the computer tool call that produced the output. | +| `created_by` | 否 | `string` | - | `ComputerToolCallOutputResource.allOf[2]` | The identifier of the actor that created the item. | +| `id` | 是 | `string` | - | `ComputerToolCallOutput` | The ID of the computer tool call output. | +| `output` | 是 | `ComputerScreenshotImage` | - | `ComputerToolCallOutput` | - | +| `status` | 是 | `string` | `in_progress`, `completed`, `incomplete` | `ComputerToolCallOutput` | The status of the message input. One of in_progress, completed, or incomplete. Populated when input items are returned via API. | +| `type` | 是 | `string` | `computer_call_output` | `ComputerToolCallOutput` | The type of the computer tool call output. Always computer_call_output. | + +### `ComputerUsePreviewTool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool that controls a virtual computer. Learn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `display_height` | 是 | `integer` | - | The height of the computer display. | +| `display_width` | 是 | `integer` | - | The width of the computer display. | +| `environment` | 是 | `ComputerEnvironment` | - | The type of computer environment to control. | +| `type` | 是 | `string` | `computer_use_preview` | The type of the computer use tool. Always computer_use_preview. | + +### `ContainerAutoParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `file_ids` | 否 | `array` | - | An optional list of uploaded files to make available to your code. | +| `memory_limit` | 否 | `ContainerMemoryLimit \| null` | - | - | +| `network_policy` | 否 | `ContainerNetworkPolicyDisabledParam \| ContainerNetworkPolicyAllowlistParam` | - | Network access policy for the container. | +| `skills` | 否 | `array` | - | An optional list of skills referenced by id or inline data. | +| `type` | 是 | `string` | `container_auto` | Automatically creates a container for this request | + +### `ContainerFileCitationBody` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A citation for a container file used to generate a model response. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `container_id` | 是 | `string` | - | The ID of the container file. | +| `end_index` | 是 | `integer` | - | The index of the last character of the container file citation in the message. | +| `file_id` | 是 | `string` | - | The ID of the file. | +| `filename` | 是 | `string` | - | The filename of the container file cited. | +| `start_index` | 是 | `integer` | - | The index of the first character of the container file citation in the message. | +| `type` | 是 | `string` | `container_file_citation` | The type of the container file citation. Always container_file_citation. | + +### `ContainerMemoryLimit` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `ContainerNetworkPolicyAllowlistParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `allowed_domains` | 是 | `array` | - | A list of allowed domains when type is allowlist. | +| `domain_secrets` | 否 | `array` | - | Optional domain-scoped secrets for allowlisted domains. | +| `type` | 是 | `string` | `allowlist` | Allow outbound network access only to specified domains. Always allowlist. | + +### `ContainerNetworkPolicyDisabledParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `disabled` | Disable outbound network access. Always disabled. | + +### `ContainerNetworkPolicyDomainSecretParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `domain` | 是 | `string` | - | The domain associated with the secret. | +| `name` | 是 | `string` | - | The name of the secret to inject for the domain. | +| `value` | 是 | `string` | - | The secret value to inject for the domain. | + +### `ContainerReferenceParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `container_id` | 是 | `string` | - | The ID of the referenced container. | +| `type` | 是 | `string` | `container_reference` | References a container created with the /v1/containers endpoint | + +### `ContainerReferenceResource` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Represents a container created with /v1/containers. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `container_id` | 是 | `string` | - | - | +| `type` | 是 | `string` | `container_reference` | The environment type. Always container_reference. | + +### `ContextManagementParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `compact_threshold` | 否 | `integer \| null` | - | - | +| `type` | 是 | `string` | - | The context management entry type. Currently only 'compaction' is supported. | + +### `Conversation-2` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The conversation that this response belonged to. Input items and output items from this response were automatically added to this conversation. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `id` | 是 | `string` | - | The unique ID of the conversation that this response was associated with. | + +### `ConversationParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `string \| ConversationParam-2` | +| 说明 | The conversation that this response belongs to. Items from this conversation are prepended to input_items for this response request. Input items and output items from this respons… | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `string` | The unique ID of the conversation. | +| 2 | `ConversationParam-2` | - | + +### `ConversationParam-2` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The conversation that this response belongs to. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `id` | 是 | `string` | - | The unique ID of the conversation. | + +### `CoordParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An x/y coordinate pair, e.g. { x: 100, y: 200 }. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `x` | 是 | `integer` | - | The x-coordinate. | +| `y` | 是 | `integer` | - | The y-coordinate. | + +### `CreateChatCompletionRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `CreateModelResponseProperties & object` | +| 说明 | - | +| 组合 | `allOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `CreateModelResponseProperties` | - | +| 2 | `object` | - | + +#### allOf 展开字段 + +| 字段 | 必填 | 类型 | 枚举/常量 | 来源 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `audio` | 否 | `object` | - | `CreateChatCompletionRequest.allOf[2]` | Parameters for audio output. Required when audio output is requested with modalities: ["audio"]. [Learn more](/docs/guides/audio). | +| `frequency_penalty` | 否 | `number` | - | `CreateChatCompletionRequest.allOf[2]` | Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line ve… | +| `function_call` | 否 | `string \| ChatCompletionFunctionCallOption` | - | `CreateChatCompletionRequest.allOf[2]` | Deprecated in favor of tool_choice. Controls which (if any) function is called by the model. none means the model will not call a function and instead generates a message. auto me… | +| `functions` | 否 | `array` | - | `CreateChatCompletionRequest.allOf[2]` | Deprecated in favor of tools. A list of functions the model may generate JSON inputs for. | +| `logit_bias` | 否 | `object/map` | - | `CreateChatCompletionRequest.allOf[2]` | Modify the likelihood of specified tokens appearing in the completion. Accepts a JSON object that maps tokens (specified by their token ID in the tokenizer) to an associated bias … | +| `logprobs` | 否 | `boolean` | - | `CreateChatCompletionRequest.allOf[2]` | Whether to return log probabilities of the output tokens or not. If true, returns the log probabilities of each output token returned in the content of message. | +| `max_completion_tokens` | 否 | `integer` | - | `CreateChatCompletionRequest.allOf[2]` | An upper bound for the number of tokens that can be generated for a completion, including visible output tokens and [reasoning tokens](/docs/guides/reasoning). | +| `max_tokens` | 否 | `integer` | - | `CreateChatCompletionRequest.allOf[2]` | The maximum number of [tokens](/tokenizer) that can be generated in the chat completion. This value can be used to control [costs](https://openai.com/api/pricing/) for text genera… | +| `messages` | 是 | `array` | - | `CreateChatCompletionRequest.allOf[2]` | A list of messages comprising the conversation so far. Depending on the [model](/docs/models) you use, different message types (modalities) are supported, like [text](/docs/guides… | +| `metadata` | 否 | `Metadata` | - | `ModelResponseProperties` | - | +| `modalities` | 否 | `ResponseModalities` | - | `CreateChatCompletionRequest.allOf[2]` | - | +| `model` | 是 | `ModelIdsShared` | - | `CreateChatCompletionRequest.allOf[2]` | Model ID used to generate the response, like gpt-4o or o3. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer t… | +| `n` | 否 | `integer` | - | `CreateChatCompletionRequest.allOf[2]` | How many chat completion choices to generate for each input message. Note that you will be charged based on the number of generated tokens across all of the choices. Keep n as 1 t… | +| `parallel_tool_calls` | 否 | `ParallelToolCalls` | - | `CreateChatCompletionRequest.allOf[2]` | - | +| `prediction` | 否 | `PredictionContent` | - | `CreateChatCompletionRequest.allOf[2]` | Configuration for a [Predicted Output](/docs/guides/predicted-outputs), which can greatly improve response times when large parts of the model response are known ahead of time. Th… | +| `presence_penalty` | 否 | `number` | - | `CreateChatCompletionRequest.allOf[2]` | Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. | +| `prompt_cache_key` | 否 | `string` | - | `ModelResponseProperties` | Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the user field. [Learn more](/docs/guides/prompt-caching). | +| `prompt_cache_retention` | 否 | `string \| null` | - | `ModelResponseProperties` | - | +| `reasoning_effort` | 否 | `ReasoningEffort` | - | `CreateChatCompletionRequest.allOf[2]` | - | +| `response_format` | 否 | `ResponseFormatText \| ResponseFormatJsonSchema \| ResponseFormatJsonObject` | - | `CreateChatCompletionRequest.allOf[2]` | An object specifying the format that the model must output. Setting to { "type": "json_schema", "json_schema": {...} } enables Structured Outputs which ensures the model will matc… | +| `safety_identifier` | 否 | `string` | - | `ModelResponseProperties` | A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. The IDs should be a string that uniquely identifies each user, wit… | +| `seed` | 否 | `integer` | - | `CreateChatCompletionRequest.allOf[2]` | This feature is in Beta. If specified, our system will make a best effort to sample deterministically, such that repeated requests with the same seed and parameters should return … | +| `service_tier` | 否 | `ServiceTier` | - | `ModelResponseProperties` | - | +| `stop` | 否 | `StopConfiguration` | - | `CreateChatCompletionRequest.allOf[2]` | - | +| `store` | 否 | `boolean` | - | `CreateChatCompletionRequest.allOf[2]` | Whether or not to store the output of this chat completion request for use in our [model distillation](/docs/guides/distillation) or [evals](/docs/guides/evals) products. Supports… | +| `stream` | 否 | `boolean` | - | `CreateChatCompletionRequest.allOf[2]` | If set to true, the model response data will be streamed to the client as it is generated using [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_e… | +| `stream_options` | 否 | `ChatCompletionStreamOptions` | - | `CreateChatCompletionRequest.allOf[2]` | - | +| `temperature` | 否 | `number \| null` | - | `ModelResponseProperties` | - | +| `tool_choice` | 否 | `ChatCompletionToolChoiceOption` | - | `CreateChatCompletionRequest.allOf[2]` | - | +| `tools` | 否 | `array` | - | `CreateChatCompletionRequest.allOf[2]` | A list of tools the model may call. You can provide either [custom tools](/docs/guides/function-calling#custom-tools) or [function tools](/docs/guides/function-calling). | +| `top_logprobs` | 否 | `integer \| null` | - | `ModelResponseProperties` | - | +| `top_p` | 否 | `number \| null` | - | `ModelResponseProperties` | - | +| `user` | 否 | `string` | - | `ModelResponseProperties` | This field is being replaced by safety_identifier and prompt_cache_key. Use prompt_cache_key instead to maintain caching optimizations. A stable identifier for your end-users. Use… | +| `verbosity` | 否 | `Verbosity` | - | `CreateChatCompletionRequest.allOf[2]` | - | +| `web_search_options` | 否 | `object` | - | `CreateChatCompletionRequest.allOf[2]` | This tool searches the web for relevant results to use in a response. Learn more about the [web search tool](/docs/guides/tools-web-search?api-mode=chat). | + +### `CreateChatCompletionResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Represents a chat completion response returned by model, based on the provided input. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `choices` | 是 | `array` | - | A list of chat completion choices. Can be more than one if n is greater than 1. | +| `created` | 是 | `integer(unixtime)` | - | The Unix timestamp (in seconds) of when the chat completion was created. | +| `id` | 是 | `string` | - | A unique identifier for the chat completion. | +| `model` | 是 | `string` | - | The model used for the chat completion. | +| `object` | 是 | `string` | `chat.completion` | The object type, which is always chat.completion. | +| `service_tier` | 否 | `ServiceTier` | - | - | +| `system_fingerprint` | 否 | `string` | - | This fingerprint represents the backend configuration that the model runs with. Can be used in conjunction with the seed request parameter to understand when backend changes have … | +| `usage` | 否 | `CompletionUsage` | - | - | + +### `CreateChatCompletionStreamResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Represents a streamed chunk of a chat completion response returned by the model, based on the provided input. [Learn more](/docs/guides/streaming-responses). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `choices` | 是 | `array` | - | A list of chat completion choices. Can contain more than one elements if n is greater than 1. Can also be empty for the last chunk if you set stream_options: {"include_usage": tru… | +| `created` | 是 | `integer(unixtime)` | - | The Unix timestamp (in seconds) of when the chat completion was created. Each chunk has the same timestamp. | +| `id` | 是 | `string` | - | A unique identifier for the chat completion. Each chunk has the same ID. | +| `model` | 是 | `string` | - | The model to generate the completion. | +| `object` | 是 | `string` | `chat.completion.chunk` | The object type, which is always chat.completion.chunk. | +| `service_tier` | 否 | `ServiceTier` | - | - | +| `system_fingerprint` | 否 | `string` | - | This fingerprint represents the backend configuration that the model runs with. Can be used in conjunction with the seed request parameter to understand when backend changes have … | +| `usage` | 否 | `CompletionUsage` | - | An optional field that will only be present when you set stream_options: {"include_usage": true} in your request. When present, it contains a null value **except for the last chun… | + +### `CreateEmbeddingRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `dimensions` | 否 | `integer` | - | The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models. | +| `encoding_format` | 否 | `string` | `float`, `base64` | The format to return the embeddings in. Can be either float or [base64](https://pypi.org/project/pybase64/). | +| `input` | 是 | `string \| array \| array \| array>` | - | Input text to embed, encoded as a string or array of tokens. To embed multiple inputs in a single request, pass an array of strings or array of token arrays. The input must not ex… | +| `model` | 是 | `string \| string` | - | ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models) for descr… | +| `user` | 否 | `string` | - | A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices#end-user-ids). | + +### `CreateEmbeddingResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `data` | 是 | `array` | - | The list of embeddings generated by the model. | +| `model` | 是 | `string` | - | The name of the model used to generate the embedding. | +| `object` | 是 | `string` | `list` | The object type, which is always "list". | +| `usage` | 是 | `object` | - | The usage information for the request. | + +### `CreateImageEditRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `background` | 否 | `string` | `transparent`, `opaque`, `auto` | Allows to set transparency for the background of the generated image(s). This parameter is only supported for the GPT image models. Must be one of transparent, opaque or auto (def… | +| `image` | 是 | `string(binary) \| array` | - | The image(s) to edit. Must be a supported image file or an array of images. For the GPT image models (gpt-image-1, gpt-image-1-mini, and gpt-image-1.5), each image should be a png… | +| `input_fidelity` | 否 | `InputFidelity \| null` | - | - | +| `mask` | 否 | `string(binary)` | - | An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited. If there are multiple images provided, the mask will be applied… | +| `model` | 否 | `string \| string` | - | The model to use for image generation. Defaults to gpt-image-1.5. | +| `n` | 否 | `integer` | - | The number of images to generate. Must be between 1 and 10. | +| `output_compression` | 否 | `integer` | - | The compression level (0-100%) for the generated images. This parameter is only supported for the GPT image models with the webp or jpeg output formats, and defaults to 100. | +| `output_format` | 否 | `string` | `png`, `jpeg`, `webp` | The format in which the generated images are returned. This parameter is only supported for the GPT image models. Must be one of png, jpeg, or webp. The default value is png. | +| `partial_images` | 否 | `PartialImages` | - | - | +| `prompt` | 是 | `string` | - | A text description of the desired image(s). The maximum length is 1000 characters for dall-e-2, and 32000 characters for the GPT image models. | +| `quality` | 否 | `string` | `standard`, `low`, `medium`, `high`, `auto` | The quality of the image that will be generated for GPT image models. Defaults to auto. | +| `response_format` | 否 | `string` | `url`, `b64_json` | The format in which the generated images are returned. Must be one of url or b64_json. URLs are only valid for 60 minutes after the image has been generated. This parameter is onl… | +| `size` | 否 | `string \| string` | - | The size of the generated images. For gpt-image-2 and gpt-image-2-2026-04-21, arbitrary resolutions are supported as WIDTHxHEIGHT strings, for example 1536x864. Width and height m… | +| `stream` | 否 | `boolean` | - | Edit the image in streaming mode. Defaults to false. See the [Image generation guide](/docs/guides/image-generation) for more information. | +| `user` | 否 | `string` | - | A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices#end-user-ids). | + +### `CreateImageRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `background` | 否 | `string` | `transparent`, `opaque`, `auto` | Allows to set transparency for the background of the generated image(s). This parameter is only supported for the GPT image models. Must be one of transparent, opaque or auto (def… | +| `model` | 否 | `string \| string` | - | The model to use for image generation. One of dall-e-2, dall-e-3, or a GPT image model (gpt-image-1, gpt-image-1-mini, gpt-image-1.5). Defaults to dall-e-2 unless a parameter spec… | +| `moderation` | 否 | `string` | `low`, `auto` | Control the content-moderation level for images generated by the GPT image models. Must be either low for less restrictive filtering or auto (default value). | +| `n` | 否 | `integer` | - | The number of images to generate. Must be between 1 and 10. For dall-e-3, only n=1 is supported. | +| `output_compression` | 否 | `integer` | - | The compression level (0-100%) for the generated images. This parameter is only supported for the GPT image models with the webp or jpeg output formats, and defaults to 100. | +| `output_format` | 否 | `string` | `png`, `jpeg`, `webp` | The format in which the generated images are returned. This parameter is only supported for the GPT image models. Must be one of png, jpeg, or webp. | +| `partial_images` | 否 | `PartialImages` | - | - | +| `prompt` | 是 | `string` | - | A text description of the desired image(s). The maximum length is 32000 characters for the GPT image models, 1000 characters for dall-e-2 and 4000 characters for dall-e-3. | +| `quality` | 否 | `string` | `standard`, `hd`, `low`, `medium`, `high`, `auto` | The quality of the image that will be generated. - auto (default value) will automatically select the best quality for the given model. - high, medium and low are supported for th… | +| `response_format` | 否 | `string` | `url`, `b64_json` | The format in which generated images with dall-e-2 and dall-e-3 are returned. Must be one of url or b64_json. URLs are only valid for 60 minutes after the image has been generated… | +| `size` | 否 | `string \| string` | - | The size of the generated images. For gpt-image-2 and gpt-image-2-2026-04-21, arbitrary resolutions are supported as WIDTHxHEIGHT strings, for example 1536x864. Width and height m… | +| `stream` | 否 | `boolean` | - | Generate the image in streaming mode. Defaults to false. See the [Image generation guide](/docs/guides/image-generation) for more information. This parameter is only supported for… | +| `style` | 否 | `string` | `vivid`, `natural` | The style of the generated images. This parameter is only supported for dall-e-3. Must be one of vivid or natural. Vivid causes the model to lean towards generating hyper-real and… | +| `user` | 否 | `string` | - | A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices#end-user-ids). | + +### `CreateImageVariationRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `image` | 是 | `string(binary)` | - | The image to use as the basis for the variation(s). Must be a valid PNG file, less than 4MB, and square. | +| `model` | 否 | `string \| string` | - | The model to use for image generation. Only dall-e-2 is supported at this time. | +| `n` | 否 | `integer` | - | The number of images to generate. Must be between 1 and 10. | +| `response_format` | 否 | `string` | `url`, `b64_json` | The format in which the generated images are returned. Must be one of url or b64_json. URLs are only valid for 60 minutes after the image has been generated. | +| `size` | 否 | `string` | `256x256`, `512x512`, `1024x1024` | The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. | +| `user` | 否 | `string` | - | A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices#end-user-ids). | + +### `CreateModelResponseProperties` + +| 项 | 值 | +| --- | --- | +| 类型 | `ModelResponseProperties & object` | +| 说明 | - | +| 组合 | `allOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ModelResponseProperties` | - | +| 2 | `object` | - | + +#### allOf 展开字段 + +| 字段 | 必填 | 类型 | 枚举/常量 | 来源 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `metadata` | 否 | `Metadata` | - | `ModelResponseProperties` | - | +| `prompt_cache_key` | 否 | `string` | - | `ModelResponseProperties` | Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the user field. [Learn more](/docs/guides/prompt-caching). | +| `prompt_cache_retention` | 否 | `string \| null` | - | `ModelResponseProperties` | - | +| `safety_identifier` | 否 | `string` | - | `ModelResponseProperties` | A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. The IDs should be a string that uniquely identifies each user, wit… | +| `service_tier` | 否 | `ServiceTier` | - | `ModelResponseProperties` | - | +| `temperature` | 否 | `number \| null` | - | `ModelResponseProperties` | - | +| `top_logprobs` | 否 | `integer \| null` | - | `ModelResponseProperties` | - | +| `top_p` | 否 | `number \| null` | - | `ModelResponseProperties` | - | +| `user` | 否 | `string` | - | `ModelResponseProperties` | This field is being replaced by safety_identifier and prompt_cache_key. Use prompt_cache_key instead to maintain caching optimizations. A stable identifier for your end-users. Use… | + +### `CreateResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `CreateModelResponseProperties & ResponseProperties & object` | +| 说明 | - | +| 组合 | `allOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `CreateModelResponseProperties` | - | +| 2 | `ResponseProperties` | - | +| 3 | `object` | - | + +#### allOf 展开字段 + +| 字段 | 必填 | 类型 | 枚举/常量 | 来源 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `background` | 否 | `boolean \| null` | - | `ResponseProperties` | - | +| `context_management` | 否 | `array \| null` | - | `CreateResponse.allOf[3]` | - | +| `conversation` | 否 | `ConversationParam \| null` | - | `CreateResponse.allOf[3]` | - | +| `include` | 否 | `array \| null` | - | `CreateResponse.allOf[3]` | - | +| `input` | 否 | `InputParam` | - | `CreateResponse.allOf[3]` | - | +| `instructions` | 否 | `string \| null` | - | `CreateResponse.allOf[3]` | - | +| `max_output_tokens` | 否 | `integer \| null` | - | `CreateResponse.allOf[3]` | - | +| `max_tool_calls` | 否 | `integer \| null` | - | `ResponseProperties` | - | +| `metadata` | 否 | `Metadata` | - | `ModelResponseProperties` | - | +| `model` | 否 | `ModelIdsResponses` | - | `ResponseProperties` | Model ID used to generate the response, like gpt-4o or o3. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer t… | +| `parallel_tool_calls` | 否 | `boolean \| null` | - | `CreateResponse.allOf[3]` | - | +| `previous_response_id` | 否 | `string \| null` | - | `ResponseProperties` | - | +| `prompt` | 否 | `Prompt` | - | `ResponseProperties` | - | +| `prompt_cache_key` | 否 | `string` | - | `ModelResponseProperties` | Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the user field. [Learn more](/docs/guides/prompt-caching). | +| `prompt_cache_retention` | 否 | `string \| null` | - | `ModelResponseProperties` | - | +| `reasoning` | 否 | `Reasoning \| null` | - | `ResponseProperties` | - | +| `safety_identifier` | 否 | `string` | - | `ModelResponseProperties` | A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. The IDs should be a string that uniquely identifies each user, wit… | +| `service_tier` | 否 | `ServiceTier` | - | `ModelResponseProperties` | - | +| `store` | 否 | `boolean \| null` | - | `CreateResponse.allOf[3]` | - | +| `stream` | 否 | `boolean \| null` | - | `CreateResponse.allOf[3]` | - | +| `stream_options` | 否 | `ResponseStreamOptions` | - | `CreateResponse.allOf[3]` | - | +| `temperature` | 否 | `number \| null` | - | `ModelResponseProperties` | - | +| `text` | 否 | `ResponseTextParam` | - | `ResponseProperties` | - | +| `tool_choice` | 否 | `ToolChoiceParam` | - | `ResponseProperties` | - | +| `tools` | 否 | `ToolsArray` | - | `ResponseProperties` | - | +| `top_logprobs` | 否 | `integer \| null` | - | `ModelResponseProperties` | - | +| `top_p` | 否 | `number \| null` | - | `ModelResponseProperties` | - | +| `truncation` | 否 | `string \| null` | - | `ResponseProperties` | - | +| `user` | 否 | `string` | - | `ModelResponseProperties` | This field is being replaced by safety_identifier and prompt_cache_key. Use prompt_cache_key instead to maintain caching optimizations. A stable identifier for your end-users. Use… | + +### `CustomGrammarFormatParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A grammar defined by the user. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `definition` | 是 | `string` | - | The grammar definition. | +| `syntax` | 是 | `GrammarSyntax1` | - | The syntax of the grammar definition. One of lark or regex. | +| `type` | 是 | `string` | `grammar` | Grammar format. Always grammar. | + +### `CustomTextFormatParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Unconstrained free-form text. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `text` | Unconstrained text format. Always text. | + +### `CustomToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A call to a custom tool created by the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | An identifier used to map this custom tool call to a tool call output. | +| `id` | 否 | `string` | - | The unique ID of the custom tool call in the OpenAI platform. | +| `input` | 是 | `string` | - | The input for the custom tool call generated by the model. | +| `name` | 是 | `string` | - | The name of the custom tool being called. | +| `namespace` | 否 | `string` | - | The namespace of the custom tool being called. | +| `type` | 是 | `string` | `custom_tool_call` | The type of the custom tool call. Always custom_tool_call. | + +### `CustomToolCallOutput` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The output of a custom tool call from your code, being sent back to the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | The call ID, used to map this custom tool call output to a custom tool call. | +| `id` | 否 | `string` | - | The unique ID of the custom tool call output in the OpenAI platform. | +| `output` | 是 | `string \| array` | - | The output from the custom tool call generated by your code. Can be a string or an list of output content. | +| `type` | 是 | `string` | `custom_tool_call_output` | The type of the custom tool call output. Always custom_tool_call_output. | + +### `CustomToolCallOutputResource` + +| 项 | 值 | +| --- | --- | +| 类型 | `CustomToolCallOutput & object` | +| 说明 | - | +| 组合 | `allOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `CustomToolCallOutput` | - | +| 2 | `object` | - | + +#### allOf 展开字段 + +| 字段 | 必填 | 类型 | 枚举/常量 | 来源 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | `CustomToolCallOutput` | The call ID, used to map this custom tool call output to a custom tool call. | +| `created_by` | 否 | `string` | - | `CustomToolCallOutputResource.allOf[2]` | The identifier of the actor that created the item. | +| `id` | 是 | `string` | - | `CustomToolCallOutput` | The unique ID of the custom tool call output in the OpenAI platform. | +| `output` | 是 | `string \| array` | - | `CustomToolCallOutput` | The output from the custom tool call generated by your code. Can be a string or an list of output content. | +| `status` | 是 | `FunctionCallOutputStatusEnum` | - | `CustomToolCallOutputResource.allOf[2]` | The status of the item. One of in_progress, completed, or incomplete. Populated when items are returned via API. | +| `type` | 是 | `string` | `custom_tool_call_output` | `CustomToolCallOutput` | The type of the custom tool call output. Always custom_tool_call_output. | + +### `CustomToolChatCompletions` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A custom tool that processes input using a specified format. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `custom` | 是 | `object` | - | Properties of the custom tool. | +| `type` | 是 | `string` | `custom` | The type of the custom tool. Always custom. | + +### `CustomToolParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A custom tool that processes input using a specified format. Learn more about [custom tools](/docs/guides/function-calling#custom-tools) | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `defer_loading` | 否 | `boolean` | - | Whether this tool should be deferred and discovered via tool search. | +| `description` | 否 | `string` | - | Optional description of the custom tool, used to provide more context. | +| `format` | 否 | `CustomTextFormatParam \| CustomGrammarFormatParam` | - | The input format for the custom tool. Default is unconstrained text. | +| `name` | 是 | `string` | - | The name of the custom tool, used to identify it in tool calls. | +| `type` | 是 | `string` | `custom` | The type of the custom tool. Always custom. | + +### `DetailEnum` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `DoubleClickAction` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A double click action. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `keys` | 是 | `array \| null` | - | - | +| `type` | 是 | `string` | `double_click` | Specifies the event type. For a double click action, this property is always set to double_click. | +| `x` | 是 | `integer` | - | The x-coordinate where the double click occurred. | +| `y` | 是 | `integer` | - | The y-coordinate where the double click occurred. | + +### `DragParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A drag action. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `keys` | 否 | `array \| null` | - | - | +| `path` | 是 | `array` | - | An array of coordinates representing the path of the drag action. Coordinates will appear as an array of objects, eg | +| `type` | 是 | `string` | `drag` | Specifies the event type. For a drag action, this property is always set to drag. | + +### `EasyInputMessage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A message input to the model with a role indicating instruction following hierarchy. Instructions given with the developer or system role take precedence over instructions given w… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 是 | `string \| InputMessageContentList` | - | Text, image, or audio input to the model, used to generate a response. Can also contain previous assistant responses. | +| `phase` | 否 | `MessagePhase \| null` | - | - | +| `role` | 是 | `string` | `user`, `assistant`, `system`, `developer` | The role of the message input. One of user, assistant, system, or developer. | +| `type` | 否 | `string` | `message` | The type of the message input. Always message. | + +### `EditImageBodyJsonParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | JSON request body for image edits. Use images (array of ImageRefParam) instead of multipart image uploads. You can reference images via external URLs, data URLs, or uploaded file … | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `background` | 否 | `string \| null` | - | Background behavior for generated image output. | +| `images` | 是 | `array` | - | Input image references to edit. For GPT image models, you can provide up to 16 images. | +| `input_fidelity` | 否 | `string \| null` | - | Controls fidelity to the original input image(s). | +| `mask` | 否 | `ImageRefParam` | - | - | +| `model` | 否 | `string \| string \| null` | - | The model to use for image editing. | +| `moderation` | 否 | `string \| null` | - | Moderation level for GPT image models. | +| `n` | 否 | `integer \| null` | - | The number of edited images to generate. | +| `output_compression` | 否 | `integer \| null` | - | Compression level for jpeg or webp output. | +| `output_format` | 否 | `string \| null` | - | Output image format. Supported for GPT image models. | +| `partial_images` | 否 | `PartialImages` | - | - | +| `prompt` | 是 | `string` | - | A text description of the desired image edit. | +| `quality` | 否 | `string \| null` | - | Output quality for GPT image models. | +| `size` | 否 | `string \| null` | - | Requested output image size. | +| `stream` | 否 | `boolean \| null` | - | Stream partial image results as events. | +| `user` | 否 | `string` | - | A unique identifier representing your end-user, which can help OpenAI monitor and detect abuse. | + +### `Embedding` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Represents an embedding vector returned by embedding endpoint. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `embedding` | 是 | `array` | - | The embedding vector, which is a list of floats. The length of vector depends on the model as listed in the [embedding guide](/docs/guides/embeddings). | +| `index` | 是 | `integer` | - | The index of the embedding in the list of embeddings. | +| `object` | 是 | `string` | `embedding` | The object type, which is always "embedding". | + +### `EmptyModelParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +### `FileCitationBody` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A citation to a file. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `file_id` | 是 | `string` | - | The ID of the file. | +| `filename` | 是 | `string` | - | The filename of the file cited. | +| `index` | 是 | `integer` | - | The index of the file in the list of files. | +| `type` | 是 | `string` | `file_citation` | The type of the file citation. Always file_citation. | + +### `FileDetailEnum` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `FileInputDetail` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `FilePath` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A path to a file. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `file_id` | 是 | `string` | - | The ID of the file. | +| `index` | 是 | `integer` | - | The index of the file in the list of files. | +| `type` | 是 | `string` | `file_path` | The type of the file path. Always file_path. | + +### `FileSearchTool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool that searches for relevant content from uploaded files. Learn more about the [file search tool](https://platform.openai.com/docs/guides/tools-file-search). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `filters` | 否 | `Filters \| null` | - | - | +| `max_num_results` | 否 | `integer` | - | The maximum number of results to return. This number should be between 1 and 50 inclusive. | +| `ranking_options` | 否 | `RankingOptions` | - | Ranking options for search. | +| `type` | 是 | `string` | `file_search` | The type of the file search tool. Always file_search. | +| `vector_store_ids` | 是 | `array` | - | The IDs of the vector stores to search. | + +### `FileSearchToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The results of a file search tool call. See the [file search guide](/docs/guides/tools-file-search) for more information. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `id` | 是 | `string` | - | The unique ID of the file search tool call. | +| `queries` | 是 | `array` | - | The queries used to search for files. | +| `results` | 否 | `array \| null` | - | - | +| `status` | 是 | `string` | `in_progress`, `searching`, `completed`, `incomplete`, `failed` | The status of the file search tool call. One of in_progress, searching, incomplete or failed, | +| `type` | 是 | `string` | `file_search_call` | The type of the file search tool call. Always file_search_call. | + +### `Filters` + +| 项 | 值 | +| --- | --- | +| 类型 | `ComparisonFilter \| CompoundFilter` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ComparisonFilter` | - | +| 2 | `CompoundFilter` | - | + +### `FunctionAndCustomToolCallOutput` + +| 项 | 值 | +| --- | --- | +| 类型 | `InputTextContent \| InputImageContent \| InputFileContent` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `InputTextContent` | - | +| 2 | `InputImageContent` | - | +| 3 | `InputFileContent` | - | + +### `FunctionCallItemStatus` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `FunctionCallOutputItemParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The output of a function tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | The unique ID of the function tool call generated by the model. | +| `id` | 否 | `string \| null` | - | - | +| `output` | 是 | `string \| array` | - | Text, image, or file output of the function tool call. | +| `status` | 否 | `FunctionCallItemStatus \| null` | - | - | +| `type` | 是 | `string` | `function_call_output` | The type of the function tool call output. Always function_call_output. | + +### `FunctionCallOutputStatusEnum` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `FunctionCallStatus` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `FunctionObject` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `description` | 否 | `string` | - | A description of what the function does, used by the model to choose when and how to call the function. | +| `name` | 是 | `string` | - | The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. | +| `parameters` | 否 | `FunctionParameters` | - | - | +| `strict` | 否 | `boolean \| null` | - | - | + +### `FunctionParameters` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/function-calling) for examples, and the [JSON Schema reference](https://json-… | + +Additional properties: `任意 JSON 值` + +### `FunctionShellAction` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Execute a shell command. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `commands` | 是 | `array` | - | - | +| `max_output_length` | 是 | `integer \| null` | - | - | +| `timeout_ms` | 是 | `integer \| null` | - | - | + +### `FunctionShellActionParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Commands and limits describing how to run the shell tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `commands` | 是 | `array` | - | Ordered shell commands for the execution environment to run. | +| `max_output_length` | 否 | `integer \| null` | - | - | +| `timeout_ms` | 否 | `integer \| null` | - | - | + +### `FunctionShellCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool call that executes one or more shell commands in a managed environment. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `action` | 是 | `FunctionShellAction` | - | The shell commands and limits that describe how to run the tool call. | +| `call_id` | 是 | `string` | - | The unique ID of the shell tool call generated by the model. | +| `created_by` | 否 | `string` | - | The ID of the entity that created this tool call. | +| `environment` | 是 | `LocalEnvironmentResource \| ContainerReferenceResource \| null` | - | - | +| `id` | 是 | `string` | - | The unique ID of the shell tool call. Populated when this item is returned via API. | +| `status` | 是 | `FunctionShellCallStatus` | - | The status of the shell call. One of in_progress, completed, or incomplete. | +| `type` | 是 | `string` | `shell_call` | The type of the item. Always shell_call. | + +### `FunctionShellCallItemParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool representing a request to execute one or more shell commands. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `action` | 是 | `FunctionShellActionParam` | - | The shell commands and limits that describe how to run the tool call. | +| `call_id` | 是 | `string` | - | The unique ID of the shell tool call generated by the model. | +| `environment` | 否 | `LocalEnvironmentParam \| ContainerReferenceParam \| null` | - | - | +| `id` | 否 | `string \| null` | - | - | +| `status` | 否 | `FunctionShellCallItemStatus \| null` | - | - | +| `type` | 是 | `string` | `shell_call` | The type of the item. Always shell_call. | + +### `FunctionShellCallItemStatus` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | Status values reported for shell tool calls. | + +### `FunctionShellCallOutput` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The output of a shell tool call that was emitted. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | The unique ID of the shell tool call generated by the model. | +| `created_by` | 否 | `string` | - | The identifier of the actor that created the item. | +| `id` | 是 | `string` | - | The unique ID of the shell call output. Populated when this item is returned via API. | +| `max_output_length` | 是 | `integer \| null` | - | - | +| `output` | 是 | `array` | - | An array of shell call output contents | +| `status` | 是 | `FunctionShellCallOutputStatusEnum` | - | The status of the shell call output. One of in_progress, completed, or incomplete. | +| `type` | 是 | `string` | `shell_call_output` | The type of the shell call output. Always shell_call_output. | + +### `FunctionShellCallOutputContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The content of a shell tool call output that was emitted. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `created_by` | 否 | `string` | - | The identifier of the actor that created the item. | +| `outcome` | 是 | `FunctionShellCallOutputTimeoutOutcome \| FunctionShellCallOutputExitOutcome` | - | Represents either an exit outcome (with an exit code) or a timeout outcome for a shell call output chunk. | +| `stderr` | 是 | `string` | - | The standard error output that was captured. | +| `stdout` | 是 | `string` | - | The standard output that was captured. | + +### `FunctionShellCallOutputContentParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Captured stdout and stderr for a portion of a shell tool call output. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `outcome` | 是 | `FunctionShellCallOutputOutcomeParam` | - | The exit or timeout outcome associated with this shell call. | +| `stderr` | 是 | `string` | - | Captured stderr output for the shell call. | +| `stdout` | 是 | `string` | - | Captured stdout output for the shell call. | + +### `FunctionShellCallOutputExitOutcome` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Indicates that the shell commands finished and returned an exit code. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `exit_code` | 是 | `integer` | - | Exit code from the shell process. | +| `type` | 是 | `string` | `exit` | The outcome type. Always exit. | + +### `FunctionShellCallOutputExitOutcomeParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Indicates that the shell commands finished and returned an exit code. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `exit_code` | 是 | `integer` | - | The exit code returned by the shell process. | +| `type` | 是 | `string` | `exit` | The outcome type. Always exit. | + +### `FunctionShellCallOutputItemParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The streamed output items emitted by a shell tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | The unique ID of the shell tool call generated by the model. | +| `id` | 否 | `string \| null` | - | - | +| `max_output_length` | 否 | `integer \| null` | - | - | +| `output` | 是 | `array` | - | Captured chunks of stdout and stderr output, along with their associated outcomes. | +| `status` | 否 | `FunctionShellCallItemStatus \| null` | - | - | +| `type` | 是 | `string` | `shell_call_output` | The type of the item. Always shell_call_output. | + +### `FunctionShellCallOutputOutcomeParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `FunctionShellCallOutputTimeoutOutcomeParam \| FunctionShellCallOutputExitOutcomeParam` | +| 说明 | The exit or timeout outcome associated with this shell call. | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `FunctionShellCallOutputTimeoutOutcomeParam` | - | +| 2 | `FunctionShellCallOutputExitOutcomeParam` | - | + +### `FunctionShellCallOutputStatusEnum` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `FunctionShellCallOutputTimeoutOutcome` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Indicates that the shell call exceeded its configured time limit. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `timeout` | The outcome type. Always timeout. | + +### `FunctionShellCallOutputTimeoutOutcomeParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Indicates that the shell call exceeded its configured time limit. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `timeout` | The outcome type. Always timeout. | + +### `FunctionShellCallStatus` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `FunctionShellToolParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool that allows the model to execute shell commands. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `environment` | 否 | `ContainerAutoParam \| LocalEnvironmentParam \| ContainerReferenceParam \| null` | - | - | +| `type` | 是 | `string` | `shell` | The type of the shell tool. Always shell. | + +### `FunctionTool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Defines a function in your own code the model can choose to call. Learn more about [function calling](https://platform.openai.com/docs/guides/function-calling). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `defer_loading` | 否 | `boolean` | - | Whether this function is deferred and loaded via tool search. | +| `description` | 否 | `string \| null` | - | - | +| `name` | 是 | `string` | - | The name of the function to call. | +| `parameters` | 是 | `object/map \| null` | - | - | +| `strict` | 是 | `boolean \| null` | - | - | +| `type` | 是 | `string` | `function` | The type of the function tool. Always function. | + +### `FunctionToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool call to run a function. See the [function calling guide](/docs/guides/function-calling) for more information. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `arguments` | 是 | `string` | - | A JSON string of the arguments to pass to the function. | +| `call_id` | 是 | `string` | - | The unique ID of the function tool call generated by the model. | +| `id` | 否 | `string` | - | The unique ID of the function tool call. | +| `name` | 是 | `string` | - | The name of the function to run. | +| `namespace` | 否 | `string` | - | The namespace of the function to run. | +| `status` | 否 | `string` | `in_progress`, `completed`, `incomplete` | The status of the item. One of in_progress, completed, or incomplete. Populated when items are returned via API. | +| `type` | 是 | `string` | `function_call` | The type of the function tool call. Always function_call. | + +### `FunctionToolCallOutput` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The output of a function tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | The unique ID of the function tool call generated by the model. | +| `id` | 否 | `string` | - | The unique ID of the function tool call output. Populated when this item is returned via API. | +| `output` | 是 | `string \| array` | - | The output from the function call generated by your code. Can be a string or an list of output content. | +| `status` | 否 | `string` | `in_progress`, `completed`, `incomplete` | The status of the item. One of in_progress, completed, or incomplete. Populated when items are returned via API. | +| `type` | 是 | `string` | `function_call_output` | The type of the function tool call output. Always function_call_output. | + +### `FunctionToolCallOutputResource` + +| 项 | 值 | +| --- | --- | +| 类型 | `FunctionToolCallOutput & object` | +| 说明 | - | +| 组合 | `allOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `FunctionToolCallOutput` | - | +| 2 | `object` | - | + +#### allOf 展开字段 + +| 字段 | 必填 | 类型 | 枚举/常量 | 来源 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `call_id` | 是 | `string` | - | `FunctionToolCallOutput` | The unique ID of the function tool call generated by the model. | +| `created_by` | 否 | `string` | - | `FunctionToolCallOutputResource.allOf[2]` | The identifier of the actor that created the item. | +| `id` | 是 | `string` | - | `FunctionToolCallOutput` | The unique ID of the function tool call output. Populated when this item is returned via API. | +| `output` | 是 | `string \| array` | - | `FunctionToolCallOutput` | The output from the function call generated by your code. Can be a string or an list of output content. | +| `status` | 是 | `string` | `in_progress`, `completed`, `incomplete` | `FunctionToolCallOutput` | The status of the item. One of in_progress, completed, or incomplete. Populated when items are returned via API. | +| `type` | 是 | `string` | `function_call_output` | `FunctionToolCallOutput` | The type of the function tool call output. Always function_call_output. | + +### `FunctionToolParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `defer_loading` | 否 | `boolean` | - | Whether this function should be deferred and discovered via tool search. | +| `description` | 否 | `string \| null` | - | - | +| `name` | 是 | `string` | - | - | +| `parameters` | 否 | `EmptyModelParam \| null` | - | - | +| `strict` | 否 | `boolean \| null` | - | - | +| `type` | 是 | `string` | `function` | - | + +### `GrammarSyntax1` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `HybridSearchOptions` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `embedding_weight` | 是 | `number` | - | The weight of the embedding in the reciprocal ranking fusion. | +| `text_weight` | 是 | `number` | - | The weight of the text in the reciprocal ranking fusion. | + +### `Image` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Represents the content or the URL of an image generated by the OpenAI API. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `b64_json` | 否 | `string` | - | The base64-encoded JSON of the generated image. Returned by default for the GPT image models, and only present if response_format is set to b64_json for dall-e-2 and dall-e-3. | +| `revised_prompt` | 否 | `string` | - | For dall-e-3 only, the revised prompt that was used to generate the image. | +| `url` | 否 | `string(uri)` | - | When using dall-e-2 or dall-e-3, the URL of the generated image if response_format is set to url (default value). Unsupported for the GPT image models. | + +### `ImageDetail` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `ImageEditCompletedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when image editing has completed and the final image is available. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `b64_json` | 是 | `string` | - | Base64-encoded final edited image data, suitable for rendering as an image. | +| `background` | 是 | `string` | `transparent`, `opaque`, `auto` | The background setting for the edited image. | +| `created_at` | 是 | `integer(unixtime)` | - | The Unix timestamp when the event was created. | +| `output_format` | 是 | `string` | `png`, `webp`, `jpeg` | The output format for the edited image. | +| `quality` | 是 | `string` | `low`, `medium`, `high`, `auto` | The quality setting for the edited image. | +| `size` | 是 | `string` | `1024x1024`, `1024x1536`, `1536x1024`, `auto` | The size of the edited image. | +| `type` | 是 | `string` | `image_edit.completed` | The type of the event. Always image_edit.completed. | +| `usage` | 是 | `ImagesUsage` | - | - | + +### `ImageEditPartialImageEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a partial image is available during image editing streaming. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `b64_json` | 是 | `string` | - | Base64-encoded partial image data, suitable for rendering as an image. | +| `background` | 是 | `string` | `transparent`, `opaque`, `auto` | The background setting for the requested edited image. | +| `created_at` | 是 | `integer(unixtime)` | - | The Unix timestamp when the event was created. | +| `output_format` | 是 | `string` | `png`, `webp`, `jpeg` | The output format for the requested edited image. | +| `partial_image_index` | 是 | `integer` | - | 0-based index for the partial image (streaming). | +| `quality` | 是 | `string` | `low`, `medium`, `high`, `auto` | The quality setting for the requested edited image. | +| `size` | 是 | `string` | `1024x1024`, `1024x1536`, `1536x1024`, `auto` | The size of the requested edited image. | +| `type` | 是 | `string` | `image_edit.partial_image` | The type of the event. Always image_edit.partial_image. | + +### `ImageEditStreamEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `ImageEditPartialImageEvent \| ImageEditCompletedEvent` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ImageEditPartialImageEvent` | - | +| 2 | `ImageEditCompletedEvent` | - | + +### `ImageGenActionEnum` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `ImageGenCompletedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when image generation has completed and the final image is available. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `b64_json` | 是 | `string` | - | Base64-encoded image data, suitable for rendering as an image. | +| `background` | 是 | `string` | `transparent`, `opaque`, `auto` | The background setting for the generated image. | +| `created_at` | 是 | `integer(unixtime)` | - | The Unix timestamp when the event was created. | +| `output_format` | 是 | `string` | `png`, `webp`, `jpeg` | The output format for the generated image. | +| `quality` | 是 | `string` | `low`, `medium`, `high`, `auto` | The quality setting for the generated image. | +| `size` | 是 | `string` | `1024x1024`, `1024x1536`, `1536x1024`, `auto` | The size of the generated image. | +| `type` | 是 | `string` | `image_generation.completed` | The type of the event. Always image_generation.completed. | +| `usage` | 是 | `ImagesUsage` | - | - | + +### `ImageGenInputUsageDetails` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The input tokens detailed information for the image generation. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `image_tokens` | 是 | `integer` | - | The number of image tokens in the input prompt. | +| `text_tokens` | 是 | `integer` | - | The number of text tokens in the input prompt. | + +### `ImageGenOutputTokensDetails` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The output token details for the image generation. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `image_tokens` | 是 | `integer` | - | The number of image output tokens generated by the model. | +| `text_tokens` | 是 | `integer` | - | The number of text output tokens generated by the model. | + +### `ImageGenPartialImageEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a partial image is available during image generation streaming. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `b64_json` | 是 | `string` | - | Base64-encoded partial image data, suitable for rendering as an image. | +| `background` | 是 | `string` | `transparent`, `opaque`, `auto` | The background setting for the requested image. | +| `created_at` | 是 | `integer(unixtime)` | - | The Unix timestamp when the event was created. | +| `output_format` | 是 | `string` | `png`, `webp`, `jpeg` | The output format for the requested image. | +| `partial_image_index` | 是 | `integer` | - | 0-based index for the partial image (streaming). | +| `quality` | 是 | `string` | `low`, `medium`, `high`, `auto` | The quality setting for the requested image. | +| `size` | 是 | `string` | `1024x1024`, `1024x1536`, `1536x1024`, `auto` | The size of the requested image. | +| `type` | 是 | `string` | `image_generation.partial_image` | The type of the event. Always image_generation.partial_image. | + +### `ImageGenStreamEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `ImageGenPartialImageEvent \| ImageGenCompletedEvent` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ImageGenPartialImageEvent` | - | +| 2 | `ImageGenCompletedEvent` | - | + +### `ImageGenTool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool that generates images using the GPT image models. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `action` | 否 | `ImageGenActionEnum` | - | Whether to generate a new image or edit an existing image. Default: auto. | +| `background` | 否 | `string` | `transparent`, `opaque`, `auto` | Background type for the generated image. One of transparent, opaque, or auto. Default: auto. | +| `input_fidelity` | 否 | `InputFidelity \| null` | - | - | +| `input_image_mask` | 否 | `object` | - | Optional mask for inpainting. Contains image_url (string, optional) and file_id (string, optional). | +| `model` | 否 | `string \| string` | - | - | +| `moderation` | 否 | `string` | `auto`, `low` | Moderation level for the generated image. Default: auto. | +| `output_compression` | 否 | `integer` | - | Compression level for the output image. Default: 100. | +| `output_format` | 否 | `string` | `png`, `webp`, `jpeg` | The output format of the generated image. One of png, webp, or jpeg. Default: png. | +| `partial_images` | 否 | `integer` | - | Number of partial images to generate in streaming mode, from 0 (default value) to 3. | +| `quality` | 否 | `string` | `low`, `medium`, `high`, `auto` | The quality of the generated image. One of low, medium, high, or auto. Default: auto. | +| `size` | 否 | `string \| string` | - | The size of the generated images. For gpt-image-2 and gpt-image-2-2026-04-21, arbitrary resolutions are supported as WIDTHxHEIGHT strings, for example 1536x864. Width and height m… | +| `type` | 是 | `string` | `image_generation` | The type of the image generation tool. Always image_generation. | + +### `ImageGenToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An image generation request made by the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `id` | 是 | `string` | - | The unique ID of the image generation call. | +| `result` | 是 | `string \| null` | - | - | +| `status` | 是 | `string` | `in_progress`, `completed`, `generating`, `failed` | The status of the image generation call. | +| `type` | 是 | `string` | `image_generation_call` | The type of the image generation call. Always image_generation_call. | + +### `ImageGenUsage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | For gpt-image-1 only, the token usage information for the image generation. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `input_tokens` | 是 | `integer` | - | The number of tokens (images and text) in the input prompt. | +| `input_tokens_details` | 是 | `ImageGenInputUsageDetails` | - | - | +| `output_tokens` | 是 | `integer` | - | The number of output tokens generated by the model. | +| `output_tokens_details` | 否 | `ImageGenOutputTokensDetails` | - | - | +| `total_tokens` | 是 | `integer` | - | The total number of tokens (images and text) used for the image generation. | + +### `ImageRefParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object/value \| object/value` | +| 说明 | Reference an input image by either URL or uploaded file ID. Provide exactly one of image_url or file_id. | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `object/value` | - | +| 2 | `object/value` | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `file_id` | 否 | `string` | - | The File API ID of an uploaded image to use as input. | +| `image_url` | 否 | `string(uri)` | - | A fully qualified URL or base64-encoded data URL. | + +### `ImagesResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The response from the image generation endpoint. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `background` | 否 | `string` | `transparent`, `opaque` | The background parameter used for the image generation. Either transparent or opaque. | +| `created` | 是 | `integer(unixtime)` | - | The Unix timestamp (in seconds) of when the image was created. | +| `data` | 否 | `array` | - | The list of generated images. | +| `output_format` | 否 | `string` | `png`, `webp`, `jpeg` | The output format of the image generation. Either png, webp, or jpeg. | +| `quality` | 否 | `string` | `low`, `medium`, `high` | The quality of the image generated. Either low, medium, or high. | +| `size` | 否 | `string` | `1024x1024`, `1024x1536`, `1536x1024` | The size of the image generated. Either 1024x1024, 1024x1536, or 1536x1024. | +| `usage` | 否 | `ImageGenUsage` | - | - | + +### `ImagesUsage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | For the GPT image models only, the token usage information for the image generation. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `input_tokens` | 是 | `integer` | - | The number of tokens (images and text) in the input prompt. | +| `input_tokens_details` | 是 | `object` | - | The input tokens detailed information for the image generation. | +| `output_tokens` | 是 | `integer` | - | The number of image tokens in the output image. | +| `total_tokens` | 是 | `integer` | - | The total number of tokens (images and text) used for the image generation. | + +### `IncludeEnum` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | Specify additional output data to include in the model response. Currently supported values are: - web_search_call.results: Include the search results of the web search tool call.… | + +### `InlineSkillParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `description` | 是 | `string` | - | The description of the skill. | +| `name` | 是 | `string` | - | The name of the skill. | +| `source` | 是 | `InlineSkillSourceParam` | - | Inline skill payload | +| `type` | 是 | `string` | `inline` | Defines an inline skill for this request. | + +### `InlineSkillSourceParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Inline skill payload | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `data` | 是 | `string` | - | Base64-encoded skill zip bundle. | +| `media_type` | 是 | `string` | `application/zip` | The media type of the inline skill payload. Must be application/zip. | +| `type` | 是 | `string` | `base64` | The type of the inline skill source. Must be base64. | + +### `InputContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `InputTextContent \| InputImageContent \| InputFileContent` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `InputTextContent` | - | +| 2 | `InputImageContent` | - | +| 3 | `InputFileContent` | - | + +### `InputFidelity` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | Control how much effort the model will exert to match the style and features, especially facial features, of input images. This parameter is only supported for gpt-image-1 and gpt… | + +### `InputFileContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A file input to the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `detail` | 否 | `FileInputDetail` | - | The detail level of the file to be sent to the model. Use low for the default rendering behavior, or high to render the file at higher quality. Defaults to low. | +| `file_data` | 否 | `string` | - | The content of the file to be sent to the model. | +| `file_id` | 否 | `string \| null` | - | - | +| `file_url` | 否 | `string(uri)` | - | The URL of the file to be sent to the model. | +| `filename` | 否 | `string` | - | The name of the file to be sent to the model. | +| `type` | 是 | `string` | `input_file` | The type of the input item. Always input_file. | + +### `InputFileContentParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A file input to the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `detail` | 否 | `FileDetailEnum` | - | The detail level of the file to be sent to the model. Use low for the default rendering behavior, or high to render the file at higher quality. Defaults to low. | +| `file_data` | 否 | `string \| null` | - | - | +| `file_id` | 否 | `string \| null` | - | - | +| `file_url` | 否 | `string(uri) \| null` | - | - | +| `filename` | 否 | `string \| null` | - | - | +| `type` | 是 | `string` | `input_file` | The type of the input item. Always input_file. | + +### `InputImageContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An image input to the model. Learn about [image inputs](/docs/guides/vision). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `detail` | 是 | `ImageDetail` | - | The detail level of the image to be sent to the model. One of high, low, auto, or original. Defaults to auto. | +| `file_id` | 否 | `string \| null` | - | - | +| `image_url` | 否 | `string(uri) \| null` | - | - | +| `type` | 是 | `string` | `input_image` | The type of the input item. Always input_image. | + +### `InputImageContentParamAutoParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An image input to the model. Learn about [image inputs](/docs/guides/vision) | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `detail` | 否 | `DetailEnum \| null` | - | - | +| `file_id` | 否 | `string \| null` | - | - | +| `image_url` | 否 | `string(uri) \| null` | - | - | +| `type` | 是 | `string` | `input_image` | The type of the input item. Always input_image. | + +### `InputItem` + +| 项 | 值 | +| --- | --- | +| 类型 | `EasyInputMessage \| Item \| CompactionTriggerItemParam \| ItemReferenceParam` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `EasyInputMessage` | - | +| 2 | `Item` | An item representing part of the context for the response to be generated by the model. Can contain text, images, and audio inputs, as well as previous assistant responses and too… | +| 3 | `CompactionTriggerItemParam` | - | +| 4 | `ItemReferenceParam` | - | + +### `InputMessage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A message input to the model with a role indicating instruction following hierarchy. Instructions given with the developer or system role take precedence over instructions given w… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 是 | `InputMessageContentList` | - | - | +| `role` | 是 | `string` | `user`, `system`, `developer` | The role of the message input. One of user, system, or developer. | +| `status` | 否 | `string` | `in_progress`, `completed`, `incomplete` | The status of item. One of in_progress, completed, or incomplete. Populated when items are returned via API. | +| `type` | 否 | `string` | `message` | The type of the message input. Always set to message. | + +### `InputMessageContentList` + +| 项 | 值 | +| --- | --- | +| 类型 | `array` | +| 说明 | A list of one or many input items to the model, containing different content types. | + +### `InputParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `string \| array` | +| 说明 | Text, image, or file inputs to the model, used to generate a response. Learn more: - [Text inputs and outputs](/docs/guides/text) - [Image inputs](/docs/guides/images) - [File inp… | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `string` | A text input to the model, equivalent to a text input with the user role. | +| 2 | `array` | A list of one or many input items to the model, containing different content types. | + +### `InputTextContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A text input to the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `text` | 是 | `string` | - | The text input to the model. | +| `type` | 是 | `string` | `input_text` | The type of the input item. Always input_text. | + +### `InputTextContentParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A text input to the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `text` | 是 | `string` | - | The text input to the model. | +| `type` | 是 | `string` | `input_text` | The type of the input item. Always input_text. | + +### `Item` + +| 项 | 值 | +| --- | --- | +| 类型 | `InputMessage \| OutputMessage \| FileSearchToolCall \| ComputerToolCall \| ComputerCallOutputItemParam \| WebSearchToolCall \| FunctionToolCall \| FunctionCallOutputItemParam … (+19)` | +| 说明 | Content item used to generate a response. | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `InputMessage` | - | +| 2 | `OutputMessage` | - | +| 3 | `FileSearchToolCall` | - | +| 4 | `ComputerToolCall` | - | +| 5 | `ComputerCallOutputItemParam` | - | +| 6 | `WebSearchToolCall` | - | +| 7 | `FunctionToolCall` | - | +| 8 | `FunctionCallOutputItemParam` | - | +| 9 | `ToolSearchCallItemParam` | - | +| 10 | `ToolSearchOutputItemParam` | - | +| 11 | `AdditionalToolsItemParam` | - | +| 12 | `ReasoningItem` | - | +| 13 | `CompactionSummaryItemParam` | - | +| 14 | `ImageGenToolCall` | - | +| 15 | `CodeInterpreterToolCall` | - | +| 16 | `LocalShellToolCall` | - | +| 17 | `LocalShellToolCallOutput` | - | +| 18 | `FunctionShellCallItemParam` | - | +| 19 | `FunctionShellCallOutputItemParam` | - | +| 20 | `ApplyPatchToolCallItemParam` | - | +| 21 | `ApplyPatchToolCallOutputItemParam` | - | +| 22 | `MCPListTools` | - | +| 23 | `MCPApprovalRequest` | - | +| 24 | `MCPApprovalResponse` | - | +| 25 | `MCPToolCall` | - | +| 26 | `CustomToolCallOutput` | - | +| 27 | `CustomToolCall` | - | + +### `ItemField` + +| 项 | 值 | +| --- | --- | +| 类型 | `Message \| FunctionToolCall \| ToolSearchCall \| ToolSearchOutput \| AdditionalTools \| FunctionToolCallOutput \| FileSearchToolCall \| WebSearchToolCall … (+18)` | +| 说明 | An item representing a message, tool call, tool output, reasoning, or other response element. | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `Message` | - | +| 2 | `FunctionToolCall` | - | +| 3 | `ToolSearchCall` | - | +| 4 | `ToolSearchOutput` | - | +| 5 | `AdditionalTools` | - | +| 6 | `FunctionToolCallOutput` | - | +| 7 | `FileSearchToolCall` | - | +| 8 | `WebSearchToolCall` | - | +| 9 | `ImageGenToolCall` | - | +| 10 | `ComputerToolCall` | - | +| 11 | `ComputerToolCallOutputResource` | - | +| 12 | `ReasoningItem` | - | +| 13 | `CompactionBody` | - | +| 14 | `CodeInterpreterToolCall` | - | +| 15 | `LocalShellToolCall` | - | +| 16 | `LocalShellToolCallOutput` | - | +| 17 | `FunctionShellCall` | - | +| 18 | `FunctionShellCallOutput` | - | +| 19 | `ApplyPatchToolCall` | - | +| 20 | `ApplyPatchToolCallOutput` | - | +| 21 | `MCPListTools` | - | +| 22 | `MCPApprovalRequest` | - | +| 23 | `MCPApprovalResponseResource` | - | +| 24 | `MCPToolCall` | - | +| 25 | `CustomToolCall` | - | +| 26 | `CustomToolCallOutput` | - | + +### `ItemReferenceParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An internal identifier for an item to reference. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `id` | 是 | `string` | - | The ID of the item to reference. | +| `type` | 否 | `string \| null` | - | - | + +### `KeyPressAction` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A collection of keypresses the model would like to perform. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `keys` | 是 | `array` | - | The combination of keys the model is requesting to be pressed. This is an array of strings, each representing a key. | +| `type` | 是 | `string` | `keypress` | Specifies the event type. For a keypress action, this property is always set to keypress. | + +### `LocalEnvironmentParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `skills` | 否 | `array` | - | An optional list of skills. | +| `type` | 是 | `string` | `local` | Use a local computer environment. | + +### `LocalEnvironmentResource` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Represents the use of a local environment to perform shell actions. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `local` | The environment type. Always local. | + +### `LocalShellExecAction` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Execute a shell command on the server. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `command` | 是 | `array` | - | The command to run. | +| `env` | 是 | `object/map` | - | Environment variables to set for the command. | +| `timeout_ms` | 否 | `integer \| null` | - | - | +| `type` | 是 | `string` | `exec` | The type of the local shell action. Always exec. | +| `user` | 否 | `string \| null` | - | - | +| `working_directory` | 否 | `string \| null` | - | - | + +### `LocalShellToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool call to run a command on the local shell. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `action` | 是 | `LocalShellExecAction` | - | - | +| `call_id` | 是 | `string` | - | The unique ID of the local shell tool call generated by the model. | +| `id` | 是 | `string` | - | The unique ID of the local shell call. | +| `status` | 是 | `string` | `in_progress`, `completed`, `incomplete` | The status of the local shell call. | +| `type` | 是 | `string` | `local_shell_call` | The type of the local shell call. Always local_shell_call. | + +### `LocalShellToolCallOutput` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The output of a local shell tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `id` | 是 | `string` | - | The unique ID of the local shell tool call generated by the model. | +| `output` | 是 | `string` | - | A JSON string of the output of the local shell tool call. | +| `status` | 否 | `string \| null` | - | - | +| `type` | 是 | `string` | `local_shell_call_output` | The type of the local shell tool call output. Always local_shell_call_output. | + +### `LocalShellToolParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool that allows the model to execute shell commands in a local environment. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `local_shell` | The type of the local shell tool. Always local_shell. | + +### `LocalSkillParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `description` | 是 | `string` | - | The description of the skill. | +| `name` | 是 | `string` | - | The name of the skill. | +| `path` | 是 | `string` | - | The path to the directory containing the skill. | + +### `LogProb` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The log probability of a token. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `bytes` | 是 | `array` | - | - | +| `logprob` | 是 | `number` | - | - | +| `token` | 是 | `string` | - | - | +| `top_logprobs` | 是 | `array` | - | - | + +### `MCPApprovalRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A request for human approval of a tool invocation. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `arguments` | 是 | `string` | - | A JSON string of arguments for the tool. | +| `id` | 是 | `string` | - | The unique ID of the approval request. | +| `name` | 是 | `string` | - | The name of the tool to run. | +| `server_label` | 是 | `string` | - | The label of the MCP server making the request. | +| `type` | 是 | `string` | `mcp_approval_request` | The type of the item. Always mcp_approval_request. | + +### `MCPApprovalResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A response to an MCP approval request. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `approval_request_id` | 是 | `string` | - | The ID of the approval request being answered. | +| `approve` | 是 | `boolean` | - | Whether the request was approved. | +| `id` | 否 | `string \| null` | - | - | +| `reason` | 否 | `string \| null` | - | - | +| `type` | 是 | `string` | `mcp_approval_response` | The type of the item. Always mcp_approval_response. | + +### `MCPApprovalResponseResource` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A response to an MCP approval request. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `approval_request_id` | 是 | `string` | - | The ID of the approval request being answered. | +| `approve` | 是 | `boolean` | - | Whether the request was approved. | +| `id` | 是 | `string` | - | The unique ID of the approval response | +| `reason` | 否 | `string \| null` | - | - | +| `type` | 是 | `string` | `mcp_approval_response` | The type of the item. Always mcp_approval_response. | + +### `MCPListTools` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A list of tools available on an MCP server. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `error` | 否 | `string \| null` | - | - | +| `id` | 是 | `string` | - | The unique ID of the list. | +| `server_label` | 是 | `string` | - | The label of the MCP server. | +| `tools` | 是 | `array` | - | The tools available on the server. | +| `type` | 是 | `string` | `mcp_list_tools` | The type of the item. Always mcp_list_tools. | + +### `MCPListToolsTool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A tool available on an MCP server. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `annotations` | 否 | `object \| null` | - | - | +| `description` | 否 | `string \| null` | - | - | +| `input_schema` | 是 | `object` | - | The JSON schema describing the tool's input. | +| `name` | 是 | `string` | - | The name of the tool. | + +### `MCPTool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Give the model access to additional tools via remote Model Context Protocol (MCP) servers. [Learn more about MCP](/docs/guides/tools-remote-mcp). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `allowed_tools` | 否 | `array \| MCPToolFilter \| null` | - | - | +| `authorization` | 否 | `string` | - | An OAuth access token that can be used with a remote MCP server, either with a custom MCP server URL or a service connector. Your application must handle the OAuth authorization f… | +| `connector_id` | 否 | `string` | `connector_dropbox`, `connector_gmail`, `connector_googlecalendar`, `connector_googledrive`, `connector_microsoftteams`, `connector_outlookcalendar`, `connector_outlookemail`, `connector_sharepoint` | Identifier for service connectors, like those available in ChatGPT. One of server_url or connector_id must be provided. Learn more about service connectors [here](/docs/guides/too… | +| `defer_loading` | 否 | `boolean` | - | Whether this MCP tool is deferred and discovered via tool search. | +| `headers` | 否 | `object/map \| null` | - | - | +| `require_approval` | 否 | `object \| string \| null` | - | - | +| `server_description` | 否 | `string` | - | Optional description of the MCP server, used to provide more context. | +| `server_label` | 是 | `string` | - | A label for this MCP server, used to identify it in tool calls. | +| `server_url` | 否 | `string(uri)` | - | The URL for the MCP server. One of server_url or connector_id must be provided. | +| `type` | 是 | `string` | `mcp` | The type of the MCP tool. Always mcp. | + +### `MCPToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An invocation of a tool on an MCP server. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `approval_request_id` | 否 | `string \| null` | - | - | +| `arguments` | 是 | `string` | - | A JSON string of the arguments passed to the tool. | +| `error` | 否 | `string \| null` | - | - | +| `id` | 是 | `string` | - | The unique ID of the tool call. | +| `name` | 是 | `string` | - | The name of the tool that was run. | +| `output` | 否 | `string \| null` | - | - | +| `server_label` | 是 | `string` | - | The label of the MCP server running the tool. | +| `status` | 否 | `MCPToolCallStatus` | - | The status of the tool call. One of in_progress, completed, incomplete, calling, or failed. | +| `type` | 是 | `string` | `mcp_call` | The type of the item. Always mcp_call. | + +### `MCPToolCallStatus` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `MCPToolFilter` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A filter object to specify which tools are allowed. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `read_only` | 否 | `boolean` | - | Indicates whether or not a tool modifies data or is read-only. If an MCP server is [annotated with readOnlyHint](https://modelcontextprotocol.io/specification/2025-06-18/schema#to… | +| `tool_names` | 否 | `array` | - | List of allowed tool names. | + +### `Message` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A message to or from the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 是 | `array` | - | The content of the message | +| `id` | 是 | `string` | - | The unique ID of the message. | +| `phase` | 否 | `MessagePhase-2 \| null` | - | - | +| `role` | 是 | `MessageRole` | - | The role of the message. One of unknown, user, assistant, system, critic, discriminator, developer, or tool. | +| `status` | 是 | `MessageStatus` | - | The status of item. One of in_progress, completed, or incomplete. Populated when items are returned via API. | +| `type` | 是 | `string` | `message` | The type of the message. Always set to message. | + +### `MessagePhase` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | Labels an assistant message as intermediate commentary (commentary) or the final answer (final_answer). For models like gpt-5.3-codex and beyond, when sending follow-up requests, … | + +### `MessagePhase-2` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `MessageRole` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `MessageStatus` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `Metadata` + +| 项 | 值 | +| --- | --- | +| 类型 | `object/map \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `object/map` | Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for object… | +| 2 | `null` | - | + +### `ModelIdsCompaction` + +| 项 | 值 | +| --- | --- | +| 类型 | `ModelIdsResponses \| string \| null` | +| 说明 | Model ID used to generate the response, like gpt-5 or o3. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer to… | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ModelIdsResponses` | - | +| 2 | `string` | - | +| 3 | `null` | - | + +### `ModelIdsResponses` + +| 项 | 值 | +| --- | --- | +| 类型 | `ModelIdsShared \| string` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ModelIdsShared` | - | +| 2 | `string` | - | + +### `ModelIdsShared` + +| 项 | 值 | +| --- | --- | +| 类型 | `string \| string` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `string` | - | +| 2 | `string` | - | + +### `ModelResponseProperties` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `metadata` | 否 | `Metadata` | - | - | +| `prompt_cache_key` | 否 | `string` | - | Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the user field. [Learn more](/docs/guides/prompt-caching). | +| `prompt_cache_retention` | 否 | `string \| null` | - | - | +| `safety_identifier` | 否 | `string` | - | A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. The IDs should be a string that uniquely identifies each user, wit… | +| `service_tier` | 否 | `ServiceTier` | - | - | +| `temperature` | 否 | `number \| null` | - | - | +| `top_logprobs` | 否 | `integer \| null` | - | - | +| `top_p` | 否 | `number \| null` | - | - | +| `user` | 否 | `string` | - | This field is being replaced by safety_identifier and prompt_cache_key. Use prompt_cache_key instead to maintain caching optimizations. A stable identifier for your end-users. Use… | + +### `MoveParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A mouse move action. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `keys` | 否 | `array \| null` | - | - | +| `type` | 是 | `string` | `move` | Specifies the event type. For a move action, this property is always set to move. | +| `x` | 是 | `integer` | - | The x-coordinate to move to. | +| `y` | 是 | `integer` | - | The y-coordinate to move to. | + +### `NamespaceToolParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Groups function/custom tools under a shared namespace. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `description` | 是 | `string` | - | A description of the namespace shown to the model. | +| `name` | 是 | `string` | - | The namespace name used in tool calls (for example, crm). | +| `tools` | 是 | `array` | - | The function/custom tools available inside this namespace. | +| `type` | 是 | `string` | `namespace` | The type of the tool. Always namespace. | + +### `OutputContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `OutputTextContent \| RefusalContent \| ReasoningTextContent` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `OutputTextContent` | - | +| 2 | `RefusalContent` | - | +| 3 | `ReasoningTextContent` | - | + +### `OutputItem` + +| 项 | 值 | +| --- | --- | +| 类型 | `OutputMessage \| FileSearchToolCall \| FunctionToolCall \| FunctionToolCallOutputResource \| WebSearchToolCall \| ComputerToolCall \| ComputerToolCallOutputResource \| ReasoningItem … (+18)` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `OutputMessage` | - | +| 2 | `FileSearchToolCall` | - | +| 3 | `FunctionToolCall` | - | +| 4 | `FunctionToolCallOutputResource` | - | +| 5 | `WebSearchToolCall` | - | +| 6 | `ComputerToolCall` | - | +| 7 | `ComputerToolCallOutputResource` | - | +| 8 | `ReasoningItem` | - | +| 9 | `ToolSearchCall` | - | +| 10 | `ToolSearchOutput` | - | +| 11 | `AdditionalTools` | - | +| 12 | `CompactionBody` | - | +| 13 | `ImageGenToolCall` | - | +| 14 | `CodeInterpreterToolCall` | - | +| 15 | `LocalShellToolCall` | - | +| 16 | `LocalShellToolCallOutput` | - | +| 17 | `FunctionShellCall` | - | +| 18 | `FunctionShellCallOutput` | - | +| 19 | `ApplyPatchToolCall` | - | +| 20 | `ApplyPatchToolCallOutput` | - | +| 21 | `MCPToolCall` | - | +| 22 | `MCPListTools` | - | +| 23 | `MCPApprovalRequest` | - | +| 24 | `MCPApprovalResponseResource` | - | +| 25 | `CustomToolCall` | - | +| 26 | `CustomToolCallOutputResource` | - | + +### `OutputMessage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An output message from the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 是 | `array` | - | The content of the output message. | +| `id` | 是 | `string` | - | The unique ID of the output message. | +| `phase` | 否 | `MessagePhase \| null` | - | - | +| `role` | 是 | `string` | `assistant` | The role of the output message. Always assistant. | +| `status` | 是 | `string` | `in_progress`, `completed`, `incomplete` | The status of the message input. One of in_progress, completed, or incomplete. Populated when input items are returned via API. | +| `type` | 是 | `string` | `message` | The type of the output message. Always message. | + +### `OutputMessageContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `OutputTextContent \| RefusalContent` | +| 说明 | - | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `OutputTextContent` | - | +| 2 | `RefusalContent` | - | + +### `OutputTextContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A text output from the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `annotations` | 是 | `array` | - | The annotations of the text output. | +| `logprobs` | 是 | `array` | - | - | +| `text` | 是 | `string` | - | The text output from the model. | +| `type` | 是 | `string` | `output_text` | The type of the output text. Always output_text. | + +### `ParallelToolCalls` + +| 项 | 值 | +| --- | --- | +| 类型 | `boolean` | +| 说明 | Whether to enable [parallel function calling](/docs/guides/function-calling#configuring-parallel-function-calling) during tool use. | + +### `PartialImages` + +| 项 | 值 | +| --- | --- | +| 类型 | `integer \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `integer` | The number of partial images to generate. This parameter is used for streaming responses that return partial images. Value must be between 0 and 3. When set to 0, the response wil… | +| 2 | `null` | - | + +### `PredictionContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Static predicted output content, such as the content of a text file that is being regenerated. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 是 | `string \| array` | - | The content that should be matched when generating a model response. If generated tokens would match this content, the entire model response can be returned much more quickly. | +| `type` | 是 | `string` | `content` | The type of the predicted content you want to provide. This type is currently always content. | + +### `Prompt` + +| 项 | 值 | +| --- | --- | +| 类型 | `object \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `object` | Reference to a prompt template and its variables. [Learn more](/docs/guides/text?api-mode=responses#reusable-prompts). | +| 2 | `null` | - | + +### `PromptCacheRetentionEnum` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `RankerVersionType` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `RankingOptions` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `hybrid_search` | 否 | `HybridSearchOptions` | - | Weights that control how reciprocal rank fusion balances semantic embedding matches versus sparse keyword matches when hybrid search is enabled. | +| `ranker` | 否 | `RankerVersionType` | - | The ranker to use for the file search. | +| `score_threshold` | 否 | `number` | - | The score threshold for the file search, a number between 0 and 1. Numbers closer to 1 will attempt to return only the most relevant results, but may return fewer results. | + +### `Reasoning` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | **gpt-5 and o-series models only** Configuration options for [reasoning models](https://platform.openai.com/docs/guides/reasoning). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `effort` | 否 | `ReasoningEffort` | - | - | +| `generate_summary` | 否 | `string \| null` | - | - | +| `summary` | 否 | `string \| null` | - | - | + +### `ReasoningEffort` + +| 项 | 值 | +| --- | --- | +| 类型 | `string \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `string` | Constrains effort on reasoning for [reasoning models](https://platform.openai.com/docs/guides/reasoning). Currently supported values are none, minimal, low, medium, high, and xhig… | +| 2 | `null` | - | + +### `ReasoningItem` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A description of the chain of thought used by a reasoning model while generating a response. Be sure to include these items in your input to the Responses API for subsequent turns… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 否 | `array` | - | Reasoning text content. | +| `encrypted_content` | 否 | `string \| null` | - | - | +| `id` | 是 | `string` | - | The unique identifier of the reasoning content. | +| `status` | 否 | `string` | `in_progress`, `completed`, `incomplete` | The status of the item. One of in_progress, completed, or incomplete. Populated when items are returned via API. | +| `summary` | 是 | `array` | - | Reasoning summary content. | +| `type` | 是 | `string` | `reasoning` | The type of the object. Always reasoning. | + +### `ReasoningTextContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Reasoning text from the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `text` | 是 | `string` | - | The reasoning text from the model. | +| `type` | 是 | `string` | `reasoning_text` | The type of the reasoning text. Always reasoning_text. | + +### `RefusalContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A refusal from the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `refusal` | 是 | `string` | - | The refusal explanation from the model. | +| `type` | 是 | `string` | `refusal` | The type of the refusal. Always refusal. | + +### `Response` + +| 项 | 值 | +| --- | --- | +| 类型 | `ModelResponseProperties & ResponseProperties & object` | +| 说明 | - | +| 组合 | `allOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ModelResponseProperties` | - | +| 2 | `ResponseProperties` | - | +| 3 | `object` | - | + +#### allOf 展开字段 + +| 字段 | 必填 | 类型 | 枚举/常量 | 来源 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `background` | 否 | `boolean \| null` | - | `ResponseProperties` | - | +| `completed_at` | 否 | `number(unixtime) \| null` | - | `Response.allOf[3]` | - | +| `conversation` | 否 | `Conversation-2 \| null` | - | `Response.allOf[3]` | - | +| `created_at` | 是 | `number(unixtime)` | - | `Response.allOf[3]` | Unix timestamp (in seconds) of when this Response was created. | +| `error` | 是 | `ResponseError` | - | `Response.allOf[3]` | - | +| `id` | 是 | `string` | - | `Response.allOf[3]` | Unique identifier for this Response. | +| `incomplete_details` | 是 | `object \| null` | - | `Response.allOf[3]` | - | +| `instructions` | 是 | `string \| array \| null` | - | `Response.allOf[3]` | - | +| `max_output_tokens` | 否 | `integer \| null` | - | `Response.allOf[3]` | - | +| `max_tool_calls` | 否 | `integer \| null` | - | `ResponseProperties` | - | +| `metadata` | 是 | `Metadata` | - | `ModelResponseProperties` | - | +| `model` | 是 | `ModelIdsResponses` | - | `ResponseProperties` | Model ID used to generate the response, like gpt-4o or o3. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer t… | +| `object` | 是 | `string` | `response` | `Response.allOf[3]` | The object type of this resource - always set to response. | +| `output` | 是 | `array` | - | `Response.allOf[3]` | An array of content items generated by the model. - The length and order of items in the output array is dependent on the model's response. - Rather than accessing the first item … | +| `output_text` | 否 | `string \| null` | - | `Response.allOf[3]` | - | +| `parallel_tool_calls` | 是 | `boolean` | - | `Response.allOf[3]` | Whether to allow the model to run tool calls in parallel. | +| `previous_response_id` | 否 | `string \| null` | - | `ResponseProperties` | - | +| `prompt` | 否 | `Prompt` | - | `ResponseProperties` | - | +| `prompt_cache_key` | 否 | `string` | - | `ModelResponseProperties` | Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the user field. [Learn more](/docs/guides/prompt-caching). | +| `prompt_cache_retention` | 否 | `string \| null` | - | `ModelResponseProperties` | - | +| `reasoning` | 否 | `Reasoning \| null` | - | `ResponseProperties` | - | +| `safety_identifier` | 否 | `string` | - | `ModelResponseProperties` | A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. The IDs should be a string that uniquely identifies each user, wit… | +| `service_tier` | 否 | `ServiceTier` | - | `ModelResponseProperties` | - | +| `status` | 否 | `string` | `completed`, `failed`, `in_progress`, `cancelled`, `queued`, `incomplete` | `Response.allOf[3]` | The status of the response generation. One of completed, failed, in_progress, cancelled, queued, or incomplete. | +| `temperature` | 是 | `number \| null` | - | `ModelResponseProperties` | - | +| `text` | 否 | `ResponseTextParam` | - | `ResponseProperties` | - | +| `tool_choice` | 是 | `ToolChoiceParam` | - | `ResponseProperties` | - | +| `tools` | 是 | `ToolsArray` | - | `ResponseProperties` | - | +| `top_logprobs` | 否 | `integer \| null` | - | `ModelResponseProperties` | - | +| `top_p` | 是 | `number \| null` | - | `ModelResponseProperties` | - | +| `truncation` | 否 | `string \| null` | - | `ResponseProperties` | - | +| `usage` | 否 | `ResponseUsage` | - | `Response.allOf[3]` | - | +| `user` | 否 | `string` | - | `ModelResponseProperties` | This field is being replaced by safety_identifier and prompt_cache_key. Use prompt_cache_key instead to maintain caching optimizations. A stable identifier for your end-users. Use… | + +### `ResponseAudioDeltaEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when there is a partial audio response. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `delta` | 是 | `string` | - | A chunk of Base64 encoded response audio bytes. | +| `sequence_number` | 是 | `integer` | - | A sequence number for this chunk of the stream response. | +| `type` | 是 | `string` | `response.audio.delta` | The type of the event. Always response.audio.delta. | + +### `ResponseAudioDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when the audio response is complete. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `sequence_number` | 是 | `integer` | - | The sequence number of the delta. | +| `type` | 是 | `string` | `response.audio.done` | The type of the event. Always response.audio.done. | + +### `ResponseAudioTranscriptDeltaEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when there is a partial transcript of audio. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `delta` | 是 | `string` | - | The partial transcript of the audio response. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.audio.transcript.delta` | The type of the event. Always response.audio.transcript.delta. | + +### `ResponseAudioTranscriptDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when the full audio transcript is completed. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.audio.transcript.done` | The type of the event. Always response.audio.transcript.done. | + +### `ResponseCodeInterpreterCallCodeDeltaEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a partial code snippet is streamed by the code interpreter. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `delta` | 是 | `string` | - | The partial code snippet being streamed by the code interpreter. | +| `item_id` | 是 | `string` | - | The unique identifier of the code interpreter tool call item. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response for which the code is being streamed. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event, used to order streaming events. | +| `type` | 是 | `string` | `response.code_interpreter_call_code.delta` | The type of the event. Always response.code_interpreter_call_code.delta. | + +### `ResponseCodeInterpreterCallCodeDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when the code snippet is finalized by the code interpreter. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `code` | 是 | `string` | - | The final code snippet output by the code interpreter. | +| `item_id` | 是 | `string` | - | The unique identifier of the code interpreter tool call item. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response for which the code is finalized. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event, used to order streaming events. | +| `type` | 是 | `string` | `response.code_interpreter_call_code.done` | The type of the event. Always response.code_interpreter_call_code.done. | + +### `ResponseCodeInterpreterCallCompletedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when the code interpreter call is completed. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The unique identifier of the code interpreter tool call item. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response for which the code interpreter call is completed. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event, used to order streaming events. | +| `type` | 是 | `string` | `response.code_interpreter_call.completed` | The type of the event. Always response.code_interpreter_call.completed. | + +### `ResponseCodeInterpreterCallInProgressEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a code interpreter call is in progress. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The unique identifier of the code interpreter tool call item. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response for which the code interpreter call is in progress. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event, used to order streaming events. | +| `type` | 是 | `string` | `response.code_interpreter_call.in_progress` | The type of the event. Always response.code_interpreter_call.in_progress. | + +### `ResponseCodeInterpreterCallInterpretingEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when the code interpreter is actively interpreting the code snippet. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The unique identifier of the code interpreter tool call item. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response for which the code interpreter is interpreting code. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event, used to order streaming events. | +| `type` | 是 | `string` | `response.code_interpreter_call.interpreting` | The type of the event. Always response.code_interpreter_call.interpreting. | + +### `ResponseCompletedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when the model response is complete. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `response` | 是 | `Response` | - | Properties of the completed response. | +| `sequence_number` | 是 | `integer` | - | The sequence number for this event. | +| `type` | 是 | `string` | `response.completed` | The type of the event. Always response.completed. | + +### `ResponseContentPartAddedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a new content part is added. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content_index` | 是 | `integer` | - | The index of the content part that was added. | +| `item_id` | 是 | `string` | - | The ID of the output item that the content part was added to. | +| `output_index` | 是 | `integer` | - | The index of the output item that the content part was added to. | +| `part` | 是 | `OutputContent` | - | The content part that was added. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.content_part.added` | The type of the event. Always response.content_part.added. | + +### `ResponseContentPartDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a content part is done. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content_index` | 是 | `integer` | - | The index of the content part that is done. | +| `item_id` | 是 | `string` | - | The ID of the output item that the content part was added to. | +| `output_index` | 是 | `integer` | - | The index of the output item that the content part was added to. | +| `part` | 是 | `OutputContent` | - | The content part that is done. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.content_part.done` | The type of the event. Always response.content_part.done. | + +### `ResponseCreatedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An event that is emitted when a response is created. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `response` | 是 | `Response` | - | The response that was created. | +| `sequence_number` | 是 | `integer` | - | The sequence number for this event. | +| `type` | 是 | `string` | `response.created` | The type of the event. Always response.created. | + +### `ResponseCustomToolCallInputDeltaEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Event representing a delta (partial update) to the input of a custom tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `delta` | 是 | `string` | - | The incremental input data (delta) for the custom tool call. | +| `item_id` | 是 | `string` | - | Unique identifier for the API item associated with this event. | +| `output_index` | 是 | `integer` | - | The index of the output this delta applies to. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.custom_tool_call_input.delta` | The event type identifier. | + +### `ResponseCustomToolCallInputDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Event indicating that input for a custom tool call is complete. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `input` | 是 | `string` | - | The complete input data for the custom tool call. | +| `item_id` | 是 | `string` | - | Unique identifier for the API item associated with this event. | +| `output_index` | 是 | `integer` | - | The index of the output this event applies to. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.custom_tool_call_input.done` | The event type identifier. | + +### `ResponseError` + +| 项 | 值 | +| --- | --- | +| 类型 | `object \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `object` | An error object returned when the model fails to generate a Response. | +| 2 | `null` | - | + +### `ResponseErrorCode` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | The error code for the response. | + +### `ResponseErrorEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when an error occurs. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `code` | 是 | `string \| null` | - | - | +| `message` | 是 | `string` | - | The error message. | +| `param` | 是 | `string \| null` | - | - | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `error` | The type of the event. Always error. | + +### `ResponseFailedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An event that is emitted when a response fails. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `response` | 是 | `Response` | - | The response that failed. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.failed` | The type of the event. Always response.failed. | + +### `ResponseFileSearchCallCompletedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a file search call is completed (results found). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The ID of the output item that the file search call is initiated. | +| `output_index` | 是 | `integer` | - | The index of the output item that the file search call is initiated. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.file_search_call.completed` | The type of the event. Always response.file_search_call.completed. | + +### `ResponseFileSearchCallInProgressEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a file search call is initiated. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The ID of the output item that the file search call is initiated. | +| `output_index` | 是 | `integer` | - | The index of the output item that the file search call is initiated. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.file_search_call.in_progress` | The type of the event. Always response.file_search_call.in_progress. | + +### `ResponseFileSearchCallSearchingEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a file search is currently searching. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The ID of the output item that the file search call is initiated. | +| `output_index` | 是 | `integer` | - | The index of the output item that the file search call is searching. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.file_search_call.searching` | The type of the event. Always response.file_search_call.searching. | + +### `ResponseFormatJsonObject` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | JSON object response format. An older method of generating JSON responses. Using json_schema is recommended for models that support it. Note that the model will not generate JSON … | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `json_object` | The type of response format being defined. Always json_object. | + +### `ResponseFormatJsonSchema` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | JSON Schema response format. Used to generate structured JSON responses. Learn more about [Structured Outputs](/docs/guides/structured-outputs). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `json_schema` | 是 | `object` | - | Structured Outputs configuration options, including a JSON Schema. | +| `type` | 是 | `string` | `json_schema` | The type of response format being defined. Always json_schema. | + +### `ResponseFormatJsonSchemaSchema` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The schema for the response format, described as a JSON Schema object. Learn how to build JSON schemas [here](https://json-schema.org/). | + +Additional properties: `任意 JSON 值` + +### `ResponseFormatText` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Default response format. Used to generate text responses. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `text` | The type of response format being defined. Always text. | + +### `ResponseFunctionCallArgumentsDeltaEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when there is a partial function-call arguments delta. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `delta` | 是 | `string` | - | The function-call arguments delta that is added. | +| `item_id` | 是 | `string` | - | The ID of the output item that the function-call arguments delta is added to. | +| `output_index` | 是 | `integer` | - | The index of the output item that the function-call arguments delta is added to. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.function_call_arguments.delta` | The type of the event. Always response.function_call_arguments.delta. | + +### `ResponseFunctionCallArgumentsDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when function-call arguments are finalized. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `arguments` | 是 | `string` | - | The function-call arguments. | +| `item_id` | 是 | `string` | - | The ID of the item. | +| `name` | 是 | `string` | - | The name of the function that was called. | +| `output_index` | 是 | `integer` | - | The index of the output item. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.function_call_arguments.done` | - | + +### `ResponseImageGenCallCompletedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when an image generation tool call has completed and the final image is available. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The unique identifier of the image generation item being processed. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response's output array. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.image_generation_call.completed` | The type of the event. Always 'response.image_generation_call.completed'. | + +### `ResponseImageGenCallGeneratingEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when an image generation tool call is actively generating an image (intermediate state). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The unique identifier of the image generation item being processed. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response's output array. | +| `sequence_number` | 是 | `integer` | - | The sequence number of the image generation item being processed. | +| `type` | 是 | `string` | `response.image_generation_call.generating` | The type of the event. Always 'response.image_generation_call.generating'. | + +### `ResponseImageGenCallInProgressEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when an image generation tool call is in progress. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The unique identifier of the image generation item being processed. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response's output array. | +| `sequence_number` | 是 | `integer` | - | The sequence number of the image generation item being processed. | +| `type` | 是 | `string` | `response.image_generation_call.in_progress` | The type of the event. Always 'response.image_generation_call.in_progress'. | + +### `ResponseImageGenCallPartialImageEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a partial image is available during image generation streaming. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The unique identifier of the image generation item being processed. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response's output array. | +| `partial_image_b64` | 是 | `string` | - | Base64-encoded partial image data, suitable for rendering as an image. | +| `partial_image_index` | 是 | `integer` | - | 0-based index for the partial image (backend is 1-based, but this is 0-based for the user). | +| `sequence_number` | 是 | `integer` | - | The sequence number of the image generation item being processed. | +| `type` | 是 | `string` | `response.image_generation_call.partial_image` | The type of the event. Always 'response.image_generation_call.partial_image'. | + +### `ResponseInProgressEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when the response is in progress. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `response` | 是 | `Response` | - | The response that is in progress. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.in_progress` | The type of the event. Always response.in_progress. | + +### `ResponseIncompleteEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An event that is emitted when a response finishes as incomplete. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `response` | 是 | `Response` | - | The response that was incomplete. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.incomplete` | The type of the event. Always response.incomplete. | + +### `ResponseLogProb` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A logprob is the logarithmic probability that the model assigns to producing a particular token at a given position in the sequence. Less-negative (higher) logprob values indicate… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `logprob` | 是 | `number` | - | The log probability of this token. | +| `token` | 是 | `string` | - | A possible text token. | +| `top_logprobs` | 否 | `array` | - | The log probabilities of up to 20 of the most likely tokens. | + +### `ResponseMCPCallArgumentsDeltaEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when there is a delta (partial update) to the arguments of an MCP tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `delta` | 是 | `string` | - | A JSON string containing the partial update to the arguments for the MCP tool call. | +| `item_id` | 是 | `string` | - | The unique identifier of the MCP tool call item being processed. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response's output array. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.mcp_call_arguments.delta` | The type of the event. Always 'response.mcp_call_arguments.delta'. | + +### `ResponseMCPCallArgumentsDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when the arguments for an MCP tool call are finalized. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `arguments` | 是 | `string` | - | A JSON string containing the finalized arguments for the MCP tool call. | +| `item_id` | 是 | `string` | - | The unique identifier of the MCP tool call item being processed. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response's output array. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.mcp_call_arguments.done` | The type of the event. Always 'response.mcp_call_arguments.done'. | + +### `ResponseMCPCallCompletedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when an MCP tool call has completed successfully. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The ID of the MCP tool call item that completed. | +| `output_index` | 是 | `integer` | - | The index of the output item that completed. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.mcp_call.completed` | The type of the event. Always 'response.mcp_call.completed'. | + +### `ResponseMCPCallFailedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when an MCP tool call has failed. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The ID of the MCP tool call item that failed. | +| `output_index` | 是 | `integer` | - | The index of the output item that failed. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.mcp_call.failed` | The type of the event. Always 'response.mcp_call.failed'. | + +### `ResponseMCPCallInProgressEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when an MCP tool call is in progress. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The unique identifier of the MCP tool call item being processed. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response's output array. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.mcp_call.in_progress` | The type of the event. Always 'response.mcp_call.in_progress'. | + +### `ResponseMCPListToolsCompletedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when the list of available MCP tools has been successfully retrieved. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The ID of the MCP tool call item that produced this output. | +| `output_index` | 是 | `integer` | - | The index of the output item that was processed. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.mcp_list_tools.completed` | The type of the event. Always 'response.mcp_list_tools.completed'. | + +### `ResponseMCPListToolsFailedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when the attempt to list available MCP tools has failed. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The ID of the MCP tool call item that failed. | +| `output_index` | 是 | `integer` | - | The index of the output item that failed. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.mcp_list_tools.failed` | The type of the event. Always 'response.mcp_list_tools.failed'. | + +### `ResponseMCPListToolsInProgressEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when the system is in the process of retrieving the list of available MCP tools. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The ID of the MCP tool call item that is being processed. | +| `output_index` | 是 | `integer` | - | The index of the output item that is being processed. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.mcp_list_tools.in_progress` | The type of the event. Always 'response.mcp_list_tools.in_progress'. | + +### `ResponseModalities` + +| 项 | 值 | +| --- | --- | +| 类型 | `array \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `array` | Output types that you would like the model to generate. Most models are capable of generating text, which is the default: ["text"] The gpt-4o-audio-preview model can also be used … | +| 2 | `null` | - | + +### `ResponseOutputItemAddedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a new output item is added. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item` | 是 | `OutputItem` | - | The output item that was added. | +| `output_index` | 是 | `integer` | - | The index of the output item that was added. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.output_item.added` | The type of the event. Always response.output_item.added. | + +### `ResponseOutputItemDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when an output item is marked done. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item` | 是 | `OutputItem` | - | The output item that was marked done. | +| `output_index` | 是 | `integer` | - | The index of the output item that was marked done. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.output_item.done` | The type of the event. Always response.output_item.done. | + +### `ResponseOutputTextAnnotationAddedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when an annotation is added to output text content. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `annotation` | 是 | `object` | - | The annotation object being added. (See annotation schema for details.) | +| `annotation_index` | 是 | `integer` | - | The index of the annotation within the content part. | +| `content_index` | 是 | `integer` | - | The index of the content part within the output item. | +| `item_id` | 是 | `string` | - | The unique identifier of the item to which the annotation is being added. | +| `output_index` | 是 | `integer` | - | The index of the output item in the response's output array. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.output_text.annotation.added` | The type of the event. Always 'response.output_text.annotation.added'. | + +### `ResponsePromptVariables` + +| 项 | 值 | +| --- | --- | +| 类型 | `object/map \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `object/map` | Optional map of values to substitute in for variables in your prompt. The substitution values can either be strings, or other Response input types like images or files. | +| 2 | `null` | - | + +### `ResponseProperties` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `background` | 否 | `boolean \| null` | - | - | +| `max_tool_calls` | 否 | `integer \| null` | - | - | +| `model` | 否 | `ModelIdsResponses` | - | Model ID used to generate the response, like gpt-4o or o3. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer t… | +| `previous_response_id` | 否 | `string \| null` | - | - | +| `prompt` | 否 | `Prompt` | - | - | +| `reasoning` | 否 | `Reasoning \| null` | - | - | +| `text` | 否 | `ResponseTextParam` | - | - | +| `tool_choice` | 否 | `ToolChoiceParam` | - | - | +| `tools` | 否 | `ToolsArray` | - | - | +| `truncation` | 否 | `string \| null` | - | - | + +### `ResponseQueuedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a response is queued and waiting to be processed. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `response` | 是 | `Response` | - | The full response object that is queued. | +| `sequence_number` | 是 | `integer` | - | The sequence number for this event. | +| `type` | 是 | `string` | `response.queued` | The type of the event. Always 'response.queued'. | + +### `ResponseReasoningSummaryPartAddedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a new reasoning summary part is added. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The ID of the item this summary part is associated with. | +| `output_index` | 是 | `integer` | - | The index of the output item this summary part is associated with. | +| `part` | 是 | `object` | - | The summary part that was added. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `summary_index` | 是 | `integer` | - | The index of the summary part within the reasoning summary. | +| `type` | 是 | `string` | `response.reasoning_summary_part.added` | The type of the event. Always response.reasoning_summary_part.added. | + +### `ResponseReasoningSummaryPartDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a reasoning summary part is completed. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The ID of the item this summary part is associated with. | +| `output_index` | 是 | `integer` | - | The index of the output item this summary part is associated with. | +| `part` | 是 | `object` | - | The completed summary part. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `summary_index` | 是 | `integer` | - | The index of the summary part within the reasoning summary. | +| `type` | 是 | `string` | `response.reasoning_summary_part.done` | The type of the event. Always response.reasoning_summary_part.done. | + +### `ResponseReasoningSummaryTextDeltaEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a delta is added to a reasoning summary text. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `delta` | 是 | `string` | - | The text delta that was added to the summary. | +| `item_id` | 是 | `string` | - | The ID of the item this summary text delta is associated with. | +| `output_index` | 是 | `integer` | - | The index of the output item this summary text delta is associated with. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `summary_index` | 是 | `integer` | - | The index of the summary part within the reasoning summary. | +| `type` | 是 | `string` | `response.reasoning_summary_text.delta` | The type of the event. Always response.reasoning_summary_text.delta. | + +### `ResponseReasoningSummaryTextDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a reasoning summary text is completed. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | The ID of the item this summary text is associated with. | +| `output_index` | 是 | `integer` | - | The index of the output item this summary text is associated with. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `summary_index` | 是 | `integer` | - | The index of the summary part within the reasoning summary. | +| `text` | 是 | `string` | - | The full text of the completed reasoning summary. | +| `type` | 是 | `string` | `response.reasoning_summary_text.done` | The type of the event. Always response.reasoning_summary_text.done. | + +### `ResponseReasoningTextDeltaEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a delta is added to a reasoning text. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content_index` | 是 | `integer` | - | The index of the reasoning content part this delta is associated with. | +| `delta` | 是 | `string` | - | The text delta that was added to the reasoning content. | +| `item_id` | 是 | `string` | - | The ID of the item this reasoning text delta is associated with. | +| `output_index` | 是 | `integer` | - | The index of the output item this reasoning text delta is associated with. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.reasoning_text.delta` | The type of the event. Always response.reasoning_text.delta. | + +### `ResponseReasoningTextDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a reasoning text is completed. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content_index` | 是 | `integer` | - | The index of the reasoning content part. | +| `item_id` | 是 | `string` | - | The ID of the item this reasoning text is associated with. | +| `output_index` | 是 | `integer` | - | The index of the output item this reasoning text is associated with. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `text` | 是 | `string` | - | The full text of the completed reasoning content. | +| `type` | 是 | `string` | `response.reasoning_text.done` | The type of the event. Always response.reasoning_text.done. | + +### `ResponseRefusalDeltaEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when there is a partial refusal text. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content_index` | 是 | `integer` | - | The index of the content part that the refusal text is added to. | +| `delta` | 是 | `string` | - | The refusal text that is added. | +| `item_id` | 是 | `string` | - | The ID of the output item that the refusal text is added to. | +| `output_index` | 是 | `integer` | - | The index of the output item that the refusal text is added to. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.refusal.delta` | The type of the event. Always response.refusal.delta. | + +### `ResponseRefusalDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when refusal text is finalized. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content_index` | 是 | `integer` | - | The index of the content part that the refusal text is finalized. | +| `item_id` | 是 | `string` | - | The ID of the output item that the refusal text is finalized. | +| `output_index` | 是 | `integer` | - | The index of the output item that the refusal text is finalized. | +| `refusal` | 是 | `string` | - | The refusal text that is finalized. | +| `sequence_number` | 是 | `integer` | - | The sequence number of this event. | +| `type` | 是 | `string` | `response.refusal.done` | The type of the event. Always response.refusal.done. | + +### `ResponseStreamEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `ResponseAudioDeltaEvent \| ResponseAudioDoneEvent \| ResponseAudioTranscriptDeltaEvent \| ResponseAudioTranscriptDoneEvent \| ResponseCodeInterpreterCallCodeDeltaEvent \| ResponseCodeInterpreterCallCodeDoneEvent \| ResponseCodeInterpreterCallCompletedEvent \| ResponseCodeInterpreterCallInProgressEvent … (+45)` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ResponseAudioDeltaEvent` | - | +| 2 | `ResponseAudioDoneEvent` | - | +| 3 | `ResponseAudioTranscriptDeltaEvent` | - | +| 4 | `ResponseAudioTranscriptDoneEvent` | - | +| 5 | `ResponseCodeInterpreterCallCodeDeltaEvent` | - | +| 6 | `ResponseCodeInterpreterCallCodeDoneEvent` | - | +| 7 | `ResponseCodeInterpreterCallCompletedEvent` | - | +| 8 | `ResponseCodeInterpreterCallInProgressEvent` | - | +| 9 | `ResponseCodeInterpreterCallInterpretingEvent` | - | +| 10 | `ResponseCompletedEvent` | - | +| 11 | `ResponseContentPartAddedEvent` | - | +| 12 | `ResponseContentPartDoneEvent` | - | +| 13 | `ResponseCreatedEvent` | - | +| 14 | `ResponseErrorEvent` | - | +| 15 | `ResponseFileSearchCallCompletedEvent` | - | +| 16 | `ResponseFileSearchCallInProgressEvent` | - | +| 17 | `ResponseFileSearchCallSearchingEvent` | - | +| 18 | `ResponseFunctionCallArgumentsDeltaEvent` | - | +| 19 | `ResponseFunctionCallArgumentsDoneEvent` | - | +| 20 | `ResponseInProgressEvent` | - | +| 21 | `ResponseFailedEvent` | - | +| 22 | `ResponseIncompleteEvent` | - | +| 23 | `ResponseOutputItemAddedEvent` | - | +| 24 | `ResponseOutputItemDoneEvent` | - | +| 25 | `ResponseReasoningSummaryPartAddedEvent` | - | +| 26 | `ResponseReasoningSummaryPartDoneEvent` | - | +| 27 | `ResponseReasoningSummaryTextDeltaEvent` | - | +| 28 | `ResponseReasoningSummaryTextDoneEvent` | - | +| 29 | `ResponseReasoningTextDeltaEvent` | - | +| 30 | `ResponseReasoningTextDoneEvent` | - | +| 31 | `ResponseRefusalDeltaEvent` | - | +| 32 | `ResponseRefusalDoneEvent` | - | +| 33 | `ResponseTextDeltaEvent` | - | +| 34 | `ResponseTextDoneEvent` | - | +| 35 | `ResponseWebSearchCallCompletedEvent` | - | +| 36 | `ResponseWebSearchCallInProgressEvent` | - | +| 37 | `ResponseWebSearchCallSearchingEvent` | - | +| 38 | `ResponseImageGenCallCompletedEvent` | - | +| 39 | `ResponseImageGenCallGeneratingEvent` | - | +| 40 | `ResponseImageGenCallInProgressEvent` | - | +| 41 | `ResponseImageGenCallPartialImageEvent` | - | +| 42 | `ResponseMCPCallArgumentsDeltaEvent` | - | +| 43 | `ResponseMCPCallArgumentsDoneEvent` | - | +| 44 | `ResponseMCPCallCompletedEvent` | - | +| 45 | `ResponseMCPCallFailedEvent` | - | +| 46 | `ResponseMCPCallInProgressEvent` | - | +| 47 | `ResponseMCPListToolsCompletedEvent` | - | +| 48 | `ResponseMCPListToolsFailedEvent` | - | +| 49 | `ResponseMCPListToolsInProgressEvent` | - | +| 50 | `ResponseOutputTextAnnotationAddedEvent` | - | +| 51 | `ResponseQueuedEvent` | - | +| 52 | `ResponseCustomToolCallInputDeltaEvent` | - | +| 53 | `ResponseCustomToolCallInputDoneEvent` | - | + +### `ResponseStreamOptions` + +| 项 | 值 | +| --- | --- | +| 类型 | `object \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `object` | Options for streaming responses. Only set this when you set stream: true. | +| 2 | `null` | - | + +### `ResponseTextDeltaEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when there is an additional text delta. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content_index` | 是 | `integer` | - | The index of the content part that the text delta was added to. | +| `delta` | 是 | `string` | - | The text delta that was added. | +| `item_id` | 是 | `string` | - | The ID of the output item that the text delta was added to. | +| `logprobs` | 是 | `array` | - | The log probabilities of the tokens in the delta. | +| `output_index` | 是 | `integer` | - | The index of the output item that the text delta was added to. | +| `sequence_number` | 是 | `integer` | - | The sequence number for this event. | +| `type` | 是 | `string` | `response.output_text.delta` | The type of the event. Always response.output_text.delta. | + +### `ResponseTextDoneEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when text content is finalized. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content_index` | 是 | `integer` | - | The index of the content part that the text content is finalized. | +| `item_id` | 是 | `string` | - | The ID of the output item that the text content is finalized. | +| `logprobs` | 是 | `array` | - | The log probabilities of the tokens in the delta. | +| `output_index` | 是 | `integer` | - | The index of the output item that the text content is finalized. | +| `sequence_number` | 是 | `integer` | - | The sequence number for this event. | +| `text` | 是 | `string` | - | The text content that is finalized. | +| `type` | 是 | `string` | `response.output_text.done` | The type of the event. Always response.output_text.done. | + +### `ResponseTextParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Configuration options for a text response from the model. Can be plain text or structured JSON data. Learn more: - [Text inputs and outputs](/docs/guides/text) - [Structured Outpu… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `format` | 否 | `TextResponseFormatConfiguration` | - | - | +| `verbosity` | 否 | `Verbosity` | - | - | + +### `ResponseUsage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Represents token usage details including input tokens, output tokens, a breakdown of output tokens, and the total tokens used. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `input_tokens` | 是 | `integer` | - | The number of input tokens. | +| `input_tokens_details` | 是 | `object` | - | A detailed breakdown of the input tokens. | +| `output_tokens` | 是 | `integer` | - | The number of output tokens. | +| `output_tokens_details` | 是 | `object` | - | A detailed breakdown of the output tokens. | +| `total_tokens` | 是 | `integer` | - | The total number of tokens used. | + +### `ResponseWebSearchCallCompletedEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a web search call is completed. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | Unique ID for the output item associated with the web search call. | +| `output_index` | 是 | `integer` | - | The index of the output item that the web search call is associated with. | +| `sequence_number` | 是 | `integer` | - | The sequence number of the web search call being processed. | +| `type` | 是 | `string` | `response.web_search_call.completed` | The type of the event. Always response.web_search_call.completed. | + +### `ResponseWebSearchCallInProgressEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a web search call is initiated. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | Unique ID for the output item associated with the web search call. | +| `output_index` | 是 | `integer` | - | The index of the output item that the web search call is associated with. | +| `sequence_number` | 是 | `integer` | - | The sequence number of the web search call being processed. | +| `type` | 是 | `string` | `response.web_search_call.in_progress` | The type of the event. Always response.web_search_call.in_progress. | + +### `ResponseWebSearchCallSearchingEvent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Emitted when a web search call is executing. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `item_id` | 是 | `string` | - | Unique ID for the output item associated with the web search call. | +| `output_index` | 是 | `integer` | - | The index of the output item that the web search call is associated with. | +| `sequence_number` | 是 | `integer` | - | The sequence number of the web search call being processed. | +| `type` | 是 | `string` | `response.web_search_call.searching` | The type of the event. Always response.web_search_call.searching. | + +### `ScreenshotParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A screenshot action. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `screenshot` | Specifies the event type. For a screenshot action, this property is always set to screenshot. | + +### `ScrollParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A scroll action. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `keys` | 否 | `array \| null` | - | - | +| `scroll_x` | 是 | `integer` | - | The horizontal scroll distance. | +| `scroll_y` | 是 | `integer` | - | The vertical scroll distance. | +| `type` | 是 | `string` | `scroll` | Specifies the event type. For a scroll action, this property is always set to scroll. | +| `x` | 是 | `integer` | - | The x-coordinate where the scroll occurred. | +| `y` | 是 | `integer` | - | The y-coordinate where the scroll occurred. | + +### `SearchContentType` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `SearchContextSize` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `ServiceTier` + +| 项 | 值 | +| --- | --- | +| 类型 | `string \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `string` | Specifies the processing type used for serving the request. - If set to 'auto', then the request will be processed with the service tier configured in the Project settings. Unless… | +| 2 | `null` | - | + +### `ServiceTierEnum` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `SkillReferenceParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `skill_id` | 是 | `string` | - | The ID of the referenced skill. | +| `type` | 是 | `string` | `skill_reference` | References a skill created with the /v1/skills endpoint. | +| `version` | 否 | `string` | - | Optional skill version. Use a positive integer or 'latest'. Omit for default. | + +### `SpecificApplyPatchParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Forces the model to call the apply_patch tool when executing a tool call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `apply_patch` | The tool to call. Always apply_patch. | + +### `SpecificFunctionShellParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Forces the model to call the shell tool when a tool call is required. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `shell` | The tool to call. Always shell. | + +### `StopConfiguration` + +| 项 | 值 | +| --- | --- | +| 类型 | `string \| array` | +| 说明 | Not supported with latest reasoning models o3 and o4-mini. Up to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequenc… | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `string` | - | +| 2 | `array` | - | + +### `SummaryTextContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A summary text from the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `text` | 是 | `string` | - | A summary of the reasoning output from the model so far. | +| `type` | 是 | `string` | `summary_text` | The type of the object. Always summary_text. | + +### `TextContent` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A text content. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `text` | 是 | `string` | - | - | +| `type` | 是 | `string` | `text` | - | + +### `TextResponseFormatConfiguration` + +| 项 | 值 | +| --- | --- | +| 类型 | `ResponseFormatText \| TextResponseFormatJsonSchema \| ResponseFormatJsonObject` | +| 说明 | An object specifying the format that the model must output. Configuring { "type": "json_schema" } enables Structured Outputs, which ensures the model will match your supplied JSON… | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ResponseFormatText` | - | +| 2 | `TextResponseFormatJsonSchema` | - | +| 3 | `ResponseFormatJsonObject` | - | + +### `TextResponseFormatJsonSchema` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | JSON Schema response format. Used to generate structured JSON responses. Learn more about [Structured Outputs](/docs/guides/structured-outputs). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `description` | 否 | `string` | - | A description of what the response format is for, used by the model to determine how to respond in the format. | +| `name` | 是 | `string` | - | The name of the response format. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. | +| `schema` | 是 | `ResponseFormatJsonSchemaSchema` | - | - | +| `strict` | 否 | `boolean \| null` | - | - | +| `type` | 是 | `string` | `json_schema` | The type of response format being defined. Always json_schema. | + +### `Tool` + +| 项 | 值 | +| --- | --- | +| 类型 | `FunctionTool \| FileSearchTool \| ComputerTool \| ComputerUsePreviewTool \| WebSearchTool \| MCPTool \| CodeInterpreterTool \| ImageGenTool … (+7)` | +| 说明 | A tool that can be used to generate a response. | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `FunctionTool` | - | +| 2 | `FileSearchTool` | - | +| 3 | `ComputerTool` | - | +| 4 | `ComputerUsePreviewTool` | - | +| 5 | `WebSearchTool` | - | +| 6 | `MCPTool` | - | +| 7 | `CodeInterpreterTool` | - | +| 8 | `ImageGenTool` | - | +| 9 | `LocalShellToolParam` | - | +| 10 | `FunctionShellToolParam` | - | +| 11 | `CustomToolParam` | - | +| 12 | `NamespaceToolParam` | - | +| 13 | `ToolSearchToolParam` | - | +| 14 | `WebSearchPreviewTool` | - | +| 15 | `ApplyPatchToolParam` | - | + +### `ToolChoiceAllowed` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Constrains the tools available to the model to a pre-defined set. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `mode` | 是 | `string` | `auto`, `required` | Constrains the tools available to the model to a pre-defined set. auto allows the model to pick from among the allowed tools and generate a message. required requires the model to… | +| `tools` | 是 | `array` | - | A list of tool definitions that the model should be allowed to call. For the Responses API, the list of tool definitions might look like: | +| `type` | 是 | `string` | `allowed_tools` | Allowed tool configuration type. Always allowed_tools. | + +### `ToolChoiceCustom` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Use this option to force the model to call a specific custom tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `name` | 是 | `string` | - | The name of the custom tool to call. | +| `type` | 是 | `string` | `custom` | For custom tool calling, the type is always custom. | + +### `ToolChoiceFunction` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Use this option to force the model to call a specific function. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `name` | 是 | `string` | - | The name of the function to call. | +| `type` | 是 | `string` | `function` | For function calling, the type is always function. | + +### `ToolChoiceMCP` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Use this option to force the model to call a specific tool on a remote MCP server. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `name` | 否 | `string \| null` | - | - | +| `server_label` | 是 | `string` | - | The label of the MCP server to use. | +| `type` | 是 | `string` | `mcp` | For MCP tools, the type is always mcp. | + +### `ToolChoiceOptions` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | Controls which (if any) tool is called by the model. none means the model will not call any tool and instead generates a message. auto means the model can pick between generating … | + +### `ToolChoiceParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `ToolChoiceOptions \| ToolChoiceAllowed \| ToolChoiceTypes \| ToolChoiceFunction \| ToolChoiceMCP \| ToolChoiceCustom \| SpecificApplyPatchParam \| SpecificFunctionShellParam` | +| 说明 | How the model should select which tool (or tools) to use when generating a response. See the tools parameter to see how to specify which tools the model can call. | +| 组合 | `oneOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `ToolChoiceOptions` | - | +| 2 | `ToolChoiceAllowed` | - | +| 3 | `ToolChoiceTypes` | - | +| 4 | `ToolChoiceFunction` | - | +| 5 | `ToolChoiceMCP` | - | +| 6 | `ToolChoiceCustom` | - | +| 7 | `SpecificApplyPatchParam` | - | +| 8 | `SpecificFunctionShellParam` | - | + +### `ToolChoiceTypes` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Indicates that the model should use a built-in tool to generate a response. [Learn more about built-in tools](/docs/guides/tools). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `file_search`, `web_search_preview`, `computer`, `computer_use_preview`, `computer_use`, `web_search_preview_2025_03_11`, `image_generation`, `code_interpreter` | The type of hosted tool the model should to use. Learn more about [built-in tools](/docs/guides/tools). Allowed values are: - file_search - web_search_preview - computer - compute… | + +### `ToolSearchCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `arguments` | 是 | `object/value` | - | Arguments used for the tool search call. | +| `call_id` | 是 | `string \| null` | - | - | +| `created_by` | 否 | `string` | - | The identifier of the actor that created the item. | +| `execution` | 是 | `ToolSearchExecutionType` | - | Whether tool search was executed by the server or by the client. | +| `id` | 是 | `string` | - | The unique ID of the tool search call item. | +| `status` | 是 | `FunctionCallStatus` | - | The status of the tool search call item that was recorded. | +| `type` | 是 | `string` | `tool_search_call` | The type of the item. Always tool_search_call. | + +### `ToolSearchCallItemParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `arguments` | 是 | `EmptyModelParam` | - | The arguments supplied to the tool search call. | +| `call_id` | 否 | `string \| null` | - | - | +| `execution` | 否 | `ToolSearchExecutionType` | - | Whether tool search was executed by the server or by the client. | +| `id` | 否 | `string \| null` | - | - | +| `status` | 否 | `FunctionCallItemStatus \| null` | - | - | +| `type` | 是 | `string` | `tool_search_call` | The item type. Always tool_search_call. | + +### `ToolSearchExecutionType` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | - | + +### `ToolSearchOutput` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 是 | `string \| null` | - | - | +| `created_by` | 否 | `string` | - | The identifier of the actor that created the item. | +| `execution` | 是 | `ToolSearchExecutionType` | - | Whether tool search was executed by the server or by the client. | +| `id` | 是 | `string` | - | The unique ID of the tool search output item. | +| `status` | 是 | `FunctionCallOutputStatusEnum` | - | The status of the tool search output item that was recorded. | +| `tools` | 是 | `array` | - | The loaded tool definitions returned by tool search. | +| `type` | 是 | `string` | `tool_search_output` | The type of the item. Always tool_search_output. | + +### `ToolSearchOutputItemParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | - | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `call_id` | 否 | `string \| null` | - | - | +| `execution` | 否 | `ToolSearchExecutionType` | - | Whether tool search was executed by the server or by the client. | +| `id` | 否 | `string \| null` | - | - | +| `status` | 否 | `FunctionCallItemStatus \| null` | - | - | +| `tools` | 是 | `array` | - | The loaded tool definitions returned by the tool search output. | +| `type` | 是 | `string` | `tool_search_output` | The item type. Always tool_search_output. | + +### `ToolSearchToolParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Hosted or BYOT tool search configuration for deferred tools. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `description` | 否 | `string \| null` | - | - | +| `execution` | 否 | `ToolSearchExecutionType` | - | Whether tool search is executed by the server or by the client. | +| `parameters` | 否 | `EmptyModelParam \| null` | - | - | +| `type` | 是 | `string` | `tool_search` | The type of the tool. Always tool_search. | + +### `ToolsArray` + +| 项 | 值 | +| --- | --- | +| 类型 | `array` | +| 说明 | An array of tools the model may call while generating a response. You can specify which tool to use by setting the tool_choice parameter. We support the following categories of to… | + +### `TopLogProb` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The top log probability of a token. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `bytes` | 是 | `array` | - | - | +| `logprob` | 是 | `number` | - | - | +| `token` | 是 | `string` | - | - | + +### `TypeParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An action to type in text. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `text` | 是 | `string` | - | The text to type. | +| `type` | 是 | `string` | `type` | Specifies the event type. For a type action, this property is always set to type. | + +### `UrlCitationBody` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A citation for a web resource used to generate a model response. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `end_index` | 是 | `integer` | - | The index of the last character of the URL citation in the message. | +| `start_index` | 是 | `integer` | - | The index of the first character of the URL citation in the message. | +| `title` | 是 | `string` | - | The title of the web resource. | +| `type` | 是 | `string` | `url_citation` | The type of the URL citation. Always url_citation. | +| `url` | 是 | `string(uri)` | - | The URL of the web resource. | + +### `VectorStoreFileAttributes` + +| 项 | 值 | +| --- | --- | +| 类型 | `object/map \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `object/map` | Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for object… | +| 2 | `null` | - | + +### `Verbosity` + +| 项 | 值 | +| --- | --- | +| 类型 | `string \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `string` | Constrains the verbosity of the model's response. Lower values will result in more concise responses, while higher values will result in more verbose responses. Currently supporte… | +| 2 | `null` | - | + +### `VoiceIdsOrCustomVoice` + +| 项 | 值 | +| --- | --- | +| 类型 | `VoiceIdsShared \| object` | +| 说明 | A built-in voice name or a custom voice reference. | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `VoiceIdsShared` | - | +| 2 | `object` | Custom voice reference. | + +### `VoiceIdsShared` + +| 项 | 值 | +| --- | --- | +| 类型 | `string \| string` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `string` | - | +| 2 | `string` | - | + +### `WaitParam` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A wait action. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `wait` | Specifies the event type. For a wait action, this property is always set to wait. | + +### `WebSearchActionFind` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Action type "find_in_page": Searches for a pattern within a loaded page. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `pattern` | 是 | `string` | - | The pattern or text to search for within the page. | +| `type` | 是 | `string` | `find_in_page` | The action type. | +| `url` | 是 | `string(uri)` | - | The URL of the page searched for the pattern. | + +### `WebSearchActionOpenPage` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Action type "open_page" - Opens a specific URL from search results. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `type` | 是 | `string` | `open_page` | The action type. | +| `url` | 否 | `string(uri) \| null` | - | The URL opened by the model. | + +### `WebSearchActionSearch` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Action type "search" - Performs a web search query. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `queries` | 否 | `array` | - | The search queries. | +| `query` | 否 | `string` | - | The search query. | +| `sources` | 否 | `array` | - | The sources used in the search. | +| `type` | 是 | `string` | `search` | The action type. | + +### `WebSearchApproximateLocation` + +| 项 | 值 | +| --- | --- | +| 类型 | `object \| null` | +| 说明 | - | +| 组合 | `anyOf` | + +| 变体 | 类型 | 说明 | +| --- | --- | --- | +| 1 | `object` | The approximate location of the user. | +| 2 | `null` | - | + +### `WebSearchContextSize` + +| 项 | 值 | +| --- | --- | +| 类型 | `string` | +| 说明 | High level guidance for the amount of context window space to use for the search. One of low, medium, or high. medium is the default. | + +### `WebSearchLocation` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Approximate location parameters for the search. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `city` | 否 | `string` | - | Free text input for the city of the user, e.g. San Francisco. | +| `country` | 否 | `string` | - | The two-letter [ISO country code](https://en.wikipedia.org/wiki/ISO_3166-1) of the user, e.g. US. | +| `region` | 否 | `string` | - | Free text input for the region of the user, e.g. California. | +| `timezone` | 否 | `string` | - | The [IANA timezone](https://timeapi.io/documentation/iana-timezones) of the user, e.g. America/Los_Angeles. | + +### `WebSearchPreviewTool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | This tool searches the web for relevant results to use in a response. Learn more about the [web search tool](https://platform.openai.com/docs/guides/tools-web-search). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `search_content_types` | 否 | `array` | - | - | +| `search_context_size` | 否 | `SearchContextSize` | - | High level guidance for the amount of context window space to use for the search. One of low, medium, or high. medium is the default. | +| `type` | 是 | `string` | `web_search_preview`, `web_search_preview_2025_03_11` | The type of the web search tool. One of web_search_preview or web_search_preview_2025_03_11. | +| `user_location` | 否 | `ApproximateLocation \| null` | - | - | + +### `WebSearchTool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Search the Internet for sources related to the prompt. Learn more about the [web search tool](/docs/guides/tools-web-search). | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `filters` | 否 | `object \| null` | - | - | +| `search_context_size` | 否 | `string` | `low`, `medium`, `high` | High level guidance for the amount of context window space to use for the search. One of low, medium, or high. medium is the default. | +| `type` | 是 | `string` | `web_search`, `web_search_2025_08_26` | The type of the web search tool. One of web_search or web_search_2025_08_26. | +| `user_location` | 否 | `WebSearchApproximateLocation` | - | - | + +### `WebSearchToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The results of a web search tool call. See the [web search guide](/docs/guides/tools-web-search) for more information. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `action` | 是 | `WebSearchActionSearch \| WebSearchActionOpenPage \| WebSearchActionFind` | - | An object describing the specific action taken in this web search call. Includes details on how the model used the web (search, open_page, find_in_page). | +| `id` | 是 | `string` | - | The unique ID of the web search tool call. | +| `status` | 是 | `string` | `in_progress`, `searching`, `completed`, `failed` | The status of the web search tool call. | +| `type` | 是 | `string` | `web_search_call` | The type of the web search tool call. Always web_search_call. | + +## Claude / Anthropic Endpoints + +| Method | Path | Request schema | Response schema | 说明 | +| --- | --- | --- | --- | --- | +| POST | `/v1/messages` | `MessageCreateParams` | `Message` 或 `RawMessageStreamEvent` | 创建非流式或 SSE 流式消息 | +| POST | `/v1/messages/count_tokens` | `MessageCountTokensParams` | `MessageTokensCount` | 仅计数,不生成消息 | + +## Claude / Anthropic TypeScript 字段表 + +以下类型从 Anthropic 官方 TypeScript SDK 的 Messages 资源类型递归引用得到,共 164 个。TypeScript union 中的 server tool 版本号是官方 SDK 暴露的字面量类型,实际可用性仍取决于 Anthropic 账号、模型和 beta 配置。 + +### `Base64ImageSource` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `data` | 是 | `string` | +| `media_type` | 是 | `'image/jpeg' \| 'image/png' \| 'image/gif' \| 'image/webp'` | +| `type` | 是 | `'base64'` | + +### `Base64PDFSource` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `data` | 是 | `string` | +| `media_type` | 是 | `'application/pdf'` | +| `type` | 是 | `'base64'` | + +### `BashCodeExecutionOutputBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `file_id` | 是 | `string` | +| `type` | 是 | `'bash_code_execution_output'` | + +### `BashCodeExecutionOutputBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `file_id` | 是 | `string` | +| `type` | 是 | `'bash_code_execution_output'` | + +### `BashCodeExecutionResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `Array` | +| `return_code` | 是 | `number` | +| `stderr` | 是 | `string` | +| `stdout` | 是 | `string` | +| `type` | 是 | `'bash_code_execution_result'` | + +### `BashCodeExecutionResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `Array` | +| `return_code` | 是 | `number` | +| `stderr` | 是 | `string` | +| `stdout` | 是 | `string` | +| `type` | 是 | `'bash_code_execution_result'` | + +### `BashCodeExecutionToolResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `BashCodeExecutionToolResultError \| BashCodeExecutionResultBlock` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'bash_code_execution_tool_result'` | + +### `BashCodeExecutionToolResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `BashCodeExecutionToolResultErrorParam \| BashCodeExecutionResultBlockParam` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'bash_code_execution_tool_result'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | + +### `BashCodeExecutionToolResultError` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `BashCodeExecutionToolResultErrorCode` | +| `type` | 是 | `'bash_code_execution_tool_result_error'` | + +### `BashCodeExecutionToolResultErrorCode` + +类型别名:`\| 'invalid_tool_input' \| 'unavailable' \| 'too_many_requests' \| 'execution_time_exceeded' \| 'output_file_too_large'` + +### `BashCodeExecutionToolResultErrorParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `BashCodeExecutionToolResultErrorCode` | +| `type` | 是 | `'bash_code_execution_tool_result_error'` | + +### `CacheControlEphemeral` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'ephemeral'` | +| `ttl` | 否 | `'5m' \| '1h'` | + +### `CacheCreation` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `ephemeral_1h_input_tokens` | 是 | `number` | +| `ephemeral_5m_input_tokens` | 是 | `number` | + +### `CitationCharLocation` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cited_text` | 是 | `string` | +| `document_index` | 是 | `number` | +| `document_title` | 是 | `string \| null` | +| `end_char_index` | 是 | `number` | +| `file_id` | 是 | `string \| null` | +| `start_char_index` | 是 | `number` | +| `type` | 是 | `'char_location'` | + +### `CitationCharLocationParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cited_text` | 是 | `string` | +| `document_index` | 是 | `number` | +| `document_title` | 是 | `string \| null` | +| `end_char_index` | 是 | `number` | +| `start_char_index` | 是 | `number` | +| `type` | 是 | `'char_location'` | + +### `CitationContentBlockLocation` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cited_text` | 是 | `string` | +| `document_index` | 是 | `number` | +| `document_title` | 是 | `string \| null` | +| `end_block_index` | 是 | `number` | +| `file_id` | 是 | `string \| null` | +| `start_block_index` | 是 | `number` | +| `type` | 是 | `'content_block_location'` | + +### `CitationContentBlockLocationParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cited_text` | 是 | `string` | +| `document_index` | 是 | `number` | +| `document_title` | 是 | `string \| null` | +| `end_block_index` | 是 | `number` | +| `start_block_index` | 是 | `number` | +| `type` | 是 | `'content_block_location'` | + +### `CitationPageLocation` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cited_text` | 是 | `string` | +| `document_index` | 是 | `number` | +| `document_title` | 是 | `string \| null` | +| `end_page_number` | 是 | `number` | +| `file_id` | 是 | `string \| null` | +| `start_page_number` | 是 | `number` | +| `type` | 是 | `'page_location'` | + +### `CitationPageLocationParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cited_text` | 是 | `string` | +| `document_index` | 是 | `number` | +| `document_title` | 是 | `string \| null` | +| `end_page_number` | 是 | `number` | +| `start_page_number` | 是 | `number` | +| `type` | 是 | `'page_location'` | + +### `CitationSearchResultLocationParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cited_text` | 是 | `string` | +| `end_block_index` | 是 | `number` | +| `search_result_index` | 是 | `number` | +| `source` | 是 | `string` | +| `start_block_index` | 是 | `number` | +| `title` | 是 | `string \| null` | +| `type` | 是 | `'search_result_location'` | + +### `CitationWebSearchResultLocationParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cited_text` | 是 | `string` | +| `encrypted_index` | 是 | `string` | +| `title` | 是 | `string \| null` | +| `type` | 是 | `'web_search_result_location'` | +| `url` | 是 | `string` | + +### `CitationsConfig` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `enabled` | 是 | `boolean` | + +### `CitationsConfigParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `enabled` | 否 | `boolean` | + +### `CitationsDelta` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `citation` | 是 | `\| CitationCharLocation \| CitationPageLocation \| CitationContentBlockLocation \| CitationsWebSearchResultLocation \| CitationsSearchResultLocation` | +| `type` | 是 | `'citations_delta'` | + +### `CitationsSearchResultLocation` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cited_text` | 是 | `string` | +| `end_block_index` | 是 | `number` | +| `search_result_index` | 是 | `number` | +| `source` | 是 | `string` | +| `start_block_index` | 是 | `number` | +| `title` | 是 | `string \| null` | +| `type` | 是 | `'search_result_location'` | + +### `CitationsWebSearchResultLocation` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cited_text` | 是 | `string` | +| `encrypted_index` | 是 | `string` | +| `title` | 是 | `string \| null` | +| `type` | 是 | `'web_search_result_location'` | +| `url` | 是 | `string` | + +### `CodeExecutionOutputBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `file_id` | 是 | `string` | +| `type` | 是 | `'code_execution_output'` | + +### `CodeExecutionOutputBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `file_id` | 是 | `string` | +| `type` | 是 | `'code_execution_output'` | + +### `CodeExecutionResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `Array` | +| `return_code` | 是 | `number` | +| `stderr` | 是 | `string` | +| `stdout` | 是 | `string` | +| `type` | 是 | `'code_execution_result'` | + +### `CodeExecutionResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `Array` | +| `return_code` | 是 | `number` | +| `stderr` | 是 | `string` | +| `stdout` | 是 | `string` | +| `type` | 是 | `'code_execution_result'` | + +### `CodeExecutionTool20250522` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'code_execution'` | +| `type` | 是 | `'code_execution_20250522'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `strict` | 否 | `boolean` | + +### `CodeExecutionTool20250825` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'code_execution'` | +| `type` | 是 | `'code_execution_20250825'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `strict` | 否 | `boolean` | + +### `CodeExecutionTool20260120` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'code_execution'` | +| `type` | 是 | `'code_execution_20260120'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `strict` | 否 | `boolean` | + +### `CodeExecutionToolResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `CodeExecutionToolResultBlockContent` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'code_execution_tool_result'` | + +### `CodeExecutionToolResultBlockContent` + +类型别名:`\| CodeExecutionToolResultError \| CodeExecutionResultBlock \| EncryptedCodeExecutionResultBlock` + +### `CodeExecutionToolResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `CodeExecutionToolResultBlockParamContent` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'code_execution_tool_result'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | + +### `CodeExecutionToolResultBlockParamContent` + +类型别名:`\| CodeExecutionToolResultErrorParam \| CodeExecutionResultBlockParam \| EncryptedCodeExecutionResultBlockParam` + +### `CodeExecutionToolResultError` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `CodeExecutionToolResultErrorCode` | +| `type` | 是 | `'code_execution_tool_result_error'` | + +### `CodeExecutionToolResultErrorCode` + +类型别名:`\| 'invalid_tool_input' \| 'unavailable' \| 'too_many_requests' \| 'execution_time_exceeded'` + +### `CodeExecutionToolResultErrorParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `CodeExecutionToolResultErrorCode` | +| `type` | 是 | `'code_execution_tool_result_error'` | + +### `Container` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `id` | 是 | `string` | +| `expires_at` | 是 | `string` | + +### `ContainerUploadBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `file_id` | 是 | `string` | +| `type` | 是 | `'container_upload'` | + +### `ContainerUploadBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `file_id` | 是 | `string` | +| `type` | 是 | `'container_upload'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | + +### `ContentBlock` + +类型别名:`\| TextBlock \| ThinkingBlock \| RedactedThinkingBlock \| ToolUseBlock \| ServerToolUseBlock \| WebSearchToolResultBlock \| WebFetchToolResultBlock \| CodeExecutionToolResultBlock \| BashCodeExecutionToolResultBlock \| TextEditorCodeExecutionToolResultBlock \| ToolSearchToolResultBlock \| ContainerUploadBlock` + +### `ContentBlockParam` + +类型别名:`\| TextBlockParam \| ImageBlockParam \| DocumentBlockParam \| SearchResultBlockParam \| ThinkingBlockParam \| RedactedThinkingBlockParam \| ToolUseBlockParam \| ToolResultBlockParam \| ServerToolUseBlockParam \| WebSearchToolResultBlockParam \| WebFetchToolResultBlockParam \| CodeExecutionToolResultBlockParam \| BashCodeExecutionToolResultBlockParam \| TextEditorCodeExecutionToolResultBlockParam \| ToolSearchToolResultBlockParam \| ContainerUploadBlockParam \| MidConversationSystemBlockParam` + +### `ContentBlockSource` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `string \| Array` | +| `type` | 是 | `'content'` | + +### `ContentBlockSourceContent` + +类型别名:`TextBlockParam \| ImageBlockParam` + +### `DirectCaller` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'direct'` | + +### `DocumentBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `citations` | 是 | `CitationsConfig \| null` | +| `source` | 是 | `Base64PDFSource \| PlainTextSource` | +| `title` | 是 | `string \| null` | +| `type` | 是 | `'document'` | + +### `DocumentBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `source` | 是 | `Base64PDFSource \| PlainTextSource \| ContentBlockSource \| URLPDFSource` | +| `type` | 是 | `'document'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `citations` | 否 | `CitationsConfigParam \| null` | +| `context` | 否 | `string \| null` | +| `title` | 否 | `string \| null` | + +### `EncryptedCodeExecutionResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `Array` | +| `encrypted_stdout` | 是 | `string` | +| `return_code` | 是 | `number` | +| `stderr` | 是 | `string` | +| `type` | 是 | `'encrypted_code_execution_result'` | + +### `EncryptedCodeExecutionResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `Array` | +| `encrypted_stdout` | 是 | `string` | +| `return_code` | 是 | `number` | +| `stderr` | 是 | `string` | +| `type` | 是 | `'encrypted_code_execution_result'` | + +### `ImageBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `source` | 是 | `Base64ImageSource \| URLImageSource` | +| `type` | 是 | `'image'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | + +### `InputJSONDelta` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `partial_json` | 是 | `string` | +| `type` | 是 | `'input_json_delta'` | + +### `JSONOutputFormat` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `schema` | 是 | `{ [key: string]: unknown }` | +| `type` | 是 | `'json_schema'` | + +### `MemoryTool20250818` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'memory'` | +| `type` | 是 | `'memory_20250818'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | +| `strict` | 否 | `boolean` | + +### `Message` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `id` | 是 | `string` | +| `container` | 是 | `Container \| null` | +| `content` | 是 | `Array` | +| `model` | 是 | `Model` | +| `role` | 是 | `'assistant'` | +| `stop_details` | 是 | `RefusalStopDetails \| null` | +| `stop_reason` | 是 | `StopReason \| null` | +| `stop_sequence` | 是 | `string \| null` | +| `type` | 是 | `'message'` | +| `usage` | 是 | `Usage` | + +### `MessageCountTokensParams` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `messages` | 是 | `Array` | +| `model` | 是 | `Model` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `output_config` | 否 | `OutputConfig` | +| `system` | 否 | `string \| Array` | +| `thinking` | 否 | `ThinkingConfigParam` | +| `tool_choice` | 否 | `ToolChoice` | +| `tools` | 否 | `Array` | + +### `MessageCountTokensTool` + +类型别名:`\| Tool \| ToolBash20250124 \| CodeExecutionTool20250522 \| CodeExecutionTool20250825 \| CodeExecutionTool20260120 \| MemoryTool20250818 \| ToolTextEditor20250124 \| ToolTextEditor20250429 \| ToolTextEditor20250728 \| WebSearchTool20250305 \| WebFetchTool20250910 \| WebSearchTool20260209 \| WebFetchTool20260209 \| WebFetchTool20260309 \| ToolSearchToolBm25_20251119 \| ToolSearchToolRegex20251119` + +### `MessageCreateParams` + +类型别名:`MessageCreateParamsNonStreaming \| MessageCreateParamsStreaming` + +### `MessageCreateParamsBase` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `max_tokens` | 是 | `number` | +| `messages` | 是 | `Array` | +| `model` | 是 | `Model` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `container` | 否 | `string \| null` | +| `inference_geo` | 否 | `string \| null` | +| `metadata` | 否 | `Metadata` | +| `output_config` | 否 | `OutputConfig` | +| `service_tier` | 否 | `'auto' \| 'standard_only'` | +| `stop_sequences` | 否 | `Array` | +| `stream` | 否 | `boolean` | +| `system` | 否 | `string \| Array` | +| `temperature` | 否 | `number` | +| `thinking` | 否 | `ThinkingConfigParam` | +| `tool_choice` | 否 | `ToolChoice` | +| `tools` | 否 | `Array` | +| `top_k` | 否 | `number` | +| `top_p` | 否 | `number` | + +### `MessageCreateParamsNonStreaming` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `stream` | 否 | `false` | + +### `MessageCreateParamsStreaming` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `stream` | 是 | `true` | + +### `MessageDeltaUsage` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cache_creation_input_tokens` | 是 | `number \| null` | +| `cache_read_input_tokens` | 是 | `number \| null` | +| `input_tokens` | 是 | `number \| null` | +| `output_tokens` | 是 | `number` | +| `output_tokens_details` | 是 | `OutputTokensDetails \| null` | +| `server_tool_use` | 是 | `ServerToolUsage \| null` | + +### `MessageParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `string \| Array` | +| `role` | 是 | `'user' \| 'assistant' \| 'system'` | + +### `MessageTokensCount` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `input_tokens` | 是 | `number` | + +### `Metadata` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `user_id` | 否 | `string \| null` | + +### `MidConversationSystemBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `Array` | +| `type` | 是 | `'mid_conv_system'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | + +### `Model` + +类型别名:`\| 'claude-opus-4-8' \| 'claude-opus-4-7' \| 'claude-mythos-preview' \| 'claude-opus-4-6' \| 'claude-sonnet-4-6' \| 'claude-haiku-4-5' \| 'claude-haiku-4-5-20251001' \| 'claude-opus-4-5' \| 'claude-opus-4-5-20251101' \| 'claude-sonnet-4-5' \| 'claude-sonnet-4-5-20250929' \| 'claude-opus-4-1' \| 'claude-opus-4-1-20250805' \| 'claude-opus-4-0' \| 'claude-opus-4-20250514' \| 'claude-sonnet-4-0' \| 'claude-sonnet-4-20250514' \| 'claude-3-haiku-20240307' \| (string & {})` + +### `OutputConfig` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `effort` | 否 | `'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max' \| null` | +| `format` | 否 | `JSONOutputFormat \| null` | + +### `OutputTokensDetails` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `thinking_tokens` | 是 | `number` | + +### `PlainTextSource` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `data` | 是 | `string` | +| `media_type` | 是 | `'text/plain'` | +| `type` | 是 | `'text'` | + +### `RawContentBlockDelta` + +类型别名:`\| TextDelta \| InputJSONDelta \| CitationsDelta \| ThinkingDelta \| SignatureDelta` + +### `RawContentBlockDeltaEvent` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `delta` | 是 | `RawContentBlockDelta` | +| `index` | 是 | `number` | +| `type` | 是 | `'content_block_delta'` | + +### `RawContentBlockStartEvent` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content_block` | 是 | `\| TextBlock \| ThinkingBlock \| RedactedThinkingBlock \| ToolUseBlock \| ServerToolUseBlock \| WebSearchToolResultBlock \| WebFetchToolResultBlock \| CodeExecutionToolResultBlock \| BashCodeExecutionToolResultBlock \| TextEditorCodeExecutionToolResultBlock \| ToolSearchToolResultBlock \| ContainerUploadBlock` | +| `index` | 是 | `number` | +| `type` | 是 | `'content_block_start'` | + +### `RawContentBlockStopEvent` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `index` | 是 | `number` | +| `type` | 是 | `'content_block_stop'` | + +### `RawMessageDeltaEvent` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `delta` | 是 | `RawMessageDeltaEvent.Delta` | +| `type` | 是 | `'message_delta'` | +| `usage` | 是 | `MessageDeltaUsage` | + +### `RawMessageStartEvent` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `message` | 是 | `Message` | +| `type` | 是 | `'message_start'` | + +### `RawMessageStopEvent` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'message_stop'` | + +### `RawMessageStreamEvent` + +类型别名:`\| RawMessageStartEvent \| RawMessageDeltaEvent \| RawMessageStopEvent \| RawContentBlockStartEvent \| RawContentBlockDeltaEvent \| RawContentBlockStopEvent` + +### `RedactedThinkingBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `data` | 是 | `string` | +| `type` | 是 | `'redacted_thinking'` | + +### `RedactedThinkingBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `data` | 是 | `string` | +| `type` | 是 | `'redacted_thinking'` | + +### `RefusalStopDetails` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `category` | 是 | `'cyber' \| 'bio' \| null` | +| `explanation` | 是 | `string \| null` | +| `type` | 是 | `'refusal'` | + +### `SearchResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `Array` | +| `source` | 是 | `string` | +| `title` | 是 | `string` | +| `type` | 是 | `'search_result'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `citations` | 否 | `CitationsConfigParam` | + +### `ServerToolCaller` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `tool_id` | 是 | `string` | +| `type` | 是 | `'code_execution_20250825'` | + +### `ServerToolCaller20260120` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `tool_id` | 是 | `string` | +| `type` | 是 | `'code_execution_20260120'` | + +### `ServerToolUsage` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `web_fetch_requests` | 是 | `number` | +| `web_search_requests` | 是 | `number` | + +### `ServerToolUseBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `id` | 是 | `string` | +| `caller` | 是 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | +| `input` | 是 | `unknown` | +| `name` | 是 | `\| 'web_search' \| 'web_fetch' \| 'code_execution' \| 'bash_code_execution' \| 'text_editor_code_execution' \| 'tool_search_tool_regex' \| 'tool_search_tool_bm25'` | +| `type` | 是 | `'server_tool_use'` | + +### `ServerToolUseBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `id` | 是 | `string` | +| `input` | 是 | `unknown` | +| `name` | 是 | `\| 'web_search' \| 'web_fetch' \| 'code_execution' \| 'bash_code_execution' \| 'text_editor_code_execution' \| 'tool_search_tool_regex' \| 'tool_search_tool_bm25'` | +| `type` | 是 | `'server_tool_use'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `caller` | 否 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | + +### `SignatureDelta` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `signature` | 是 | `string` | +| `type` | 是 | `'signature_delta'` | + +### `StopReason` + +类型别名:`'end_turn' \| 'max_tokens' \| 'stop_sequence' \| 'tool_use' \| 'pause_turn' \| 'refusal'` + +### `TextBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `citations` | 是 | `Array \| null` | +| `text` | 是 | `string` | +| `type` | 是 | `'text'` | + +### `TextBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `text` | 是 | `string` | +| `type` | 是 | `'text'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `citations` | 否 | `Array \| null` | + +### `TextCitation` + +类型别名:`\| CitationCharLocation \| CitationPageLocation \| CitationContentBlockLocation \| CitationsWebSearchResultLocation \| CitationsSearchResultLocation` + +### `TextCitationParam` + +类型别名:`\| CitationCharLocationParam \| CitationPageLocationParam \| CitationContentBlockLocationParam \| CitationWebSearchResultLocationParam \| CitationSearchResultLocationParam` + +### `TextDelta` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `text` | 是 | `string` | +| `type` | 是 | `'text_delta'` | + +### `TextEditorCodeExecutionCreateResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `is_file_update` | 是 | `boolean` | +| `type` | 是 | `'text_editor_code_execution_create_result'` | + +### `TextEditorCodeExecutionCreateResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `is_file_update` | 是 | `boolean` | +| `type` | 是 | `'text_editor_code_execution_create_result'` | + +### `TextEditorCodeExecutionStrReplaceResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `lines` | 是 | `Array \| null` | +| `new_lines` | 是 | `number \| null` | +| `new_start` | 是 | `number \| null` | +| `old_lines` | 是 | `number \| null` | +| `old_start` | 是 | `number \| null` | +| `type` | 是 | `'text_editor_code_execution_str_replace_result'` | + +### `TextEditorCodeExecutionStrReplaceResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'text_editor_code_execution_str_replace_result'` | +| `lines` | 否 | `Array \| null` | +| `new_lines` | 否 | `number \| null` | +| `new_start` | 否 | `number \| null` | +| `old_lines` | 否 | `number \| null` | +| `old_start` | 否 | `number \| null` | + +### `TextEditorCodeExecutionToolResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `\| TextEditorCodeExecutionToolResultError \| TextEditorCodeExecutionViewResultBlock \| TextEditorCodeExecutionCreateResultBlock \| TextEditorCodeExecutionStrReplaceResultBlock` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'text_editor_code_execution_tool_result'` | + +### `TextEditorCodeExecutionToolResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `\| TextEditorCodeExecutionToolResultErrorParam \| TextEditorCodeExecutionViewResultBlockParam \| TextEditorCodeExecutionCreateResultBlockParam \| TextEditorCodeExecutionStrReplaceResultBlockParam` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'text_editor_code_execution_tool_result'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | + +### `TextEditorCodeExecutionToolResultError` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `TextEditorCodeExecutionToolResultErrorCode` | +| `error_message` | 是 | `string \| null` | +| `type` | 是 | `'text_editor_code_execution_tool_result_error'` | + +### `TextEditorCodeExecutionToolResultErrorCode` + +类型别名:`\| 'invalid_tool_input' \| 'unavailable' \| 'too_many_requests' \| 'execution_time_exceeded' \| 'file_not_found'` + +### `TextEditorCodeExecutionToolResultErrorParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `TextEditorCodeExecutionToolResultErrorCode` | +| `type` | 是 | `'text_editor_code_execution_tool_result_error'` | +| `error_message` | 否 | `string \| null` | + +### `TextEditorCodeExecutionViewResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `string` | +| `file_type` | 是 | `'text' \| 'image' \| 'pdf'` | +| `num_lines` | 是 | `number \| null` | +| `start_line` | 是 | `number \| null` | +| `total_lines` | 是 | `number \| null` | +| `type` | 是 | `'text_editor_code_execution_view_result'` | + +### `TextEditorCodeExecutionViewResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `string` | +| `file_type` | 是 | `'text' \| 'image' \| 'pdf'` | +| `type` | 是 | `'text_editor_code_execution_view_result'` | +| `num_lines` | 否 | `number \| null` | +| `start_line` | 否 | `number \| null` | +| `total_lines` | 否 | `number \| null` | + +### `ThinkingBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `signature` | 是 | `string` | +| `thinking` | 是 | `string` | +| `type` | 是 | `'thinking'` | + +### `ThinkingBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `signature` | 是 | `string` | +| `thinking` | 是 | `string` | +| `type` | 是 | `'thinking'` | + +### `ThinkingConfigAdaptive` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'adaptive'` | +| `display` | 否 | `'summarized' \| 'omitted' \| null` | + +### `ThinkingConfigDisabled` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'disabled'` | + +### `ThinkingConfigEnabled` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `budget_tokens` | 是 | `number` | +| `type` | 是 | `'enabled'` | +| `display` | 否 | `'summarized' \| 'omitted' \| null` | + +### `ThinkingConfigParam` + +类型别名:`ThinkingConfigEnabled \| ThinkingConfigDisabled \| ThinkingConfigAdaptive` + +### `ThinkingDelta` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `thinking` | 是 | `string` | +| `type` | 是 | `'thinking_delta'` | + +### `Tool` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `input_schema` | 是 | `Tool.InputSchema` | +| `name` | 是 | `string` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `description` | 否 | `string` | +| `eager_input_streaming` | 否 | `boolean \| null` | +| `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | +| `strict` | 否 | `boolean` | +| `type` | 否 | `'custom' \| null` | + +### `ToolBash20250124` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'bash'` | +| `type` | 是 | `'bash_20250124'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | +| `strict` | 否 | `boolean` | + +### `ToolChoice` + +类型别名:`ToolChoiceAuto \| ToolChoiceAny \| ToolChoiceTool \| ToolChoiceNone` + +### `ToolChoiceAny` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'any'` | +| `disable_parallel_tool_use` | 否 | `boolean` | + +### `ToolChoiceAuto` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'auto'` | +| `disable_parallel_tool_use` | 否 | `boolean` | + +### `ToolChoiceNone` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'none'` | + +### `ToolChoiceTool` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `string` | +| `type` | 是 | `'tool'` | +| `disable_parallel_tool_use` | 否 | `boolean` | + +### `ToolReferenceBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `tool_name` | 是 | `string` | +| `type` | 是 | `'tool_reference'` | + +### `ToolReferenceBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `tool_name` | 是 | `string` | +| `type` | 是 | `'tool_reference'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | + +### `ToolResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'tool_result'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `content` | 否 | `\| string \| Array< \| TextBlockParam \| ImageBlockParam \| SearchResultBlockParam \| DocumentBlockParam \| ToolReferenceBlockParam >` | +| `is_error` | 否 | `boolean` | + +### `ToolSearchToolBm25_20251119` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'tool_search_tool_bm25'` | +| `type` | 是 | `'tool_search_tool_bm25_20251119' \| 'tool_search_tool_bm25'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `strict` | 否 | `boolean` | + +### `ToolSearchToolRegex20251119` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'tool_search_tool_regex'` | +| `type` | 是 | `'tool_search_tool_regex_20251119' \| 'tool_search_tool_regex'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `strict` | 否 | `boolean` | + +### `ToolSearchToolResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `ToolSearchToolResultError \| ToolSearchToolSearchResultBlock` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'tool_search_tool_result'` | + +### `ToolSearchToolResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `ToolSearchToolResultErrorParam \| ToolSearchToolSearchResultBlockParam` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'tool_search_tool_result'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | + +### `ToolSearchToolResultError` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `ToolSearchToolResultErrorCode` | +| `error_message` | 是 | `string \| null` | +| `type` | 是 | `'tool_search_tool_result_error'` | + +### `ToolSearchToolResultErrorCode` + +类型别名:`\| 'invalid_tool_input' \| 'unavailable' \| 'too_many_requests' \| 'execution_time_exceeded'` + +### `ToolSearchToolResultErrorParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `ToolSearchToolResultErrorCode` | +| `type` | 是 | `'tool_search_tool_result_error'` | + +### `ToolSearchToolSearchResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `tool_references` | 是 | `Array` | +| `type` | 是 | `'tool_search_tool_search_result'` | + +### `ToolSearchToolSearchResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `tool_references` | 是 | `Array` | +| `type` | 是 | `'tool_search_tool_search_result'` | + +### `ToolTextEditor20250124` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'str_replace_editor'` | +| `type` | 是 | `'text_editor_20250124'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | +| `strict` | 否 | `boolean` | + +### `ToolTextEditor20250429` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'str_replace_based_edit_tool'` | +| `type` | 是 | `'text_editor_20250429'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | +| `strict` | 否 | `boolean` | + +### `ToolTextEditor20250728` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'str_replace_based_edit_tool'` | +| `type` | 是 | `'text_editor_20250728'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `input_examples` | 否 | `Array<{ [key: string]: unknown }>` | +| `max_characters` | 否 | `number \| null` | +| `strict` | 否 | `boolean` | + +### `ToolUnion` + +类型别名:`\| Tool \| ToolBash20250124 \| CodeExecutionTool20250522 \| CodeExecutionTool20250825 \| CodeExecutionTool20260120 \| MemoryTool20250818 \| ToolTextEditor20250124 \| ToolTextEditor20250429 \| ToolTextEditor20250728 \| WebSearchTool20250305 \| WebFetchTool20250910 \| WebSearchTool20260209 \| WebFetchTool20260209 \| WebFetchTool20260309 \| ToolSearchToolBm25_20251119 \| ToolSearchToolRegex20251119` + +### `ToolUseBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `id` | 是 | `string` | +| `caller` | 是 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | +| `input` | 是 | `unknown` | +| `name` | 是 | `string` | +| `type` | 是 | `'tool_use'` | + +### `ToolUseBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `id` | 是 | `string` | +| `input` | 是 | `unknown` | +| `name` | 是 | `string` | +| `type` | 是 | `'tool_use'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `caller` | 否 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | + +### `URLImageSource` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'url'` | +| `url` | 是 | `string` | + +### `URLPDFSource` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'url'` | +| `url` | 是 | `string` | + +### `Usage` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `cache_creation` | 是 | `CacheCreation \| null` | +| `cache_creation_input_tokens` | 是 | `number \| null` | +| `cache_read_input_tokens` | 是 | `number \| null` | +| `inference_geo` | 是 | `string \| null` | +| `input_tokens` | 是 | `number` | +| `output_tokens` | 是 | `number` | +| `output_tokens_details` | 是 | `OutputTokensDetails \| null` | +| `server_tool_use` | 是 | `ServerToolUsage \| null` | +| `service_tier` | 是 | `'standard' \| 'priority' \| 'batch' \| null` | + +### `UserLocation` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `type` | 是 | `'approximate'` | +| `city` | 否 | `string \| null` | +| `country` | 否 | `string \| null` | +| `region` | 否 | `string \| null` | +| `timezone` | 否 | `string \| null` | + +### `WebFetchBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `DocumentBlock` | +| `retrieved_at` | 是 | `string \| null` | +| `type` | 是 | `'web_fetch_result'` | +| `url` | 是 | `string` | + +### `WebFetchBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `DocumentBlockParam` | +| `type` | 是 | `'web_fetch_result'` | +| `url` | 是 | `string` | +| `retrieved_at` | 否 | `string \| null` | + +### `WebFetchTool20250910` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'web_fetch'` | +| `type` | 是 | `'web_fetch_20250910'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `allowed_domains` | 否 | `Array \| null` | +| `blocked_domains` | 否 | `Array \| null` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `citations` | 否 | `CitationsConfigParam \| null` | +| `defer_loading` | 否 | `boolean` | +| `max_content_tokens` | 否 | `number \| null` | +| `max_uses` | 否 | `number \| null` | +| `strict` | 否 | `boolean` | + +### `WebFetchTool20260209` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'web_fetch'` | +| `type` | 是 | `'web_fetch_20260209'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `allowed_domains` | 否 | `Array \| null` | +| `blocked_domains` | 否 | `Array \| null` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `citations` | 否 | `CitationsConfigParam \| null` | +| `defer_loading` | 否 | `boolean` | +| `max_content_tokens` | 否 | `number \| null` | +| `max_uses` | 否 | `number \| null` | +| `strict` | 否 | `boolean` | + +### `WebFetchTool20260309` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'web_fetch'` | +| `type` | 是 | `'web_fetch_20260309'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `allowed_domains` | 否 | `Array \| null` | +| `blocked_domains` | 否 | `Array \| null` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `citations` | 否 | `CitationsConfigParam \| null` | +| `defer_loading` | 否 | `boolean` | +| `max_content_tokens` | 否 | `number \| null` | +| `max_uses` | 否 | `number \| null` | +| `strict` | 否 | `boolean` | +| `use_cache` | 否 | `boolean` | + +### `WebFetchToolResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `caller` | 是 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | +| `content` | 是 | `WebFetchToolResultErrorBlock \| WebFetchBlock` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'web_fetch_tool_result'` | + +### `WebFetchToolResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `WebFetchToolResultErrorBlockParam \| WebFetchBlockParam` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'web_fetch_tool_result'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `caller` | 否 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | + +### `WebFetchToolResultErrorBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `WebFetchToolResultErrorCode` | +| `type` | 是 | `'web_fetch_tool_result_error'` | + +### `WebFetchToolResultErrorBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `WebFetchToolResultErrorCode` | +| `type` | 是 | `'web_fetch_tool_result_error'` | + +### `WebFetchToolResultErrorCode` + +类型别名:`\| 'invalid_tool_input' \| 'url_too_long' \| 'url_not_allowed' \| 'url_not_in_prior_context' \| 'url_not_accessible' \| 'unsupported_content_type' \| 'too_many_requests' \| 'max_uses_exceeded' \| 'unavailable'` + +### `WebSearchResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `encrypted_content` | 是 | `string` | +| `page_age` | 是 | `string \| null` | +| `title` | 是 | `string` | +| `type` | 是 | `'web_search_result'` | +| `url` | 是 | `string` | + +### `WebSearchResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `encrypted_content` | 是 | `string` | +| `title` | 是 | `string` | +| `type` | 是 | `'web_search_result'` | +| `url` | 是 | `string` | +| `page_age` | 否 | `string \| null` | + +### `WebSearchTool20250305` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'web_search'` | +| `type` | 是 | `'web_search_20250305'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `allowed_domains` | 否 | `Array \| null` | +| `blocked_domains` | 否 | `Array \| null` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `max_uses` | 否 | `number \| null` | +| `strict` | 否 | `boolean` | +| `user_location` | 否 | `UserLocation \| null` | + +### `WebSearchTool20260209` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `name` | 是 | `'web_search'` | +| `type` | 是 | `'web_search_20260209'` | +| `allowed_callers` | 否 | `Array<'direct' \| 'code_execution_20250825' \| 'code_execution_20260120'>` | +| `allowed_domains` | 否 | `Array \| null` | +| `blocked_domains` | 否 | `Array \| null` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `defer_loading` | 否 | `boolean` | +| `max_uses` | 否 | `number \| null` | +| `strict` | 否 | `boolean` | +| `user_location` | 否 | `UserLocation \| null` | + +### `WebSearchToolRequestError` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `WebSearchToolResultErrorCode` | +| `type` | 是 | `'web_search_tool_result_error'` | + +### `WebSearchToolResultBlock` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `caller` | 是 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | +| `content` | 是 | `WebSearchToolResultBlockContent` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'web_search_tool_result'` | + +### `WebSearchToolResultBlockContent` + +类型别名:`WebSearchToolResultError \| Array` + +### `WebSearchToolResultBlockParam` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `content` | 是 | `WebSearchToolResultBlockParamContent` | +| `tool_use_id` | 是 | `string` | +| `type` | 是 | `'web_search_tool_result'` | +| `cache_control` | 否 | `CacheControlEphemeral \| null` | +| `caller` | 否 | `DirectCaller \| ServerToolCaller \| ServerToolCaller20260120` | + +### `WebSearchToolResultBlockParamContent` + +类型别名:`\| Array \| WebSearchToolRequestError` + +### `WebSearchToolResultError` + +| 字段 | 必填 | 类型 | +| --- | --- | --- | +| `error_code` | 是 | `WebSearchToolResultErrorCode` | +| `type` | 是 | `'web_search_tool_result_error'` | + +### `WebSearchToolResultErrorCode` + +类型别名:`\| 'invalid_tool_input' \| 'unavailable' \| 'max_uses_exceeded' \| 'too_many_requests' \| 'query_too_long' \| 'request_too_large'` + +## Gemini Endpoints + +| Method | Path | Request schema | Response schema | Aether format | +| --- | --- | --- | --- | --- | +| POST | `v1beta/{+model}:generateContent` | `GenerateContentRequest` | `GenerateContentResponse` | `gemini:generate_content` | +| POST | `v1beta/{+model}:streamGenerateContent` | `GenerateContentRequest` | `GenerateContentResponse (SSE)` | `gemini:generate_content` | +| POST | `v1beta/{+model}:embedContent` | `EmbedContentRequest` | `EmbedContentResponse` | `gemini:embedding` | +| POST | `v1beta/{+model}:batchEmbedContents` | `BatchEmbedContentsRequest` | `BatchEmbedContentsResponse` | `gemini:embedding` | +| POST | `v1beta/{+model}:countTokens` | `CountTokensRequest` | `CountTokensResponse` | `gemini:generate_content` | +| POST | `v1beta/files` | `CreateFileRequest` | `CreateFileResponse` | `gemini files` | +| GET | `v1beta/files` | - | `ListFilesResponse` | `gemini files` | +| GET | `v1beta/{+name}` | - | `File` | `gemini files` | +| DELETE | `v1beta/{+name}` | - | `Empty` | `gemini files` | +| POST | `v1beta/{+model}:predictLongRunning` | `PredictLongRunningRequest` | `Operation` | `gemini video` | + +## Gemini Schema 字段表 + +以下 schema 从 Gemini native 接口根 schema 递归引用得到,共 97 个。 + +### `AttributionSourceId` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Identifier for the source contributing to this attribution. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `groundingPassage` | 否 | `GroundingPassageId` | - | Identifier for an inline passage. | +| `semanticRetrieverChunk` | 否 | `SemanticRetrieverChunk` | - | Identifier for a Chunk fetched via Semantic Retriever. | + +### `AudioResponseFormat` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Configuration for audio output format. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `bitRate` | 否 | `integer(int32)` | - | Optional. Bit rate in bits per second (bps). Only applicable for compressed formats (MP3, Opus). | +| `delivery` | 否 | `string` | `DELIVERY_UNSPECIFIED`, `INLINE`, `URI` | Optional. The delivery mode for the audio output. | +| `mimeType` | 否 | `string` | `MIME_TYPE_UNSPECIFIED`, `AUDIO_MP3`, `AUDIO_OGG_OPUS`, `AUDIO_L16`, `AUDIO_WAV`, `AUDIO_ALAW`, `AUDIO_MULAW` | Optional. The MIME type of the audio output. | +| `sampleRate` | 否 | `integer(int32)` | - | Optional. Sample rate in Hz. | + +### `BatchEmbedContentsRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Batch request to get embeddings from the model for a list of prompts. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `requests` | 否 | `array` | - | Required. Embed requests for the batch. The model in each of these requests must match the model specified BatchEmbedContentsRequest.model. | + +### `BatchEmbedContentsResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The response to a BatchEmbedContentsRequest. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `embeddings` | 否 | `array` | - | Output only. The embeddings for each request, in the same order as provided in the batch request. | +| `usageMetadata` | 否 | `EmbeddingUsageMetadata` | - | Output only. The usage metadata for the request. | + +### `Blob` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Raw media bytes. Text should not be sent as raw bytes, use the 'text' field. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `data` | 否 | `string(byte)` | - | Raw bytes for media formats. | +| `mimeType` | 否 | `string` | - | The IANA standard MIME type of the source data. Examples of supported types: - Images: image/png, image/jpeg, image/jpg, image/webp, image/heic, image/heif, image/gif, image/avif … | + +### `Candidate` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A response candidate generated from the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `avgLogprobs` | 否 | `number(double)` | - | Output only. Average log probability score of the candidate. | +| `citationMetadata` | 否 | `CitationMetadata` | - | Output only. Citation information for model-generated candidate. This field may be populated with recitation information for any text included in the content. These are passages t… | +| `content` | 否 | `Content` | - | Output only. Generated content returned from the model. | +| `finishMessage` | 否 | `string` | - | Optional. Output only. Details the reason why the model stopped generating tokens. This is populated only when finish_reason is set. | +| `finishReason` | 否 | `string` | `FINISH_REASON_UNSPECIFIED`, `STOP`, `MAX_TOKENS`, `SAFETY`, `RECITATION`, `LANGUAGE`, `OTHER`, `BLOCKLIST`, `PROHIBITED_CONTENT`, `SPII`, `MALFORMED_FUNCTION_CALL`, `IMAGE_SAFETY`, `IMAGE_PROHIBITED_CONTENT`, `IMAGE_OTHER`, `NO_IMAGE`, `IMAGE_RECITATION`, `UNEXPECTED_TOOL_CALL`, `TOO_MANY_TOOL_CALLS`, `MISSING_THOUGHT_SIGNATURE`, `MALFORMED_RESPONSE`, `ESCALATION` | Optional. Output only. The reason why the model stopped generating tokens. If empty, the model has not stopped generating tokens. | +| `groundingAttributions` | 否 | `array` | - | Output only. Attribution information for sources that contributed to a grounded answer. This field is populated for GenerateAnswer calls. | +| `groundingMetadata` | 否 | `GroundingMetadata` | - | Output only. Grounding metadata for the candidate. This field is populated for GenerateContent calls. | +| `index` | 否 | `integer(int32)` | - | Output only. Index of the candidate in the list of response candidates. | +| `logprobsResult` | 否 | `LogprobsResult` | - | Output only. Log-likelihood scores for the response tokens and top tokens | +| `safetyRatings` | 否 | `array` | - | List of ratings for the safety of a response candidate. There is at most one rating per category. | +| `tokenCount` | 否 | `integer(int32)` | - | Output only. Token count for this candidate. | +| `urlContextMetadata` | 否 | `UrlContextMetadata` | - | Output only. Metadata related to url context retrieval tool. | + +### `CitationMetadata` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A collection of source attributions for a piece of content. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `citationSources` | 否 | `array` | - | Citations to sources for a specific response. | + +### `CitationSource` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A citation to a source for a portion of a specific response. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `endIndex` | 否 | `integer(int32)` | - | Optional. End of the attributed segment, exclusive. | +| `license` | 否 | `string` | - | Optional. License for the GitHub project that is attributed as a source for segment. License info is required for code citations. | +| `startIndex` | 否 | `integer(int32)` | - | Optional. Start of segment of the response that is attributed to this source. Index indicates the start of the segment, measured in bytes. | +| `uri` | 否 | `string` | - | Optional. URI that is attributed as a source for a portion of the text. | + +### `CodeExecution` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Tool that executes code generated by the model, and automatically returns the result to the model. See also ExecutableCode and CodeExecutionResult which are only generated when us… | + +### `CodeExecutionResult` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Result of executing the ExecutableCode. Generated only when the CodeExecution tool is used. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `id` | 否 | `string` | - | Optional. The identifier of the ExecutableCode part this result is for. Only populated if the corresponding ExecutableCode has an id. | +| `outcome` | 否 | `string` | `OUTCOME_UNSPECIFIED`, `OUTCOME_OK`, `OUTCOME_FAILED`, `OUTCOME_DEADLINE_EXCEEDED` | Required. Outcome of the code execution. | +| `output` | 否 | `string` | - | Optional. Contains stdout when code execution is successful, stderr or other description otherwise. | + +### `ComputerUse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Computer Use tool type. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `environment` | 否 | `string` | `ENVIRONMENT_UNSPECIFIED`, `ENVIRONMENT_BROWSER` | Required. The environment being operated. | +| `excludedPredefinedFunctions` | 否 | `array` | - | Optional. By default, predefined functions are included in the final model call. Some of them can be explicitly excluded from being automatically included. This can serve two purp… | + +### `Content` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The base structured datatype containing multi-part content of a message. A Content includes a role field designating the producer of the Content and a parts field containing multi… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `parts` | 否 | `array` | - | Ordered Parts that constitute a single message. Parts may have different MIME types. | +| `role` | 否 | `string` | - | Optional. The producer of the content. Must be either 'user' or 'model'. Useful to set for multi-turn conversations, otherwise can be left blank or unset. | + +### `ContentEmbedding` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A list of floats representing an embedding. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `shape` | 否 | `array` | - | This field stores the soft tokens tensor frame shape (e.g. [1, 1, 256, 2048]). | +| `values` | 否 | `array` | - | The embedding values. This is for 3P users only and will not be populated for 1P calls. | + +### `CountTokensRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Counts the number of tokens in the prompt sent to a model. Models may tokenize text differently, so each model may return a different token_count. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `contents` | 否 | `array` | - | Optional. The input given to the model as a prompt. This field is ignored when generate_content_request is set. | +| `generateContentRequest` | 否 | `GenerateContentRequest` | - | Optional. The overall input given to the Model. This includes the prompt as well as other model steering information like [system instructions](https://ai.google.dev/gemini-api/do… | + +### `CountTokensResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A response from CountTokens. It returns the model's token_count for the prompt. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `cacheTokensDetails` | 否 | `array` | - | Output only. List of modalities that were processed in the cached content. | +| `cachedContentTokenCount` | 否 | `integer(int32)` | - | Number of tokens in the cached part of the prompt (the cached content). | +| `promptTokensDetails` | 否 | `array` | - | Output only. List of modalities that were processed in the request input. | +| `totalTokens` | 否 | `integer(int32)` | - | The number of tokens that the Model tokenizes the prompt into. Always non-negative. | + +### `CreateFileRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Request for CreateFile. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `file` | 否 | `File` | - | Optional. Metadata for the file to create. | + +### `CreateFileResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Response for CreateFile. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `file` | 否 | `File` | - | Metadata for the created file. | + +### `DynamicRetrievalConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Describes the options to customize dynamic retrieval. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `dynamicThreshold` | 否 | `number(float)` | - | The threshold to be used in dynamic retrieval. If not set, a system default value is used. | +| `mode` | 否 | `string` | `MODE_UNSPECIFIED`, `MODE_DYNAMIC` | The mode of the predictor to be used in dynamic retrieval. | + +### `EmbedContentConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Configurations for the EmbedContent request. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `audioTrackExtraction` | 否 | `boolean` | - | Optional. Whether to extract audio from video content. | +| `autoTruncate` | 否 | `boolean` | - | Optional. Whether to silently truncate the input content if it's longer than the maximum sequence length. | +| `documentOcr` | 否 | `boolean` | - | Optional. Whether to enable OCR for document content. | +| `outputDimensionality` | 否 | `integer(int32)` | - | Optional. Reduced dimension for the output embedding. If set, excessive values in the output embedding are truncated from the end. | +| `taskType` | 否 | `string` | `TASK_TYPE_UNSPECIFIED`, `RETRIEVAL_QUERY`, `RETRIEVAL_DOCUMENT`, `SEMANTIC_SIMILARITY`, `CLASSIFICATION`, `CLUSTERING`, `QUESTION_ANSWERING`, `FACT_VERIFICATION`, `CODE_RETRIEVAL_QUERY` | Optional. The task type of the embedding. | +| `title` | 否 | `string` | - | Optional. The title for the text. | + +### `EmbedContentRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Request containing the Content for the model to embed. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 否 | `Content` | - | Required. The content to embed. Only the parts.text fields will be counted. | +| `embedContentConfig` | 否 | `EmbedContentConfig` | - | Optional. Configuration for the EmbedContent request. | +| `model` | 否 | `string` | - | Required. The model's resource name. This serves as an ID for the Model to use. This name should match a model name returned by the ListModels method. Format: models/{model} | +| `outputDimensionality` | 否 | `integer(int32)` | - | Optional. Deprecated: Please use EmbedContentConfig.output_dimensionality instead. Optional reduced dimension for the output embedding. If set, excessive values in the output embe… | +| `taskType` | 否 | `string` | `TASK_TYPE_UNSPECIFIED`, `RETRIEVAL_QUERY`, `RETRIEVAL_DOCUMENT`, `SEMANTIC_SIMILARITY`, `CLASSIFICATION`, `CLUSTERING`, `QUESTION_ANSWERING`, `FACT_VERIFICATION`, `CODE_RETRIEVAL_QUERY` | Optional. Deprecated: Please use EmbedContentConfig.task_type instead. Optional task type for which the embeddings will be used. Not supported on earlier models (models/embedding-… | +| `title` | 否 | `string` | - | Optional. Deprecated: Please use EmbedContentConfig.title instead. An optional title for the text. Only applicable when TaskType is RETRIEVAL_DOCUMENT. Note: Specifying a title fo… | + +### `EmbedContentResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The response to an EmbedContentRequest. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `embedding` | 否 | `ContentEmbedding` | - | Output only. The embedding generated from the input content. | +| `usageMetadata` | 否 | `EmbeddingUsageMetadata` | - | Output only. The usage metadata for the request. | + +### `EmbeddingUsageMetadata` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Metadata on the usage of the embedding request. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `promptTokenCount` | 否 | `integer(int32)` | - | Output only. Number of tokens in the prompt. | +| `promptTokenDetails` | 否 | `array` | - | Output only. List of modalities that were processed in the request input. | + +### `ExecutableCode` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Code generated by the model that is meant to be executed, and the result returned to the model. Only generated when using the CodeExecution tool, in which the code will be automat… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `code` | 否 | `string` | - | Required. The code to be executed. | +| `id` | 否 | `string` | - | Optional. Unique identifier of the ExecutableCode part. The server returns the CodeExecutionResult with the matching id. | +| `language` | 否 | `string` | `LANGUAGE_UNSPECIFIED`, `PYTHON` | Required. Programming language of the code. | + +### `File` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A file uploaded to the API. Next ID: 15 | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `createTime` | 否 | `string(google-datetime)` | - | Output only. The timestamp of when the File was created. | +| `displayName` | 否 | `string` | - | Optional. The human-readable display name for the File. The display name must be no more than 512 characters in length, including spaces. Example: "Welcome Image" | +| `downloadUri` | 否 | `string` | - | Output only. The download uri of the File. | +| `error` | 否 | `Status` | - | Output only. Error status if File processing failed. | +| `expirationTime` | 否 | `string(google-datetime)` | - | Output only. The timestamp of when the File will be deleted. Only set if the File is scheduled to expire. | +| `mimeType` | 否 | `string` | - | Output only. MIME type of the file. | +| `name` | 否 | `string` | - | Immutable. Identifier. The File resource name. The ID (name excluding the "files/" prefix) can contain up to 40 characters that are lowercase alphanumeric or dashes (-). The ID ca… | +| `sha256Hash` | 否 | `string(byte)` | - | Output only. SHA-256 hash of the uploaded bytes. | +| `sizeBytes` | 否 | `string(int64)` | - | Output only. Size of the file in bytes. | +| `source` | 否 | `string` | `SOURCE_UNSPECIFIED`, `UPLOADED`, `GENERATED`, `REGISTERED` | Source of the File. | +| `state` | 否 | `string` | `STATE_UNSPECIFIED`, `PROCESSING`, `ACTIVE`, `FAILED` | Output only. Processing state of the File. | +| `updateTime` | 否 | `string(google-datetime)` | - | Output only. The timestamp of when the File was last updated. | +| `uri` | 否 | `string` | - | Output only. The uri of the File. | +| `videoMetadata` | 否 | `VideoFileMetadata` | - | Output only. Metadata for a video. | + +### `FileData` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | URI based data. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `fileUri` | 否 | `string` | - | Required. URI. | +| `mimeType` | 否 | `string` | - | Optional. The IANA standard MIME type of the source data. | + +### `FileSearch` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The FileSearch tool that retrieves knowledge from Semantic Retrieval corpora. Files are imported to Semantic Retrieval corpora using the ImportFile API. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `fileSearchStoreNames` | 否 | `array` | - | Required. The names of the file_search_stores to retrieve from. Example: fileSearchStores/my-file-search-store-123 | +| `metadataFilter` | 否 | `string` | - | Optional. Metadata filter to apply to the semantic retrieval documents and chunks. | +| `topK` | 否 | `integer(int32)` | - | Optional. The number of semantic retrieval chunks to retrieve. | + +### `FunctionCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A predicted FunctionCall returned from the model that contains a string representing the FunctionDeclaration.name with the arguments and their values. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `args` | 否 | `object/map` | - | Optional. The function parameters and values in JSON object format. | +| `id` | 否 | `string` | - | Optional. Unique identifier of the function call. If populated, the client to execute the function_call and return the response with the matching id. | +| `name` | 否 | `string` | - | Required. The name of the function to call. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 128. | + +### `FunctionCallingConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Configuration for specifying function calling behavior. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `allowedFunctionNames` | 否 | `array` | - | Optional. A set of function names that, when provided, limits the functions the model will call. This should only be set when the Mode is ANY or VALIDATED. Function names should m… | +| `mode` | 否 | `string` | `MODE_UNSPECIFIED`, `AUTO`, `ANY`, `NONE`, `VALIDATED` | Optional. Specifies the mode in which function calling should execute. If unspecified, the default value will be set to AUTO. | + +### `FunctionDeclaration` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Structured representation of a function declaration as defined by the [OpenAPI 3.03 specification](https://spec.openapis.org/oas/v3.0.3). Included in this declaration are the func… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `behavior` | 否 | `string` | `UNSPECIFIED`, `BLOCKING`, `NON_BLOCKING` | Optional. Specifies the function Behavior. Currently only supported by the BidiGenerateContent method. | +| `description` | 否 | `string` | - | Required. A brief description of the function. | +| `name` | 否 | `string` | - | Required. The name of the function. Must be a-z, A-Z, 0-9, or contain underscores, colons, dots, and dashes, with a maximum length of 128. | +| `parameters` | 否 | `Schema` | - | Optional. Describes the parameters to this function. Reflects the Open API 3.03 Parameter Object string Key: the name of the parameter. Parameter names are case sensitive. Schema … | +| `parametersJsonSchema` | 否 | `any` | - | Optional. Describes the parameters to the function in JSON Schema format. The schema must describe an object where the properties are the parameters to the function. For example: … | +| `response` | 否 | `Schema` | - | Optional. Describes the output from this function in JSON Schema format. Reflects the Open API 3.03 Response Object. The Schema defines the type used for the response value of the… | +| `responseJsonSchema` | 否 | `any` | - | Optional. Describes the output from this function in JSON Schema format. The value specified by the schema is the response value of the function. This field is mutually exclusive … | + +### `FunctionResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The result output from a FunctionCall that contains a string representing the FunctionDeclaration.name and a structured JSON object containing any output from the function is used… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `id` | 否 | `string` | - | Optional. The identifier of the function call this response is for. Populated by the client to match the corresponding function call id. | +| `name` | 否 | `string` | - | Required. The name of the function to call. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 128. | +| `parts` | 否 | `array` | - | Optional. Ordered Parts that constitute a function response. Parts may have different IANA MIME types. | +| `response` | 否 | `object/map` | - | Required. The function response in JSON object format. Callers can use any keys of their choice that fit the function's syntax to return the function output, e.g. "output", "resul… | +| `scheduling` | 否 | `string` | `SCHEDULING_UNSPECIFIED`, `SILENT`, `WHEN_IDLE`, `INTERRUPT` | Optional. Specifies how the response should be scheduled in the conversation. Only applicable to NON_BLOCKING function calls, is ignored otherwise. Defaults to WHEN_IDLE. | +| `willContinue` | 否 | `boolean` | - | Optional. Signals that function call continues, and more responses will be returned, turning the function call into a generator. Is only applicable to NON_BLOCKING function calls,… | + +### `FunctionResponseBlob` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Raw media bytes for function response. Text should not be sent as raw bytes, use the 'FunctionResponse.response' field. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `data` | 否 | `string(byte)` | - | Raw bytes for media formats. | +| `mimeType` | 否 | `string` | - | The IANA standard MIME type of the source data. Examples: - image/png - image/jpeg If an unsupported MIME type is provided, an error will be returned. For a complete list of suppo… | + +### `FunctionResponsePart` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A datatype containing media that is part of a FunctionResponse message. A FunctionResponsePart consists of data which has an associated datatype. A FunctionResponsePart can only c… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `inlineData` | 否 | `FunctionResponseBlob` | - | Inline media bytes. | + +### `GenerateContentRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Request to generate a completion from the model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `cachedContent` | 否 | `string` | - | Optional. The name of the content [cached](https://ai.google.dev/gemini-api/docs/caching) to use as context to serve the prediction. Format: cachedContents/{cachedContent} | +| `contents` | 否 | `array` | - | Required. The content of the current conversation with the model. For single-turn queries, this is a single instance. For multi-turn queries like [chat](https://ai.google.dev/gemi… | +| `generationConfig` | 否 | `GenerationConfig` | - | Optional. Configuration options for model generation and outputs. | +| `model` | 否 | `string` | - | Required. The name of the Model to use for generating the completion. Format: models/{model}. | +| `safetySettings` | 否 | `array` | - | Optional. A list of unique SafetySetting instances for blocking unsafe content. This will be enforced on the GenerateContentRequest.contents and GenerateContentResponse.candidates… | +| `serviceTier` | 否 | `string` | `unspecified`, `standard`, `flex`, `priority` | Optional. The service tier of the request. | +| `store` | 否 | `boolean` | - | Optional. Configures the logging behavior for a given request. If set, it takes precedence over the project-level logging config. | +| `systemInstruction` | 否 | `Content` | - | Optional. Developer set [system instruction(s)](https://ai.google.dev/gemini-api/docs/system-instructions). Currently, text only. | +| `toolConfig` | 否 | `ToolConfig` | - | Optional. Tool configuration for any Tool specified in the request. Refer to the [Function calling guide](https://ai.google.dev/gemini-api/docs/function-calling#function_calling_m… | +| `tools` | 否 | `array` | - | Optional. A list of Tools the Model may use to generate the next response. A Tool is a piece of code that enables the system to interact with external systems to perform an action… | + +### `GenerateContentResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Response from the model supporting multiple candidate responses. Safety ratings and content filtering are reported for both prompt in GenerateContentResponse.prompt_feedback and f… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `candidates` | 否 | `array` | - | Candidate responses from the model. | +| `modelStatus` | 否 | `ModelStatus` | - | Output only. The current model status of this model. | +| `modelVersion` | 否 | `string` | - | Output only. The model version used to generate the response. | +| `promptFeedback` | 否 | `PromptFeedback` | - | Returns the prompt's feedback related to the content filters. | +| `responseId` | 否 | `string` | - | Output only. response_id is used to identify each response. | +| `usageMetadata` | 否 | `UsageMetadata` | - | Output only. Metadata on the generation requests' token usage. | + +### `GenerationConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Configuration options for model generation and outputs. Not all parameters are configurable for every model. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `_responseJsonSchema` | 否 | `any` | - | Optional. Output schema of the generated response. This is an alternative to response_schema that accepts [JSON Schema](https://json-schema.org/). If set, response_schema must be … | +| `candidateCount` | 否 | `integer(int32)` | - | Optional. Number of generated responses to return. If unset, this will default to 1. Please note that this doesn't work for previous generation models (Gemini 1.0 family) | +| `enableEnhancedCivicAnswers` | 否 | `boolean` | - | Optional. Enables enhanced civic answers. It may not be available for all models. | +| `frequencyPenalty` | 否 | `number(float)` | - | Optional. Frequency penalty applied to the next token's logprobs, multiplied by the number of times each token has been seen in the respponse so far. A positive penalty will disco… | +| `imageConfig` | 否 | `ImageConfig` | - | Optional. Config for image generation. An error will be returned if this field is set for models that don't support these config options. | +| `logprobs` | 否 | `integer(int32)` | - | Optional. Only valid if response_logprobs=True. This sets the number of top logprobs, including the chosen candidate, to return at each decoding step in the Candidate.logprobs_res… | +| `maxOutputTokens` | 否 | `integer(int32)` | - | Optional. The maximum number of tokens to include in a response candidate. Note: The default value varies by model, see the Model.output_token_limit attribute of the Model returne… | +| `mediaResolution` | 否 | `string` | `MEDIA_RESOLUTION_UNSPECIFIED`, `MEDIA_RESOLUTION_LOW`, `MEDIA_RESOLUTION_MEDIUM`, `MEDIA_RESOLUTION_HIGH` | Optional. If specified, the media resolution specified will be used. | +| `presencePenalty` | 否 | `number(float)` | - | Optional. Presence penalty applied to the next token's logprobs if the token has already been seen in the response. This penalty is binary on/off and not dependant on the number o… | +| `responseFormat` | 否 | `ResponseFormatConfig` | - | Optional. Configuration for the response output format. Allows specifying output configuration per modality (text, audio, image) in a flat structure. | +| `responseJsonSchema` | 否 | `any` | - | Optional. An internal detail. Use responseJsonSchema rather than this field. | +| `responseLogprobs` | 否 | `boolean` | - | Optional. If true, export the logprobs results in response. | +| `responseMimeType` | 否 | `string` | - | Optional. MIME type of the generated candidate text. Supported MIME types are: text/plain: (default) Text output. application/json: JSON response in the response candidates. text/… | +| `responseModalities` | 否 | `array` | - | Optional. The requested modalities of the response. Represents the set of modalities that the model can return, and should be expected in the response. This is an exact match to t… | +| `responseSchema` | 否 | `Schema` | - | Optional. Output schema of the generated candidate text. Schemas must be a subset of the [OpenAPI schema](https://spec.openapis.org/oas/v3.0.3#schema) and can be objects, primitiv… | +| `seed` | 否 | `integer(int32)` | - | Optional. Seed used in decoding. If not set, the request uses a randomly generated seed. | +| `speechConfig` | 否 | `SpeechConfig` | - | Optional. The speech generation config. | +| `stopSequences` | 否 | `array` | - | Optional. The set of character sequences (up to 5) that will stop output generation. If specified, the API will stop at the first appearance of a stop_sequence. The stop sequence … | +| `temperature` | 否 | `number(float)` | - | Optional. Controls the randomness of the output. Note: The default value varies by model, see the Model.temperature attribute of the Model returned from the getModel function. Val… | +| `thinkingConfig` | 否 | `ThinkingConfig` | - | Optional. Config for thinking features. An error will be returned if this field is set for models that don't support thinking. | +| `topK` | 否 | `integer(int32)` | - | Optional. The maximum number of tokens to consider when sampling. Gemini models use Top-p (nucleus) sampling or a combination of Top-k and nucleus sampling. Top-k sampling conside… | +| `topP` | 否 | `number(float)` | - | Optional. The maximum cumulative probability of tokens to consider when sampling. The model uses combined Top-k and Top-p (nucleus) sampling. Tokens are sorted based on their assi… | + +### `GoogleAiGenerativelanguageV1betaGroundingSupport` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Grounding support. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `confidenceScores` | 否 | `array` | - | Optional. Confidence score of the support references. Ranges from 0 to 1. 1 is the most confident. This list must have the same size as the grounding_chunk_indices. | +| `groundingChunkIndices` | 否 | `array` | - | Optional. A list of indices (into 'grounding_chunk' in response.candidate.grounding_metadata) specifying the citations associated with the claim. For instance [1,3,4] means that g… | +| `renderedParts` | 否 | `array` | - | Output only. Indices into the parts field of the candidate's content. These indices specify which rendered parts are associated with this support source. | +| `segment` | 否 | `GoogleAiGenerativelanguageV1betaSegment` | - | Segment of the content this support belongs to. | + +### `GoogleAiGenerativelanguageV1betaSegment` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Segment of the content. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `endIndex` | 否 | `integer(int32)` | - | End index in the given Part, measured in bytes. Offset from the start of the Part, exclusive, starting at zero. | +| `partIndex` | 否 | `integer(int32)` | - | The index of a Part object within its parent Content object. | +| `startIndex` | 否 | `integer(int32)` | - | Start index in the given Part, measured in bytes. Offset from the start of the Part, inclusive, starting at zero. | +| `text` | 否 | `string` | - | The text corresponding to the segment from the response. | + +### `GoogleMaps` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The GoogleMaps Tool that provides geospatial context for the user's query. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `enableWidget` | 否 | `boolean` | - | Optional. Whether to return a widget context token in the GroundingMetadata of the response. Developers can use the widget context token to render a Google Maps widget with geospa… | + +### `GoogleSearch` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | GoogleSearch tool type. Tool to support Google Search in Model. Powered by Google. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `searchTypes` | 否 | `SearchTypes` | - | Optional. The set of search types to enable. If not set, web search is enabled by default. | +| `timeRangeFilter` | 否 | `Interval` | - | Optional. Filter search results to a specific time range. If customers set a start time, they must set an end time (and vice versa). | + +### `GoogleSearchRetrieval` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Tool to retrieve public web data for grounding, powered by Google. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `dynamicRetrievalConfig` | 否 | `DynamicRetrievalConfig` | - | Specifies the dynamic retrieval configuration for the given source. | + +### `GroundingAttribution` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Attribution for a source that contributed to an answer. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `content` | 否 | `Content` | - | Grounding source content that makes up this attribution. | +| `sourceId` | 否 | `AttributionSourceId` | - | Output only. Identifier for the source contributing to this attribution. | + +### `GroundingChunk` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A GroundingChunk represents a segment of supporting evidence that grounds the model's response. It can be a chunk from the web, a retrieved context from a file, or information fro… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `image` | 否 | `Image` | - | Optional. Grounding chunk from image search. | +| `maps` | 否 | `Maps` | - | Optional. Grounding chunk from Google Maps. | +| `retrievedContext` | 否 | `RetrievedContext` | - | Optional. Grounding chunk from context retrieved by the file search tool. | +| `web` | 否 | `Web` | - | Grounding chunk from the web. | + +### `GroundingChunkCustomMetadata` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | User provided metadata about the GroundingFact. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `key` | 否 | `string` | - | The key of the metadata. | +| `numericValue` | 否 | `number(float)` | - | Optional. The numeric value of the metadata. The expected range for this value depends on the specific key used. | +| `stringListValue` | 否 | `GroundingChunkStringList` | - | Optional. A list of string values for the metadata. | +| `stringValue` | 否 | `string` | - | Optional. The string value of the metadata. | + +### `GroundingChunkStringList` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A list of string values. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `values` | 否 | `array` | - | The string values of the list. | + +### `GroundingMetadata` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Metadata returned to client when grounding is enabled. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `googleMapsWidgetContextToken` | 否 | `string` | - | Optional. Resource name of the Google Maps widget context token that can be used with the PlacesContextElement widget in order to render contextual data. Only populated in the cas… | +| `groundingChunks` | 否 | `array` | - | List of supporting references retrieved from specified grounding source. When streaming, this only contains the grounding chunks that have not been included in the grounding metad… | +| `groundingSupports` | 否 | `array` | - | List of grounding support. | +| `imageSearchQueries` | 否 | `array` | - | Image search queries used for grounding. | +| `retrievalMetadata` | 否 | `RetrievalMetadata` | - | Metadata related to retrieval in the grounding flow. | +| `searchEntryPoint` | 否 | `SearchEntryPoint` | - | Optional. Google search entry for the following-up web searches. | +| `webSearchQueries` | 否 | `array` | - | Web search queries for the following-up web search. | + +### `GroundingPassageId` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Identifier for a part within a GroundingPassage. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `partIndex` | 否 | `integer(int32)` | - | Output only. Index of the part within the GenerateAnswerRequest's GroundingPassage.content. | +| `passageId` | 否 | `string` | - | Output only. ID of the passage matching the GenerateAnswerRequest's GroundingPassage.id. | + +### `Image` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Chunk from image search. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `domain` | 否 | `string` | - | The root domain of the web page that the image is from, e.g. "example.com". | +| `imageUri` | 否 | `string` | - | The image asset URL. | +| `sourceUri` | 否 | `string` | - | The web page URI for attribution. | +| `title` | 否 | `string` | - | The title of the web page that the image is from. | + +### `ImageConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Config for image generation features. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `aspectRatio` | 否 | `string` | - | Optional. The aspect ratio of the image to generate. Supported aspect ratios: 1:1, 1:4, 4:1, 1:8, 8:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, or 21:9. If not specified, the mod… | +| `imageSize` | 否 | `string` | - | Optional. Specifies the size of generated images. Supported values are 512, 1K, 2K, 4K. If not specified, the model will use default value 1K. | + +### `ImageResponseFormat` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Configuration for image output format. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `aspectRatio` | 否 | `string` | `ASPECT_RATIO_UNSPECIFIED`, `ASPECT_RATIO_ONE_BY_ONE`, `ASPECT_RATIO_TWO_BY_THREE`, `ASPECT_RATIO_THREE_BY_TWO`, `ASPECT_RATIO_THREE_BY_FOUR`, `ASPECT_RATIO_FOUR_BY_THREE`, `ASPECT_RATIO_FOUR_BY_FIVE`, `ASPECT_RATIO_FIVE_BY_FOUR`, `ASPECT_RATIO_NINE_BY_SIXTEEN`, `ASPECT_RATIO_SIXTEEN_BY_NINE`, `ASPECT_RATIO_TWENTY_ONE_BY_NINE`, `ASPECT_RATIO_ONE_BY_EIGHT`, `ASPECT_RATIO_EIGHT_BY_ONE`, `ASPECT_RATIO_ONE_BY_FOUR`, `ASPECT_RATIO_FOUR_BY_ONE` | Optional. The aspect ratio for the image output. | +| `delivery` | 否 | `string` | `DELIVERY_UNSPECIFIED`, `INLINE`, `URI` | Optional. The delivery mode for the image output. | +| `imageSize` | 否 | `string` | `IMAGE_SIZE_UNSPECIFIED`, `IMAGE_SIZE_FIVE_TWELVE`, `IMAGE_SIZE_ONE_K`, `IMAGE_SIZE_TWO_K`, `IMAGE_SIZE_FOUR_K` | Optional. The size of the image output. | +| `mimeType` | 否 | `string` | `MIME_TYPE_UNSPECIFIED`, `IMAGE_JPEG` | Optional. The MIME type of the image output. | + +### `ImageSearch` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Image search for grounding and related configurations. | + +### `Interval` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Represents a time interval, encoded as a Timestamp start (inclusive) and a Timestamp end (exclusive). The start must be less than or equal to the end. When the start equals the en… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `endTime` | 否 | `string(google-datetime)` | - | Optional. Exclusive end of the interval. If specified, a Timestamp matching this interval will have to be before the end. | +| `startTime` | 否 | `string(google-datetime)` | - | Optional. Inclusive start of the interval. If specified, a Timestamp matching this interval will have to be the same or after the start. | + +### `LatLng` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | An object that represents a latitude/longitude pair. This is expressed as a pair of doubles to represent degrees latitude and degrees longitude. Unless specified otherwise, this o… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `latitude` | 否 | `number(double)` | - | The latitude in degrees. It must be in the range [-90.0, +90.0]. | +| `longitude` | 否 | `number(double)` | - | The longitude in degrees. It must be in the range [-180.0, +180.0]. | + +### `ListFilesResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Response for ListFiles. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `files` | 否 | `array` | - | The list of Files. | +| `nextPageToken` | 否 | `string` | - | A token that can be sent as a page_token into a subsequent ListFiles call. | + +### `LogprobsResult` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Logprobs Result | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `chosenCandidates` | 否 | `array` | - | Length = total number of decoding steps. The chosen candidates may or may not be in top_candidates. | +| `logProbabilitySum` | 否 | `number(float)` | - | Sum of log probabilities for all tokens. | +| `topCandidates` | 否 | `array` | - | Length = total number of decoding steps. | + +### `LogprobsResultCandidate` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Candidate for the logprobs token and score. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `logProbability` | 否 | `number(float)` | - | The candidate's log probability. | +| `token` | 否 | `string` | - | The candidate’s token string value. | +| `tokenId` | 否 | `integer(int32)` | - | The candidate’s token id value. | + +### `Maps` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A grounding chunk from Google Maps. A Maps chunk corresponds to a single place. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `placeAnswerSources` | 否 | `PlaceAnswerSources` | - | Sources that provide answers about the features of a given place in Google Maps. | +| `placeId` | 否 | `string` | - | The ID of the place, in places/{place_id} format. A user can use this ID to look up that place. | +| `text` | 否 | `string` | - | Text description of the place answer. | +| `title` | 否 | `string` | - | Title of the place. | +| `uri` | 否 | `string` | - | URI reference of the place. | + +### `McpServer` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A MCPServer is a server that can be called by the model to perform actions. It is a server that implements the MCP protocol. Next ID: 6 | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `name` | 否 | `string` | - | The name of the MCPServer. | +| `streamableHttpTransport` | 否 | `StreamableHttpTransport` | - | A transport that can stream HTTP requests and responses. | + +### `ModalityTokenCount` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Represents token counting info for a single modality. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `modality` | 否 | `string` | `MODALITY_UNSPECIFIED`, `TEXT`, `IMAGE`, `VIDEO`, `AUDIO`, `DOCUMENT` | The modality associated with this token count. | +| `tokenCount` | 否 | `integer(int32)` | - | Number of tokens. | + +### `ModelStatus` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The status of the underlying model. This is used to indicate the stage of the underlying model and the retirement time if applicable. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `message` | 否 | `string` | - | A message explaining the model status. | +| `modelStage` | 否 | `string` | `MODEL_STAGE_UNSPECIFIED`, `UNSTABLE_EXPERIMENTAL`, `EXPERIMENTAL`, `PREVIEW`, `STABLE`, `LEGACY`, `DEPRECATED`, `RETIRED` | The stage of the underlying model. | +| `retirementTime` | 否 | `string(google-datetime)` | - | The time at which the model will be retired. | + +### `MultiSpeakerVoiceConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The configuration for the multi-speaker setup. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `speakerVoiceConfigs` | 否 | `array` | - | Required. All the enabled speaker voices. | + +### `Operation` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | This resource represents a long-running operation that is the result of a network API call. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `done` | 否 | `boolean` | - | If the value is false, it means the operation is still in progress. If true, the operation is completed, and either error or response is available. | +| `error` | 否 | `Status` | - | The error result of the operation in case of failure or cancellation. | +| `metadata` | 否 | `object/map` | - | Service-specific metadata associated with the operation. It typically contains progress information and common metadata such as create time. Some services might not provide such m… | +| `name` | 否 | `string` | - | The server-assigned name, which is only unique within the same service that originally returns it. If you use the default HTTP mapping, the name should be a resource name ending w… | +| `response` | 否 | `object/map` | - | The normal, successful response of the operation. If the original method returns no data on success, such as Delete, the response is google.protobuf.Empty. If the original method … | + +### `Part` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A datatype containing media that is part of a multi-part Content message. A Part consists of data which has an associated datatype. A Part can only contain one of the accepted typ… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `codeExecutionResult` | 否 | `CodeExecutionResult` | - | Result of executing the ExecutableCode. | +| `executableCode` | 否 | `ExecutableCode` | - | Code generated by the model that is meant to be executed. | +| `fileData` | 否 | `FileData` | - | URI based data. | +| `functionCall` | 否 | `FunctionCall` | - | A predicted FunctionCall returned from the model that contains a string representing the FunctionDeclaration.name with the arguments and their values. | +| `functionResponse` | 否 | `FunctionResponse` | - | The result output of a FunctionCall that contains a string representing the FunctionDeclaration.name and a structured JSON object containing any output from the function is used a… | +| `inlineData` | 否 | `Blob` | - | Inline media bytes. | +| `mediaResolution` | 否 | `MediaResolution` | - | Optional. Media resolution for the input media. | +| `partMetadata` | 否 | `object/map` | - | Custom metadata associated with the Part. Agents using genai.Part as content representation may need to keep track of the additional information. For example it can be name of a f… | +| `text` | 否 | `string` | - | Inline text. | +| `thought` | 否 | `boolean` | - | Optional. Indicates if the part is thought from the model. | +| `thoughtSignature` | 否 | `string(byte)` | - | Optional. An opaque signature for the thought so it can be reused in subsequent requests. | +| `toolCall` | 否 | `ToolCall` | - | Server-side tool call. This field is populated when the model predicts a tool invocation that should be executed on the server. The client is expected to echo this message back to… | +| `toolResponse` | 否 | `ToolResponse` | - | The output from a server-side ToolCall execution. This field is populated by the client with the results of executing the corresponding ToolCall. | +| `videoMetadata` | 否 | `VideoMetadata` | - | Optional. Video metadata. The metadata should only be specified while the video data is presented in inline_data or file_data. | + +### `PlaceAnswerSources` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Collection of sources that provide answers about the features of a given place in Google Maps. Each PlaceAnswerSources message corresponds to a specific place in Google Maps. The … | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `reviewSnippets` | 否 | `array` | - | Snippets of reviews that are used to generate answers about the features of a given place in Google Maps. | + +### `PrebuiltVoiceConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The configuration for the prebuilt speaker to use. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `voiceName` | 否 | `string` | - | The name of the preset voice to use. | + +### `PredictLongRunningRequest` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Request message for [PredictionService.PredictLongRunning]. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `instances` | 否 | `array` | - | Required. The instances that are the input to the prediction call. | +| `parameters` | 否 | `any` | - | Optional. The parameters that govern the prediction call. | + +### `PromptFeedback` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A set of the feedback metadata the prompt specified in GenerateContentRequest.content. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `blockReason` | 否 | `string` | `BLOCK_REASON_UNSPECIFIED`, `SAFETY`, `OTHER`, `BLOCKLIST`, `PROHIBITED_CONTENT`, `IMAGE_SAFETY` | Optional. If set, the prompt was blocked and no candidates are returned. Rephrase the prompt. | +| `safetyRatings` | 否 | `array` | - | Ratings for safety of the prompt. There is at most one rating per category. | + +### `ResponseFormatConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Configuration for the response output format. This is a flat object where each optional sub-field configures a specific output modality. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `audio` | 否 | `AudioResponseFormat` | - | Optional. Audio output format configuration. | +| `image` | 否 | `ImageResponseFormat` | - | Optional. Image output format configuration. | +| `text` | 否 | `TextResponseFormat` | - | Optional. Text output format configuration. | + +### `RetrievalConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Retrieval config. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `languageCode` | 否 | `string` | - | Optional. The language code of the user. Language code for content. Use language tags defined by [BCP47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt). | +| `latLng` | 否 | `LatLng` | - | Optional. The location of the user. | + +### `RetrievalMetadata` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Metadata related to retrieval in the grounding flow. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `googleSearchDynamicRetrievalScore` | 否 | `number(float)` | - | Optional. Score indicating how likely information from google search could help answer the prompt. The score is in the range [0, 1], where 0 is the least likely and 1 is the most … | + +### `RetrievedContext` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Chunk from context retrieved by the file search tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `customMetadata` | 否 | `array` | - | Optional. User-provided metadata about the retrieved context. | +| `fileSearchStore` | 否 | `string` | - | Optional. Name of the FileSearchStore containing the document. Example: fileSearchStores/123 | +| `mediaId` | 否 | `string` | - | Optional. The media blob resource name for multimodal file search results. Format: fileSearchStores/{file_search_store_id}/media/{blob_id} | +| `pageNumber` | 否 | `integer(int32)` | - | Optional. Page number of the retrieved context, if applicable. | +| `text` | 否 | `string` | - | Optional. Text of the chunk. | +| `title` | 否 | `string` | - | Optional. Title of the document. | +| `uri` | 否 | `string` | - | Optional. URI reference of the semantic retrieval document. | + +### `ReviewSnippet` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Encapsulates a snippet of a user review that answers a question about the features of a specific place in Google Maps. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `googleMapsUri` | 否 | `string` | - | A link that corresponds to the user review on Google Maps. | +| `reviewId` | 否 | `string` | - | The ID of the review snippet. | +| `title` | 否 | `string` | - | Title of the review. | + +### `SafetyRating` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Safety rating for a piece of content. The safety rating contains the category of harm and the harm probability level in that category for a piece of content. Content is classified… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `blocked` | 否 | `boolean` | - | Was this content blocked because of this rating? | +| `category` | 否 | `string` | `HARM_CATEGORY_UNSPECIFIED`, `HARM_CATEGORY_DEROGATORY`, `HARM_CATEGORY_TOXICITY`, `HARM_CATEGORY_VIOLENCE`, `HARM_CATEGORY_SEXUAL`, `HARM_CATEGORY_MEDICAL`, `HARM_CATEGORY_DANGEROUS`, `HARM_CATEGORY_HARASSMENT`, `HARM_CATEGORY_HATE_SPEECH`, `HARM_CATEGORY_SEXUALLY_EXPLICIT`, `HARM_CATEGORY_DANGEROUS_CONTENT`, `HARM_CATEGORY_CIVIC_INTEGRITY` | Required. The category for this rating. | +| `probability` | 否 | `string` | `HARM_PROBABILITY_UNSPECIFIED`, `NEGLIGIBLE`, `LOW`, `MEDIUM`, `HIGH` | Required. The probability of harm for this content. | + +### `SafetySetting` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Safety setting, affecting the safety-blocking behavior. Passing a safety setting for a category changes the allowed probability that content is blocked. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `category` | 否 | `string` | `HARM_CATEGORY_UNSPECIFIED`, `HARM_CATEGORY_DEROGATORY`, `HARM_CATEGORY_TOXICITY`, `HARM_CATEGORY_VIOLENCE`, `HARM_CATEGORY_SEXUAL`, `HARM_CATEGORY_MEDICAL`, `HARM_CATEGORY_DANGEROUS`, `HARM_CATEGORY_HARASSMENT`, `HARM_CATEGORY_HATE_SPEECH`, `HARM_CATEGORY_SEXUALLY_EXPLICIT`, `HARM_CATEGORY_DANGEROUS_CONTENT`, `HARM_CATEGORY_CIVIC_INTEGRITY` | Required. The category for this setting. | +| `threshold` | 否 | `string` | `HARM_BLOCK_THRESHOLD_UNSPECIFIED`, `BLOCK_LOW_AND_ABOVE`, `BLOCK_MEDIUM_AND_ABOVE`, `BLOCK_ONLY_HIGH`, `BLOCK_NONE`, `OFF` | Required. Controls the probability threshold at which harm is blocked. | + +### `Schema` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The Schema object allows the definition of input and output data types. These types can be objects, but also primitives and arrays. Represents a select subset of an [OpenAPI 3.0 s… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `anyOf` | 否 | `array` | - | Optional. The value should be validated against any (one or more) of the subschemas in the list. | +| `default` | 否 | `any` | - | Optional. Default value of the field. Per JSON Schema, this field is intended for documentation generators and doesn't affect validation. Thus it's included here and ignored so th… | +| `description` | 否 | `string` | - | Optional. A brief description of the parameter. This could contain examples of use. Parameter description may be formatted as Markdown. | +| `enum` | 否 | `array` | - | Optional. Possible values of the element of Type.STRING with enum format. For example we can define an Enum Direction as : {type:STRING, format:enum, enum:["EAST", NORTH", "SOUTH"… | +| `example` | 否 | `any` | - | Optional. Example of the object. Will only populated when the object is the root. | +| `format` | 否 | `string` | - | Optional. The format of the data. Any value is allowed, but most do not trigger any special functionality. | +| `items` | 否 | `Schema` | - | Optional. Schema of the elements of Type.ARRAY. | +| `maxItems` | 否 | `string(int64)` | - | Optional. Maximum number of the elements for Type.ARRAY. | +| `maxLength` | 否 | `string(int64)` | - | Optional. Maximum length of the Type.STRING | +| `maxProperties` | 否 | `string(int64)` | - | Optional. Maximum number of the properties for Type.OBJECT. | +| `maximum` | 否 | `number(double)` | - | Optional. Maximum value of the Type.INTEGER and Type.NUMBER | +| `minItems` | 否 | `string(int64)` | - | Optional. Minimum number of the elements for Type.ARRAY. | +| `minLength` | 否 | `string(int64)` | - | Optional. SCHEMA FIELDS FOR TYPE STRING Minimum length of the Type.STRING | +| `minProperties` | 否 | `string(int64)` | - | Optional. Minimum number of the properties for Type.OBJECT. | +| `minimum` | 否 | `number(double)` | - | Optional. SCHEMA FIELDS FOR TYPE INTEGER and NUMBER Minimum value of the Type.INTEGER and Type.NUMBER | +| `nullable` | 否 | `boolean` | - | Optional. Indicates if the value may be null. | +| `pattern` | 否 | `string` | - | Optional. Pattern of the Type.STRING to restrict a string to a regular expression. | +| `properties` | 否 | `object/map` | - | Optional. Properties of Type.OBJECT. | +| `propertyOrdering` | 否 | `array` | - | Optional. The order of the properties. Not a standard field in open api spec. Used to determine the order of the properties in the response. | +| `required` | 否 | `array` | - | Optional. Required properties of Type.OBJECT. | +| `title` | 否 | `string` | - | Optional. The title of the schema. | +| `type` | 否 | `string` | `TYPE_UNSPECIFIED`, `STRING`, `NUMBER`, `INTEGER`, `BOOLEAN`, `ARRAY`, `OBJECT`, `NULL` | Required. Data type. | + +### `SearchEntryPoint` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Google search entry point. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `renderedContent` | 否 | `string` | - | Optional. Web content snippet that can be embedded in a web page or an app webview. | +| `sdkBlob` | 否 | `string(byte)` | - | Optional. Base64 encoded JSON representing array of tuple. | + +### `SearchTypes` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Different types of search that can be enabled on the GoogleSearch tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `imageSearch` | 否 | `ImageSearch` | - | Optional. Enables image search. Image bytes are returned. | +| `webSearch` | 否 | `WebSearch` | - | Optional. Enables web search. Only text results are returned. | + +### `SemanticRetrieverChunk` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Identifier for a Chunk retrieved via Semantic Retriever specified in the GenerateAnswerRequest using SemanticRetrieverConfig. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `chunk` | 否 | `string` | - | Output only. Name of the Chunk containing the attributed text. Example: corpora/123/documents/abc/chunks/xyz | +| `source` | 否 | `string` | - | Output only. Name of the source matching the request's SemanticRetrieverConfig.source. Example: corpora/123 or corpora/123/documents/abc | + +### `SpeakerVoiceConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The configuration for a single speaker in a multi speaker setup. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `speaker` | 否 | `string` | - | Required. The name of the speaker to use. Should be the same as in the prompt. | +| `voiceConfig` | 否 | `VoiceConfig` | - | Required. The configuration for the voice to use. | + +### `SpeechConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Config for speech generation and transcription. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `languageCode` | 否 | `string` | - | Optional. The IETF [BCP-47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) language code that the user configured the app to use. Used for speech recognition and synthesis. Valid v… | +| `multiSpeakerVoiceConfig` | 否 | `MultiSpeakerVoiceConfig` | - | Optional. The configuration for the multi-speaker setup. It is mutually exclusive with the voice_config field. | +| `voiceConfig` | 否 | `VoiceConfig` | - | The configuration in case of single-voice output. | + +### `Status` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The Status type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/gr… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `code` | 否 | `integer(int32)` | - | The status code, which should be an enum value of google.rpc.Code. | +| `details` | 否 | `array>` | - | A list of messages that carry the error details. There is a common set of message types for APIs to use. | +| `message` | 否 | `string` | - | A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the google.rpc.Status.details field, or localized by th… | + +### `StreamableHttpTransport` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A transport that can stream HTTP requests and responses. Next ID: 6 | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `headers` | 否 | `object/map` | - | Optional: Fields for authentication headers, timeouts, etc., if needed. | +| `sseReadTimeout` | 否 | `string(google-duration)` | - | Timeout for SSE read operations. | +| `terminateOnClose` | 否 | `boolean` | - | Whether to close the client session when the transport closes. | +| `timeout` | 否 | `string(google-duration)` | - | HTTP timeout for regular operations. | +| `url` | 否 | `string` | - | The full URL for the MCPServer endpoint. Example: "https://api.example.com/mcp" | + +### `TextResponseFormat` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Configuration for text output format. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `mimeType` | 否 | `string` | `MIME_TYPE_UNSPECIFIED`, `APPLICATION_JSON`, `TEXT_PLAIN` | Optional. The MIME type of the text output. | +| `schema` | 否 | `any` | - | Optional. The JSON schema that the output should conform to. Only applicable when mime_type is APPLICATION_JSON. | + +### `ThinkingConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Config for thinking features. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `includeThoughts` | 否 | `boolean` | - | Indicates whether to include thoughts in the response. If true, thoughts are returned only when available. | +| `thinkingBudget` | 否 | `integer(int32)` | - | The number of thoughts tokens that the model should generate. | +| `thinkingLevel` | 否 | `string` | `THINKING_LEVEL_UNSPECIFIED`, `MINIMAL`, `LOW`, `MEDIUM`, `HIGH` | Optional. Controls the maximum depth of the model's internal reasoning process before it produces a response. The default value is model-dependent. Refer to the [Thinking levels g… | + +### `Tool` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Tool details that the model may use to generate response. A Tool is a piece of code that enables the system to interact with external systems to perform an action, or set of actio… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `codeExecution` | 否 | `CodeExecution` | - | Optional. Enables the model to execute code as part of generation. | +| `computerUse` | 否 | `ComputerUse` | - | Optional. Tool to support the model interacting directly with the computer. If enabled, it automatically populates computer-use specific Function Declarations. | +| `fileSearch` | 否 | `FileSearch` | - | Optional. FileSearch tool type. Tool to retrieve knowledge from Semantic Retrieval corpora. | +| `functionDeclarations` | 否 | `array` | - | Optional. A list of FunctionDeclarations available to the model that can be used for function calling. The model or system does not execute the function. Instead the defined funct… | +| `googleMaps` | 否 | `GoogleMaps` | - | Optional. Tool that allows grounding the model's response with geospatial context related to the user's query. | +| `googleSearch` | 否 | `GoogleSearch` | - | Optional. GoogleSearch tool type. Tool to support Google Search in Model. Powered by Google. | +| `googleSearchRetrieval` | 否 | `GoogleSearchRetrieval` | - | Optional. Retrieval tool that is powered by Google search. | +| `mcpServers` | 否 | `array` | - | Optional. MCP Servers to connect to. | +| `urlContext` | 否 | `UrlContext` | - | Optional. Tool to support URL context retrieval. | + +### `ToolCall` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | A predicted server-side ToolCall returned from the model. This message contains information about a tool that the model wants to invoke. The client is NOT expected to execute this… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `args` | 否 | `object/map` | - | Optional. The tool call arguments. Example: {"arg1" : "value1", "arg2" : "value2" , ...} | +| `id` | 否 | `string` | - | Optional. Unique identifier of the tool call. The server returns the tool response with the matching id. | +| `toolType` | 否 | `string` | `TOOL_TYPE_UNSPECIFIED`, `GOOGLE_SEARCH_WEB`, `GOOGLE_SEARCH_IMAGE`, `URL_CONTEXT`, `GOOGLE_MAPS`, `FILE_SEARCH` | Required. The type of tool that was called. | + +### `ToolConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The Tool configuration containing parameters for specifying Tool use in the request. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `functionCallingConfig` | 否 | `FunctionCallingConfig` | - | Optional. Function calling config. | +| `includeServerSideToolInvocations` | 否 | `boolean` | - | Optional. If true, the API response will include the server-side tool calls and responses within the Content message. This allows clients to observe the server's tool interactions. | +| `retrievalConfig` | 否 | `RetrievalConfig` | - | Optional. Retrieval config. | + +### `ToolResponse` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The output from a server-side ToolCall execution. This message contains the results of a tool invocation that was initiated by a ToolCall from the model. The client should pass th… | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `id` | 否 | `string` | - | Optional. The identifier of the tool call this response is for. | +| `response` | 否 | `object/map` | - | Optional. The tool response. | +| `toolType` | 否 | `string` | `TOOL_TYPE_UNSPECIFIED`, `GOOGLE_SEARCH_WEB`, `GOOGLE_SEARCH_IMAGE`, `URL_CONTEXT`, `GOOGLE_MAPS`, `FILE_SEARCH` | Required. The type of tool that was called, matching the tool_type in the corresponding ToolCall. | + +### `TopCandidates` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Candidates with top log probabilities at each decoding step. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `candidates` | 否 | `array` | - | Sorted by log probability in descending order. | + +### `UrlContext` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Tool to support URL context retrieval. | + +### `UrlContextMetadata` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Metadata related to url context retrieval tool. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `urlMetadata` | 否 | `array` | - | List of url context. | + +### `UrlMetadata` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Context of the a single url retrieval. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `retrievedUrl` | 否 | `string` | - | Retrieved url by the tool. | +| `urlRetrievalStatus` | 否 | `string` | `URL_RETRIEVAL_STATUS_UNSPECIFIED`, `URL_RETRIEVAL_STATUS_SUCCESS`, `URL_RETRIEVAL_STATUS_ERROR`, `URL_RETRIEVAL_STATUS_PAYWALL`, `URL_RETRIEVAL_STATUS_UNSAFE` | Status of the url retrieval. | + +### `UsageMetadata` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Metadata on the generation request's token usage. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `cacheTokensDetails` | 否 | `array` | - | Output only. List of modalities of the cached content in the request input. | +| `cachedContentTokenCount` | 否 | `integer(int32)` | - | Number of tokens in the cached part of the prompt (the cached content) | +| `candidatesTokenCount` | 否 | `integer(int32)` | - | Total number of tokens across all the generated response candidates. | +| `candidatesTokensDetails` | 否 | `array` | - | Output only. List of modalities that were returned in the response. | +| `promptTokenCount` | 否 | `integer(int32)` | - | Number of tokens in the prompt. When cached_content is set, this is still the total effective prompt size meaning this includes the number of tokens in the cached content. | +| `promptTokensDetails` | 否 | `array` | - | Output only. List of modalities that were processed in the request input. | +| `serviceTier` | 否 | `string` | `unspecified`, `standard`, `flex`, `priority` | Output only. Service tier of the request. | +| `thoughtsTokenCount` | 否 | `integer(int32)` | - | Output only. Number of tokens of thoughts for thinking models. | +| `toolUsePromptTokenCount` | 否 | `integer(int32)` | - | Output only. Number of tokens present in tool-use prompt(s). | +| `toolUsePromptTokensDetails` | 否 | `array` | - | Output only. List of modalities that were processed for tool-use request inputs. | +| `totalTokenCount` | 否 | `integer(int32)` | - | Total token count for the generation request (prompt + thoughts + response candidates). | + +### `VideoFileMetadata` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Metadata for a video File. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `videoDuration` | 否 | `string(google-duration)` | - | Duration of the video. | + +### `VideoMetadata` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Deprecated: Use GenerateContentRequest.processing_options instead. Metadata describes the input video content. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `endOffset` | 否 | `string(google-duration)` | - | Optional. The end offset of the video. | +| `fps` | 否 | `number(double)` | - | Optional. The frame rate of the video sent to the model. If not specified, the default value will be 1.0. The fps range is (0.0, 24.0]. | +| `startOffset` | 否 | `string(google-duration)` | - | Optional. The start offset of the video. | + +### `VoiceConfig` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | The configuration for the voice to use. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `prebuiltVoiceConfig` | 否 | `PrebuiltVoiceConfig` | - | The configuration for the prebuilt voice to use. | + +### `Web` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Chunk from the web. | + +| 字段 | 必填 | 类型 | 枚举/常量 | 说明 | +| --- | --- | --- | --- | --- | +| `title` | 否 | `string` | - | Output only. Title of the chunk. | +| `uri` | 否 | `string` | - | Output only. URI reference of the chunk. | + +### `WebSearch` + +| 项 | 值 | +| --- | --- | +| 类型 | `object` | +| 说明 | Standard web search for grounding and related configurations. | diff --git a/frontend/src/api/dashboard.ts b/frontend/src/api/dashboard.ts index c4c6b6e76..8cd4ed255 100644 --- a/frontend/src/api/dashboard.ts +++ b/frontend/src/api/dashboard.ts @@ -235,6 +235,12 @@ export interface RequestDetail { has_provider_request_body?: boolean has_response_body?: boolean has_client_response_body?: boolean + body_load_errors?: { + request_body?: boolean + provider_request_body?: boolean + response_body?: boolean + client_response_body?: boolean + } | null metadata?: Record routing?: Record body_capture?: Record diff --git a/frontend/src/api/endpoints/types/provider.ts b/frontend/src/api/endpoints/types/provider.ts index dc7543d62..a0338a86a 100644 --- a/frontend/src/api/endpoints/types/provider.ts +++ b/frontend/src/api/endpoints/types/provider.ts @@ -830,8 +830,17 @@ export interface FailoverRuleItem { } export interface FailoverRulesConfig { - success_failover_patterns: FailoverRuleItem[] - error_stop_patterns: FailoverRuleItem[] + max_retries?: number + stop_status_codes?: number[] + stop_on_status_codes?: number[] + early_stop_status_codes?: number[] + non_retryable_status_codes?: number[] + continue_on_status_codes?: number[] + retryable_status_codes?: number[] + retry_on_status_codes?: number[] + continue_status_codes?: number[] + success_failover_patterns?: FailoverRuleItem[] + error_stop_patterns?: FailoverRuleItem[] } export interface ProviderWithEndpointsSummary { diff --git a/frontend/src/api/me.ts b/frontend/src/api/me.ts index 0cd672e3d..531de4e5e 100644 --- a/frontend/src/api/me.ts +++ b/frontend/src/api/me.ts @@ -65,6 +65,8 @@ export interface UsageRecordDetail { rate_multiplier?: number // 成本倍率(仅管理员可见) response_time_ms?: number | null first_byte_time_ms?: number | null + updated_at?: string | null + response_time_updated_at?: string | null is_stream: boolean upstream_is_stream?: boolean client_requested_stream?: boolean @@ -352,6 +354,8 @@ export const meApi = { rate_multiplier?: number | null response_time_ms: number | null first_byte_time_ms: number | null + updated_at?: string | null + response_time_updated_at?: string | null status_code?: number | null error_message?: string | null api_format?: string | null diff --git a/frontend/src/api/usage.ts b/frontend/src/api/usage.ts index d887da842..f9928e26b 100644 --- a/frontend/src/api/usage.ts +++ b/frontend/src/api/usage.ts @@ -27,6 +27,8 @@ export interface UsageRecord { cost?: number response_time?: number created_at: string + updated_at?: string | null + response_time_updated_at?: string | null has_fallback?: boolean // 🆕 是否发生了 fallback client_family?: string | null client_ip?: string | null @@ -472,7 +474,10 @@ export const usageApi = { provider?: string api_format?: string // API 格式筛选(如 openai:chat, claude:messages) status?: string // 'stream' | 'standard' | 'error' + client_family?: string hide_unknown?: boolean + include_total?: boolean + total_only?: boolean limit?: number offset?: number }): Promise<{ @@ -480,6 +485,7 @@ export const usageApi = { total: number limit: number offset: number + total_is_estimated?: boolean }> { const key = buildCacheKey('usage:records', params as Record | undefined) return dedupedRequest(key, async () => { @@ -488,6 +494,38 @@ export const usageApi = { }) }, + async getAllUsageRecordTotal(params?: { + start_date?: string + end_date?: string + preset?: string + timezone?: string + tz_offset_minutes?: number + search?: string + user_id?: string + username?: string + model?: string + provider?: string + api_format?: string + status?: string + client_family?: string + hide_unknown?: boolean + }): Promise { + const requestParams = compactParams({ + ...params, + include_total: true, + total_only: true, + limit: 1, + offset: 0, + }) + const key = buildCacheKey('usage:records:total', requestParams) + return dedupedRequest(key, async () => { + const response = await apiClient.get('/api/admin/usage/records', { + params: requestParams, + }) + return assertNumber(response.data.total, 'total') + }) + }, + /** * 获取活跃请求的状态(轻量级接口,用于轮询更新) * @param ids 可选,逗号分隔的请求 ID 列表 @@ -511,6 +549,8 @@ export const usageApi = { rate_multiplier?: number | null response_time_ms: number | null first_byte_time_ms: number | null + updated_at?: string | null + response_time_updated_at?: string | null status_code?: number | null error_message?: string | null provider?: string | null diff --git a/frontend/src/features/providers/components/FailoverRulesDialog.vue b/frontend/src/features/providers/components/FailoverRulesDialog.vue index 5aefb8652..21a3adb6e 100644 --- a/frontend/src/features/providers/components/FailoverRulesDialog.vue +++ b/frontend/src/features/providers/components/FailoverRulesDialog.vue @@ -19,45 +19,95 @@ HTTP 200 但响应体匹配正则时,视为失败并触发转移

- +
+ + + +
- 暂无规则 -
- -
- - + {{ successJsonError }} +
+

+ 仅管理成功转移规则;JSON 应为数组,pattern 必填。 +

+ + @@ -68,54 +118,104 @@ 错误终止规则

- HTTP 非 200 且响应体匹配正则时,停止转移并直接返回错误。可选填状态码缩小匹配范围 + HTTP 非 200 且规则命中时,停止转移并直接返回错误。状态码不填则所有错误状态都尝试匹配正则

- +
+ + + +
- 暂无规则 +