mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-11 19:59:50 +08:00
Merge upstream/main into main
This commit is contained in:
@@ -41,7 +41,8 @@ use super::{
|
|||||||
LocalSameFormatProviderSpec,
|
LocalSameFormatProviderSpec,
|
||||||
};
|
};
|
||||||
use crate::ai_serving::planner::standard::{
|
use crate::ai_serving::planner::standard::{
|
||||||
codex_model_capabilities_for_transport, same_format_provider_request_body_failure_extra_data,
|
codex_model_capabilities_for_transport, openai_provider_request_contract_failure_extra_data,
|
||||||
|
same_format_provider_request_body_failure_extra_data,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn resolve_same_format_provider_transport_unsupported_reason_for_trace(
|
pub(crate) fn resolve_same_format_provider_transport_unsupported_reason_for_trace(
|
||||||
@@ -267,33 +268,39 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
|
|||||||
prepared.mapped_model.as_str(),
|
prepared.mapped_model.as_str(),
|
||||||
source_model,
|
source_model,
|
||||||
);
|
);
|
||||||
if crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
|
if let Err(violation) =
|
||||||
&mut base_provider_request_body,
|
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
|
||||||
crate::ai_serving::OpenAiProviderRequestFinalization {
|
&mut base_provider_request_body,
|
||||||
source_api_format: spec.api_format,
|
crate::ai_serving::OpenAiProviderRequestFinalization {
|
||||||
provider_api_format: prepared.provider_api_format.as_str(),
|
source_api_format: spec.api_format,
|
||||||
provider_type: transport.provider.provider_type.as_str(),
|
provider_api_format: prepared.provider_api_format.as_str(),
|
||||||
provider_model: prepared.mapped_model.as_str(),
|
provider_type: transport.provider.provider_type.as_str(),
|
||||||
source_model,
|
provider_model: prepared.mapped_model.as_str(),
|
||||||
body_rules: transport.endpoint.body_rules.as_ref(),
|
source_model,
|
||||||
upstream_is_stream: prepared.upstream_is_stream,
|
body_rules: transport.endpoint.body_rules.as_ref(),
|
||||||
require_body_stream_field: request_requires_body_stream_field(
|
upstream_is_stream: prepared.upstream_is_stream,
|
||||||
body_json,
|
require_body_stream_field: request_requires_body_stream_field(
|
||||||
prepared.force_body_stream_field,
|
body_json,
|
||||||
),
|
prepared.force_body_stream_field,
|
||||||
},
|
),
|
||||||
codex_model_capabilities.as_ref(),
|
},
|
||||||
)
|
codex_model_capabilities.as_ref(),
|
||||||
.is_err()
|
)
|
||||||
{
|
{
|
||||||
mark_skipped_local_same_format_provider_candidate(
|
mark_skipped_local_same_format_provider_candidate_with_extra_data(
|
||||||
state,
|
state,
|
||||||
input,
|
input,
|
||||||
trace_id,
|
trace_id,
|
||||||
candidate,
|
candidate,
|
||||||
attempt.candidate_index,
|
attempt.candidate_index,
|
||||||
&attempt.candidate_id,
|
&attempt.candidate_id,
|
||||||
"provider_request_body_missing",
|
"provider_request_body_build_failed",
|
||||||
|
Some(openai_provider_request_contract_failure_extra_data(
|
||||||
|
&violation,
|
||||||
|
spec.api_format,
|
||||||
|
prepared.provider_api_format.as_str(),
|
||||||
|
"same_format_provider_request_finalization",
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ use crate::ai_serving::planner::spec_metadata::local_standard_spec_metadata;
|
|||||||
use crate::ai_serving::planner::standard::{
|
use crate::ai_serving::planner::standard::{
|
||||||
apply_codex_openai_special_headers, apply_deepseek_tool_call_thinking_compat,
|
apply_codex_openai_special_headers, apply_deepseek_tool_call_thinking_compat,
|
||||||
codex_model_capabilities_for_transport, is_deepseek_provider,
|
codex_model_capabilities_for_transport, is_deepseek_provider,
|
||||||
request_body_build_failure_extra_data, request_conversion_failure_extra_data,
|
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
|
||||||
|
request_conversion_failure_extra_data,
|
||||||
};
|
};
|
||||||
use crate::ai_serving::transport::kiro::{
|
use crate::ai_serving::transport::kiro::{
|
||||||
build_kiro_provider_headers, build_kiro_provider_request_body,
|
build_kiro_provider_headers, build_kiro_provider_request_body,
|
||||||
@@ -748,24 +749,24 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
|
|||||||
prepared_candidate.mapped_model.as_str(),
|
prepared_candidate.mapped_model.as_str(),
|
||||||
source_model,
|
source_model,
|
||||||
);
|
);
|
||||||
if crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
|
if let Err(violation) =
|
||||||
&mut provider_request_body,
|
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
|
||||||
crate::ai_serving::OpenAiProviderRequestFinalization {
|
&mut provider_request_body,
|
||||||
source_api_format: spec_metadata.api_format,
|
crate::ai_serving::OpenAiProviderRequestFinalization {
|
||||||
provider_api_format,
|
source_api_format: spec_metadata.api_format,
|
||||||
provider_type: transport.provider.provider_type.as_str(),
|
provider_api_format,
|
||||||
provider_model: prepared_candidate.mapped_model.as_str(),
|
provider_type: transport.provider.provider_type.as_str(),
|
||||||
source_model,
|
provider_model: prepared_candidate.mapped_model.as_str(),
|
||||||
body_rules: transport.endpoint.body_rules.as_ref(),
|
source_model,
|
||||||
upstream_is_stream,
|
body_rules: transport.endpoint.body_rules.as_ref(),
|
||||||
require_body_stream_field: request_requires_body_stream_field(
|
upstream_is_stream,
|
||||||
body_json,
|
require_body_stream_field: request_requires_body_stream_field(
|
||||||
force_body_stream_field,
|
body_json,
|
||||||
),
|
force_body_stream_field,
|
||||||
},
|
),
|
||||||
codex_model_capabilities.as_ref(),
|
},
|
||||||
)
|
codex_model_capabilities.as_ref(),
|
||||||
.is_err()
|
)
|
||||||
{
|
{
|
||||||
mark_skipped_local_standard_candidate_with_extra_data(
|
mark_skipped_local_standard_candidate_with_extra_data(
|
||||||
state,
|
state,
|
||||||
@@ -775,15 +776,12 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
|
|||||||
attempt.candidate_index,
|
attempt.candidate_index,
|
||||||
&attempt.candidate_id,
|
&attempt.candidate_id,
|
||||||
"provider_request_body_build_failed",
|
"provider_request_body_build_failed",
|
||||||
request_conversion_failure_extra_data(
|
Some(openai_provider_request_contract_failure_extra_data(
|
||||||
body_json,
|
&violation,
|
||||||
spec_metadata.api_format,
|
spec_metadata.api_format,
|
||||||
provider_api_format,
|
provider_api_format,
|
||||||
Some(prepared_candidate.mapped_model.as_str()),
|
|
||||||
Some(parts.uri.path()),
|
|
||||||
upstream_is_stream,
|
|
||||||
"standard_family_request_finalization",
|
"standard_family_request_finalization",
|
||||||
),
|
)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ pub(crate) use crate::ai_serving::{
|
|||||||
normalize_openai_responses_request_to_openai_chat_request, parse_openai_tool_result_content,
|
normalize_openai_responses_request_to_openai_chat_request, parse_openai_tool_result_content,
|
||||||
};
|
};
|
||||||
pub(crate) use aether_ai_serving::{
|
pub(crate) use aether_ai_serving::{
|
||||||
request_body_build_failure_extra_data, request_conversion_failure_extra_data,
|
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
|
||||||
same_format_provider_request_body_failure_extra_data,
|
request_conversion_failure_extra_data, same_format_provider_request_body_failure_extra_data,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn build_standard_upstream_url(
|
pub(crate) fn build_standard_upstream_url(
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ pub(crate) fn build_local_openai_responses_request_body_with_codex_model_capabil
|
|||||||
&mut provider_request_body,
|
&mut provider_request_body,
|
||||||
provider_api_format,
|
provider_api_format,
|
||||||
);
|
);
|
||||||
|
crate::ai_serving::strip_incompatible_openai_responses_reasoning_items(
|
||||||
|
&mut provider_request_body,
|
||||||
|
provider_api_format,
|
||||||
|
);
|
||||||
enforce_provider_body_stream_policy(
|
enforce_provider_body_stream_policy(
|
||||||
&mut provider_request_body,
|
&mut provider_request_body,
|
||||||
provider_api_format,
|
provider_api_format,
|
||||||
@@ -172,6 +176,10 @@ pub(crate) fn build_cross_format_openai_responses_request_body_with_codex_model_
|
|||||||
&mut provider_request_body,
|
&mut provider_request_body,
|
||||||
provider_api_format,
|
provider_api_format,
|
||||||
);
|
);
|
||||||
|
crate::ai_serving::strip_incompatible_openai_responses_reasoning_items(
|
||||||
|
&mut provider_request_body,
|
||||||
|
provider_api_format,
|
||||||
|
);
|
||||||
enforce_provider_body_stream_policy(
|
enforce_provider_body_stream_policy(
|
||||||
&mut provider_request_body,
|
&mut provider_request_body,
|
||||||
provider_api_format,
|
provider_api_format,
|
||||||
|
|||||||
@@ -152,6 +152,43 @@ fn local_openai_responses_wrapper_preserves_body_order_after_edits() {
|
|||||||
assert!(provider_request_body.get("instructions").is_none());
|
assert!(provider_request_body.get("instructions").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_openai_responses_wrapper_strips_foreign_reasoning_item_ids() {
|
||||||
|
let body_json = json!({
|
||||||
|
"model": "gpt-5.4",
|
||||||
|
"input": [
|
||||||
|
{"type": "reasoning", "id": "rs_provider_123", "summary": []},
|
||||||
|
{
|
||||||
|
"type": "reasoning",
|
||||||
|
"id": "item_72d3bd8d367d01977ace23f1",
|
||||||
|
"summary": []
|
||||||
|
},
|
||||||
|
{"type": "message", "role": "user", "content": "continue"}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let provider_request_body = build_local_openai_responses_request_body(
|
||||||
|
&body_json,
|
||||||
|
"gpt-5.4",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"codex",
|
||||||
|
"openai:responses",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
&http::HeaderMap::new(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.expect("local OpenAI Responses body should build");
|
||||||
|
|
||||||
|
let input = provider_request_body["input"]
|
||||||
|
.as_array()
|
||||||
|
.expect("input array");
|
||||||
|
assert_eq!(input.len(), 2);
|
||||||
|
assert_eq!(input[0]["id"], "rs_provider_123");
|
||||||
|
assert_eq!(input[1]["type"], "message");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_openai_responses_compact_wrapper_strips_store_for_same_format_requests() {
|
fn local_openai_responses_compact_wrapper_strips_store_for_same_format_requests() {
|
||||||
let body_json = json!({
|
let body_json = json!({
|
||||||
|
|||||||
+18
-25
@@ -29,7 +29,8 @@ use crate::ai_serving::planner::standard::{
|
|||||||
apply_deepseek_tool_call_thinking_compat, build_cross_format_openai_chat_request_body,
|
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_cross_format_openai_chat_upstream_url, build_local_openai_chat_request_body,
|
||||||
build_local_openai_chat_upstream_url, codex_model_capabilities_for_transport,
|
build_local_openai_chat_upstream_url, codex_model_capabilities_for_transport,
|
||||||
request_body_build_failure_extra_data, request_conversion_failure_extra_data,
|
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
|
||||||
|
request_conversion_failure_extra_data,
|
||||||
};
|
};
|
||||||
use crate::ai_serving::transport::antigravity::is_antigravity_provider_transport;
|
use crate::ai_serving::transport::antigravity::is_antigravity_provider_transport;
|
||||||
use crate::ai_serving::transport::auth::resolve_local_openai_bearer_auth;
|
use crate::ai_serving::transport::auth::resolve_local_openai_bearer_auth;
|
||||||
@@ -109,7 +110,7 @@ fn finalize_openai_chat_provider_request_body(
|
|||||||
original_body: &Value,
|
original_body: &Value,
|
||||||
transport: &GatewayProviderTransportSnapshot,
|
transport: &GatewayProviderTransportSnapshot,
|
||||||
mapped_model: &str,
|
mapped_model: &str,
|
||||||
) -> bool {
|
) -> Option<Value> {
|
||||||
if let Some(mapping) = custom_directive_mapping {
|
if let Some(mapping) = custom_directive_mapping {
|
||||||
crate::ai_serving::apply_model_directive_mapping_patch(provider_request_body, mapping);
|
crate::ai_serving::apply_model_directive_mapping_patch(provider_request_body, mapping);
|
||||||
}
|
}
|
||||||
@@ -156,7 +157,15 @@ fn finalize_openai_chat_provider_request_body(
|
|||||||
},
|
},
|
||||||
codex_model_capabilities.as_ref(),
|
codex_model_capabilities.as_ref(),
|
||||||
)
|
)
|
||||||
.is_ok()
|
.err()
|
||||||
|
.map(|violation| {
|
||||||
|
openai_provider_request_contract_failure_extra_data(
|
||||||
|
&violation,
|
||||||
|
"openai:chat",
|
||||||
|
provider_api_format,
|
||||||
|
"openai_chat_request_finalization",
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -288,7 +297,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
|
|||||||
.await;
|
.await;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
if !finalize_openai_chat_provider_request_body(
|
if let Some(extra_data) = finalize_openai_chat_provider_request_body(
|
||||||
&mut provider_request_body,
|
&mut provider_request_body,
|
||||||
model_directive_mapping.as_ref(),
|
model_directive_mapping.as_ref(),
|
||||||
provider_api_format,
|
provider_api_format,
|
||||||
@@ -306,11 +315,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
|
|||||||
candidate_index,
|
candidate_index,
|
||||||
candidate_id,
|
candidate_id,
|
||||||
"provider_request_body_build_failed",
|
"provider_request_body_build_failed",
|
||||||
request_body_build_failure_extra_data(
|
Some(extra_data),
|
||||||
body_json,
|
|
||||||
"openai:chat",
|
|
||||||
provider_api_format,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -501,7 +506,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
|
|||||||
"openai_chat_payload_body_build",
|
"openai_chat_payload_body_build",
|
||||||
body_build_started_at.elapsed().as_millis() as u64,
|
body_build_started_at.elapsed().as_millis() as u64,
|
||||||
);
|
);
|
||||||
if !finalize_openai_chat_provider_request_body(
|
if let Some(extra_data) = finalize_openai_chat_provider_request_body(
|
||||||
&mut provider_request_body,
|
&mut provider_request_body,
|
||||||
model_directive_mapping.as_ref(),
|
model_directive_mapping.as_ref(),
|
||||||
"openai:chat",
|
"openai:chat",
|
||||||
@@ -519,11 +524,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
|
|||||||
candidate_index,
|
candidate_index,
|
||||||
candidate_id,
|
candidate_id,
|
||||||
"provider_request_body_build_failed",
|
"provider_request_body_build_failed",
|
||||||
request_body_build_failure_extra_data(
|
Some(extra_data),
|
||||||
body_json,
|
|
||||||
"openai:chat",
|
|
||||||
provider_api_format,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -813,7 +814,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
|
|||||||
.await;
|
.await;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
if !finalize_openai_chat_provider_request_body(
|
if let Some(extra_data) = finalize_openai_chat_provider_request_body(
|
||||||
&mut provider_request_body,
|
&mut provider_request_body,
|
||||||
model_directive_mapping.as_ref(),
|
model_directive_mapping.as_ref(),
|
||||||
provider_api_format.as_str(),
|
provider_api_format.as_str(),
|
||||||
@@ -831,15 +832,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
|
|||||||
candidate_index,
|
candidate_index,
|
||||||
candidate_id,
|
candidate_id,
|
||||||
"provider_request_body_build_failed",
|
"provider_request_body_build_failed",
|
||||||
request_conversion_failure_extra_data(
|
Some(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;
|
.await;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
+24
-26
@@ -32,7 +32,8 @@ use crate::ai_serving::planner::standard::{
|
|||||||
build_cross_format_openai_responses_upstream_url,
|
build_cross_format_openai_responses_upstream_url,
|
||||||
build_local_openai_responses_request_body_with_codex_model_capabilities,
|
build_local_openai_responses_request_body_with_codex_model_capabilities,
|
||||||
build_local_openai_responses_upstream_url, codex_model_capabilities_for_transport,
|
build_local_openai_responses_upstream_url, codex_model_capabilities_for_transport,
|
||||||
request_body_build_failure_extra_data, request_conversion_failure_extra_data,
|
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
|
||||||
|
request_conversion_failure_extra_data,
|
||||||
};
|
};
|
||||||
use crate::ai_serving::transport::antigravity::is_antigravity_provider_transport;
|
use crate::ai_serving::transport::antigravity::is_antigravity_provider_transport;
|
||||||
use crate::ai_serving::transport::auth::{
|
use crate::ai_serving::transport::auth::{
|
||||||
@@ -530,24 +531,24 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
|
|||||||
provider_api_format,
|
provider_api_format,
|
||||||
Some(body_json),
|
Some(body_json),
|
||||||
);
|
);
|
||||||
if crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
|
if let Err(violation) =
|
||||||
&mut base_provider_request_body,
|
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
|
||||||
crate::ai_serving::OpenAiProviderRequestFinalization {
|
&mut base_provider_request_body,
|
||||||
source_api_format: spec_metadata.api_format,
|
crate::ai_serving::OpenAiProviderRequestFinalization {
|
||||||
provider_api_format,
|
source_api_format: spec_metadata.api_format,
|
||||||
provider_type: transport.provider.provider_type.as_str(),
|
provider_api_format,
|
||||||
provider_model: mapped_model.as_str(),
|
provider_type: transport.provider.provider_type.as_str(),
|
||||||
source_model,
|
provider_model: mapped_model.as_str(),
|
||||||
body_rules: transport.endpoint.body_rules.as_ref(),
|
source_model,
|
||||||
upstream_is_stream,
|
body_rules: transport.endpoint.body_rules.as_ref(),
|
||||||
require_body_stream_field: request_requires_body_stream_field(
|
upstream_is_stream,
|
||||||
body_json,
|
require_body_stream_field: request_requires_body_stream_field(
|
||||||
force_body_stream_field,
|
body_json,
|
||||||
),
|
force_body_stream_field,
|
||||||
},
|
),
|
||||||
codex_model_capabilities.as_ref(),
|
},
|
||||||
)
|
codex_model_capabilities.as_ref(),
|
||||||
.is_err()
|
)
|
||||||
{
|
{
|
||||||
mark_skipped_local_openai_responses_candidate_with_extra_data(
|
mark_skipped_local_openai_responses_candidate_with_extra_data(
|
||||||
state,
|
state,
|
||||||
@@ -557,15 +558,12 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
|
|||||||
candidate_index,
|
candidate_index,
|
||||||
candidate_id,
|
candidate_id,
|
||||||
"provider_request_body_build_failed",
|
"provider_request_body_build_failed",
|
||||||
request_conversion_failure_extra_data(
|
Some(openai_provider_request_contract_failure_extra_data(
|
||||||
body_json,
|
&violation,
|
||||||
spec_metadata.api_format,
|
spec_metadata.api_format,
|
||||||
provider_api_format,
|
provider_api_format,
|
||||||
Some(mapped_model.as_str()),
|
"openai_responses_request_finalization",
|
||||||
Some(parts.uri.path()),
|
)),
|
||||||
upstream_is_stream,
|
|
||||||
"openai_responses_request_conversion",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
@@ -172,7 +172,9 @@ pub(crate) use aether_ai_formats::api::{
|
|||||||
pub(crate) use aether_ai_formats::{
|
pub(crate) use aether_ai_formats::{
|
||||||
api_format_defaults_to_client_error_failover, api_format_defaults_to_non_stream,
|
api_format_defaults_to_client_error_failover, api_format_defaults_to_non_stream,
|
||||||
api_format_permission_covers, intersect_api_format_allowed_lists, is_embedding_api_format,
|
api_format_permission_covers, intersect_api_format_allowed_lists, is_embedding_api_format,
|
||||||
is_rerank_api_format, openai_responses_request_operation, ApiOperation, ClientSurface,
|
is_rerank_api_format, openai_responses_request_operation,
|
||||||
|
openai_responses_synthetic_reasoning_item_id,
|
||||||
|
strip_incompatible_openai_responses_reasoning_items, ApiOperation, ClientSurface,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn plan_kind_matches_api_operation(
|
pub(crate) fn plan_kind_matches_api_operation(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use http::Uri;
|
use http::Uri;
|
||||||
|
|
||||||
use super::{classify_control_route, headers};
|
use super::{classify_control_route, headers};
|
||||||
|
use crate::control::GatewayPublicRequestContext;
|
||||||
|
use crate::handlers::shared::local_proxy_route_requires_buffered_body;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn classifies_admin_pool_overview_as_admin_proxy_route() {
|
fn classifies_admin_pool_overview_as_admin_proxy_route() {
|
||||||
@@ -92,6 +94,16 @@ fn classifies_admin_pool_provider_key_routes_as_admin_proxy_route() {
|
|||||||
batch_update.route_kind.as_deref(),
|
batch_update.route_kind.as_deref(),
|
||||||
Some("batch_update_keys")
|
Some("batch_update_keys")
|
||||||
);
|
);
|
||||||
|
let batch_update_context = GatewayPublicRequestContext::from_request_parts(
|
||||||
|
"trace-admin-pool-batch-update",
|
||||||
|
&http::Method::PATCH,
|
||||||
|
&batch_update_uri,
|
||||||
|
&headers,
|
||||||
|
Some(batch_update),
|
||||||
|
);
|
||||||
|
assert!(local_proxy_route_requires_buffered_body(
|
||||||
|
&batch_update_context
|
||||||
|
));
|
||||||
|
|
||||||
let resolve_selection_uri: Uri = "/api/admin/pool/provider-1/keys/resolve-selection"
|
let resolve_selection_uri: Uri = "/api/admin/pool/provider-1/keys/resolve-selection"
|
||||||
.parse()
|
.parse()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ use crate::ai_serving::api::{
|
|||||||
CanonicalContentPart, CanonicalStreamEvent, CanonicalStreamFrame, ClaudeClientEmitter,
|
CanonicalContentPart, CanonicalStreamEvent, CanonicalStreamFrame, ClaudeClientEmitter,
|
||||||
OpenAIChatClientEmitter, OpenAIResponsesClientEmitter, StreamingCanonicalUsage,
|
OpenAIChatClientEmitter, OpenAIResponsesClientEmitter, StreamingCanonicalUsage,
|
||||||
};
|
};
|
||||||
|
use crate::ai_serving::openai_responses_synthetic_reasoning_item_id;
|
||||||
use crate::clock::current_unix_secs;
|
use crate::clock::current_unix_secs;
|
||||||
use crate::execution_runtime::ndjson::encode_stream_frame_ndjson;
|
use crate::execution_runtime::ndjson::encode_stream_frame_ndjson;
|
||||||
use crate::execution_runtime::transport::{
|
use crate::execution_runtime::transport::{
|
||||||
@@ -2705,7 +2706,7 @@ fn openai_responses_body(
|
|||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
if !collected.thinking.trim().is_empty() {
|
if !collected.thinking.trim().is_empty() {
|
||||||
output.push(json!({
|
output.push(json!({
|
||||||
"id": format!("{response_id}_rs_0"),
|
"id": openai_responses_synthetic_reasoning_item_id(&response_id, 0),
|
||||||
"type": "reasoning",
|
"type": "reasoning",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"summary": [{
|
"summary": [{
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ use aether_data_contracts::repository::candidates::{
|
|||||||
use aether_data_contracts::repository::provider_catalog::{
|
use aether_data_contracts::repository::provider_catalog::{
|
||||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||||
};
|
};
|
||||||
|
use aether_data_contracts::repository::usage::{
|
||||||
|
ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY, ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY,
|
||||||
|
};
|
||||||
use aether_usage_runtime::{
|
use aether_usage_runtime::{
|
||||||
build_usage_event_data_seed, UsageEvent, UsageEventData, UsageEventType,
|
build_usage_event_data_seed, UsageEvent, UsageEventData, UsageEventType,
|
||||||
};
|
};
|
||||||
@@ -195,6 +198,7 @@ pub(crate) async fn build_local_execution_exhaustion(
|
|||||||
data.provider_api_key_id = data
|
data.provider_api_key_id = data
|
||||||
.provider_api_key_id
|
.provider_api_key_id
|
||||||
.or_else(|| candidate.key_id.clone());
|
.or_else(|| candidate.key_id.clone());
|
||||||
|
attach_runtime_miss_candidate_usage_metadata(&mut data, candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
exhaustion.data = data;
|
exhaustion.data = data;
|
||||||
@@ -385,23 +389,25 @@ pub(crate) async fn record_failed_usage_for_runtime_miss_request(
|
|||||||
|
|
||||||
let selected_candidate =
|
let selected_candidate =
|
||||||
select_last_runtime_miss_executed_candidate(&context.candidate_contexts);
|
select_last_runtime_miss_executed_candidate(&context.candidate_contexts);
|
||||||
let api_format = selected_candidate
|
let routing_candidate = selected_candidate
|
||||||
|
.or_else(|| select_last_runtime_miss_routing_candidate(&context.candidate_contexts));
|
||||||
|
let api_format = routing_candidate
|
||||||
.and_then(|value| value.client_api_format.clone())
|
.and_then(|value| value.client_api_format.clone())
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
trimmed_non_empty(decision.and_then(|value| value.auth_endpoint_signature.as_deref()))
|
trimmed_non_empty(decision.and_then(|value| value.auth_endpoint_signature.as_deref()))
|
||||||
});
|
});
|
||||||
let provider_api_format = selected_candidate
|
let provider_api_format = routing_candidate
|
||||||
.and_then(|value| value.provider_api_format.clone())
|
.and_then(|value| value.provider_api_format.clone())
|
||||||
.or_else(|| api_format.clone());
|
.or_else(|| api_format.clone());
|
||||||
let provider_name = selected_candidate
|
let provider_name = routing_candidate
|
||||||
.and_then(|value| value.provider_name.clone())
|
.and_then(|value| value.provider_name.clone())
|
||||||
.or_else(|| selected_candidate.and_then(|value| value.candidate.provider_id.clone()))
|
.or_else(|| routing_candidate.and_then(|value| value.candidate.provider_id.clone()))
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
let model = trimmed_non_empty(diagnostic.and_then(|value| value.requested_model.as_deref()))
|
let model = trimmed_non_empty(diagnostic.and_then(|value| value.requested_model.as_deref()))
|
||||||
.or_else(|| selected_candidate.and_then(|value| value.global_model_name.clone()))
|
.or_else(|| routing_candidate.and_then(|value| value.global_model_name.clone()))
|
||||||
.or_else(|| selected_candidate.and_then(|value| value.selected_provider_model_name.clone()))
|
.or_else(|| routing_candidate.and_then(|value| value.selected_provider_model_name.clone()))
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
let target_model = selected_candidate
|
let target_model = routing_candidate
|
||||||
.and_then(|value| value.selected_provider_model_name.clone())
|
.and_then(|value| value.selected_provider_model_name.clone())
|
||||||
.filter(|value| !value.eq_ignore_ascii_case(model.as_str()));
|
.filter(|value| !value.eq_ignore_ascii_case(model.as_str()));
|
||||||
|
|
||||||
@@ -436,10 +442,10 @@ pub(crate) async fn record_failed_usage_for_runtime_miss_request(
|
|||||||
provider_name,
|
provider_name,
|
||||||
model,
|
model,
|
||||||
target_model,
|
target_model,
|
||||||
provider_id: selected_candidate.and_then(|value| value.candidate.provider_id.clone()),
|
provider_id: routing_candidate.and_then(|value| value.candidate.provider_id.clone()),
|
||||||
provider_endpoint_id: selected_candidate
|
provider_endpoint_id: routing_candidate
|
||||||
.and_then(|value| value.candidate.endpoint_id.clone()),
|
.and_then(|value| value.candidate.endpoint_id.clone()),
|
||||||
provider_api_key_id: selected_candidate.and_then(|value| value.candidate.key_id.clone()),
|
provider_api_key_id: routing_candidate.and_then(|value| value.candidate.key_id.clone()),
|
||||||
request_type: Some(infer_request_type(api_format.as_deref())),
|
request_type: Some(infer_request_type(api_format.as_deref())),
|
||||||
api_format: api_format.clone(),
|
api_format: api_format.clone(),
|
||||||
api_family: api_format
|
api_family: api_format
|
||||||
@@ -459,7 +465,7 @@ pub(crate) async fn record_failed_usage_for_runtime_miss_request(
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(infer_endpoint_kind)
|
.and_then(infer_endpoint_kind)
|
||||||
.map(ToOwned::to_owned),
|
.map(ToOwned::to_owned),
|
||||||
has_format_conversion: selected_candidate.and_then(|value| {
|
has_format_conversion: routing_candidate.and_then(|value| {
|
||||||
value
|
value
|
||||||
.client_api_format
|
.client_api_format
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -478,13 +484,16 @@ pub(crate) async fn record_failed_usage_for_runtime_miss_request(
|
|||||||
client_response_body: Some(client_body),
|
client_response_body: Some(client_body),
|
||||||
..UsageEventData::default()
|
..UsageEventData::default()
|
||||||
};
|
};
|
||||||
|
if let Some(candidate) = routing_candidate {
|
||||||
|
insert_runtime_miss_candidate_usage_metadata(&mut request_metadata, &candidate.candidate);
|
||||||
|
}
|
||||||
apply_runtime_miss_usage_routing(
|
apply_runtime_miss_usage_routing(
|
||||||
&mut data,
|
&mut data,
|
||||||
&mut request_metadata,
|
&mut request_metadata,
|
||||||
execution_path,
|
execution_path,
|
||||||
selected_candidate.map(|value| value.candidate.id.as_str()),
|
routing_candidate.map(|value| value.candidate.id.as_str()),
|
||||||
selected_candidate.map(|value| value.candidate.candidate_index),
|
routing_candidate.map(|value| value.candidate.candidate_index),
|
||||||
selected_candidate.and_then(|value| value.key_name.as_deref()),
|
routing_candidate.and_then(|value| value.key_name.as_deref()),
|
||||||
diagnostic,
|
diagnostic,
|
||||||
decision.and_then(|value| value.route_family.as_deref()),
|
decision.and_then(|value| value.route_family.as_deref()),
|
||||||
decision.and_then(|value| value.route_kind.as_deref()),
|
decision.and_then(|value| value.route_kind.as_deref()),
|
||||||
@@ -637,6 +646,22 @@ fn select_last_runtime_miss_executed_candidate(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_last_runtime_miss_routing_candidate(
|
||||||
|
candidates: &[RuntimeMissCandidateContext],
|
||||||
|
) -> Option<&RuntimeMissCandidateContext> {
|
||||||
|
candidates.iter().max_by_key(|candidate| {
|
||||||
|
(
|
||||||
|
candidate.candidate.retry_index,
|
||||||
|
candidate.candidate.candidate_index,
|
||||||
|
candidate
|
||||||
|
.candidate
|
||||||
|
.finished_at_unix_ms
|
||||||
|
.or(candidate.candidate.started_at_unix_ms)
|
||||||
|
.unwrap_or(candidate.candidate.created_at_unix_ms),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn request_candidate_represents_provider_execution(candidate: &StoredRequestCandidate) -> bool {
|
fn request_candidate_represents_provider_execution(candidate: &StoredRequestCandidate) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
candidate.status,
|
candidate.status,
|
||||||
@@ -935,6 +960,66 @@ fn candidate_extra_data_string(candidate: &StoredRequestCandidate, key: &str) ->
|
|||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn attach_runtime_miss_candidate_usage_metadata(
|
||||||
|
data: &mut UsageEventData,
|
||||||
|
candidate: &StoredRequestCandidate,
|
||||||
|
) {
|
||||||
|
let mut metadata = match data.request_metadata.take() {
|
||||||
|
Some(Value::Object(object)) => object,
|
||||||
|
Some(other) => Map::from_iter([("seed".to_string(), other)]),
|
||||||
|
None => Map::new(),
|
||||||
|
};
|
||||||
|
insert_runtime_miss_candidate_usage_metadata(&mut metadata, candidate);
|
||||||
|
data.request_metadata = (!metadata.is_empty()).then_some(Value::Object(metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_runtime_miss_candidate_usage_metadata(
|
||||||
|
metadata: &mut Map<String, Value>,
|
||||||
|
candidate: &StoredRequestCandidate,
|
||||||
|
) {
|
||||||
|
if let Some(skip_reason) = candidate
|
||||||
|
.skip_reason
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
metadata.insert(
|
||||||
|
ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY.to_string(),
|
||||||
|
Value::String(skip_reason.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let diagnostic = candidate
|
||||||
|
.extra_data
|
||||||
|
.as_ref()
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.and_then(|extra_data| {
|
||||||
|
extra_data
|
||||||
|
.get("failure_diagnostic")
|
||||||
|
.filter(|value| {
|
||||||
|
value.as_object().is_some_and(|diagnostic| {
|
||||||
|
diagnostic.get("safe_to_show") != Some(&Value::Bool(false))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
extra_data
|
||||||
|
.get("request_conversion_error")
|
||||||
|
.filter(|v| v.is_object())
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
extra_data
|
||||||
|
.get("request_body_build_error")
|
||||||
|
.filter(|v| v.is_object())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if let Some(diagnostic) = diagnostic {
|
||||||
|
metadata.insert(
|
||||||
|
ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY.to_string(),
|
||||||
|
diagnostic.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn runtime_miss_candidate_failure_diagnostic(
|
fn runtime_miss_candidate_failure_diagnostic(
|
||||||
candidate: &RuntimeMissCandidateContext,
|
candidate: &RuntimeMissCandidateContext,
|
||||||
) -> Option<RuntimeMissFailureDiagnostic> {
|
) -> Option<RuntimeMissFailureDiagnostic> {
|
||||||
@@ -1170,9 +1255,10 @@ fn trimmed_non_empty(value: Option<&str>) -> Option<String> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
apply_runtime_miss_usage_routing, beautify_local_execution_client_error_message,
|
apply_runtime_miss_usage_routing, beautify_local_execution_client_error_message,
|
||||||
|
insert_runtime_miss_candidate_usage_metadata,
|
||||||
request_candidate_represents_provider_execution, runtime_miss_client_error_body,
|
request_candidate_represents_provider_execution, runtime_miss_client_error_body,
|
||||||
select_last_runtime_miss_executed_candidate, LocalExecutionRuntimeMissContext,
|
select_last_runtime_miss_executed_candidate, select_last_runtime_miss_routing_candidate,
|
||||||
RuntimeMissCandidateContext,
|
LocalExecutionRuntimeMissContext, RuntimeMissCandidateContext,
|
||||||
};
|
};
|
||||||
use crate::constants::EXECUTION_PATH_LOCAL_EXECUTION_RUNTIME_MISS;
|
use crate::constants::EXECUTION_PATH_LOCAL_EXECUTION_RUNTIME_MISS;
|
||||||
use crate::state::LocalExecutionRuntimeMissDiagnostic;
|
use crate::state::LocalExecutionRuntimeMissDiagnostic;
|
||||||
@@ -1263,7 +1349,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_miss_executed_candidate_selection_ignores_skipped_only_histories() {
|
fn runtime_miss_executed_candidate_selection_ignores_skipped_only_histories() {
|
||||||
let skipped_candidate = StoredRequestCandidate::new(
|
let mut skipped_candidate = StoredRequestCandidate::new(
|
||||||
"cand-skipped".to_string(),
|
"cand-skipped".to_string(),
|
||||||
"req-1".to_string(),
|
"req-1".to_string(),
|
||||||
Some("user-1".to_string()),
|
Some("user-1".to_string()),
|
||||||
@@ -1290,6 +1376,26 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.expect("candidate should build");
|
.expect("candidate should build");
|
||||||
|
skipped_candidate.skip_reason = Some("provider_request_body_build_failed".to_string());
|
||||||
|
skipped_candidate.extra_data = Some(json!({
|
||||||
|
"failure_diagnostic": {
|
||||||
|
"kind": "request_body_build",
|
||||||
|
"path": "$.reasoning.summary",
|
||||||
|
"message": "invalid reasoning summary",
|
||||||
|
"safe_to_show": true
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let mut request_metadata = Map::new();
|
||||||
|
insert_runtime_miss_candidate_usage_metadata(&mut request_metadata, &skipped_candidate);
|
||||||
|
assert_eq!(
|
||||||
|
request_metadata["routing_candidate_skip_reason"],
|
||||||
|
"provider_request_body_build_failed"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
request_metadata["routing_failure_diagnostic"]["path"],
|
||||||
|
"$.reasoning.summary"
|
||||||
|
);
|
||||||
|
|
||||||
assert!(!request_candidate_represents_provider_execution(
|
assert!(!request_candidate_represents_provider_execution(
|
||||||
&skipped_candidate
|
&skipped_candidate
|
||||||
@@ -1307,6 +1413,11 @@ mod tests {
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
assert!(select_last_runtime_miss_executed_candidate(&contexts).is_none());
|
assert!(select_last_runtime_miss_executed_candidate(&contexts).is_none());
|
||||||
|
assert_eq!(
|
||||||
|
select_last_runtime_miss_routing_candidate(&contexts)
|
||||||
|
.map(|candidate| candidate.candidate.id.as_str()),
|
||||||
|
Some("cand-skipped")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -236,6 +236,15 @@ async fn admin_monitoring_trace_request_falls_back_to_usage_routing_snapshot() {
|
|||||||
usage.provider_api_key_id = Some("provider-key-1".to_string());
|
usage.provider_api_key_id = Some("provider-key-1".to_string());
|
||||||
usage.error_message = Some("no local stream plans".to_string());
|
usage.error_message = Some("no local stream plans".to_string());
|
||||||
usage.response_time_ms = Some(45);
|
usage.response_time_ms = Some(45);
|
||||||
|
usage.request_metadata = Some(json!({
|
||||||
|
"routing_candidate_skip_reason": "provider_request_body_build_failed",
|
||||||
|
"routing_failure_diagnostic": {
|
||||||
|
"kind": "request_body_build",
|
||||||
|
"path": "$.reasoning.summary",
|
||||||
|
"message": "上游请求体语义校验失败",
|
||||||
|
"safe_to_show": true
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![usage]));
|
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![usage]));
|
||||||
let data_state =
|
let data_state =
|
||||||
@@ -267,6 +276,10 @@ async fn admin_monitoring_trace_request_falls_back_to_usage_routing_snapshot() {
|
|||||||
assert_eq!(payload["final_status"], json!("failed"));
|
assert_eq!(payload["final_status"], json!("failed"));
|
||||||
assert_eq!(payload["candidates"][0]["id"], json!("routing-cand-1"));
|
assert_eq!(payload["candidates"][0]["id"], json!("routing-cand-1"));
|
||||||
assert_eq!(payload["candidates"][0]["status"], json!("failed"));
|
assert_eq!(payload["candidates"][0]["status"], json!("failed"));
|
||||||
|
assert_eq!(
|
||||||
|
payload["candidates"][0]["skip_reason"],
|
||||||
|
json!("provider_request_body_build_failed")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["candidates"][0]["error_type"],
|
payload["candidates"][0]["error_type"],
|
||||||
json!("no_local_stream_plans")
|
json!("no_local_stream_plans")
|
||||||
@@ -279,6 +292,10 @@ async fn admin_monitoring_trace_request_falls_back_to_usage_routing_snapshot() {
|
|||||||
payload["candidates"][0]["extra_data"]["execution_path"],
|
payload["candidates"][0]["extra_data"]["execution_path"],
|
||||||
json!("local_execution_runtime_miss")
|
json!("local_execution_runtime_miss")
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payload["candidates"][0]["extra_data"]["failure_diagnostic"]["path"],
|
||||||
|
json!("$.reasoning.summary")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ fn build_admin_monitoring_usage_routing_snapshot_trace(
|
|||||||
endpoint_id: usage.provider_endpoint_id.clone(),
|
endpoint_id: usage.provider_endpoint_id.clone(),
|
||||||
key_id: usage.provider_api_key_id.clone(),
|
key_id: usage.provider_api_key_id.clone(),
|
||||||
status,
|
status,
|
||||||
skip_reason: None,
|
skip_reason: usage.routing_candidate_skip_reason().map(ToOwned::to_owned),
|
||||||
is_cached: false,
|
is_cached: false,
|
||||||
status_code: usage.status_code,
|
status_code: usage.status_code,
|
||||||
error_type: usage
|
error_type: usage
|
||||||
@@ -356,6 +356,9 @@ fn build_admin_monitoring_usage_routing_snapshot_extra_data(
|
|||||||
if let Some(candidate_index) = usage.routing_candidate_index() {
|
if let Some(candidate_index) = usage.routing_candidate_index() {
|
||||||
object.insert("candidate_index".to_string(), json!(candidate_index));
|
object.insert("candidate_index".to_string(), json!(candidate_index));
|
||||||
}
|
}
|
||||||
|
if let Some(diagnostic) = usage.routing_failure_diagnostic() {
|
||||||
|
object.insert("failure_diagnostic".to_string(), diagnostic.clone());
|
||||||
|
}
|
||||||
Some(Value::Object(object))
|
Some(Value::Object(object))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -377,6 +377,7 @@ pub(crate) fn admin_proxy_local_requires_buffered_body(
|
|||||||
| (Some("users_manage"), http::Method::PATCH, Some("lock_user_api_key"))
|
| (Some("users_manage"), http::Method::PATCH, Some("lock_user_api_key"))
|
||||||
| (Some("pool_manage"), http::Method::POST, Some("batch_import_keys"))
|
| (Some("pool_manage"), http::Method::POST, Some("batch_import_keys"))
|
||||||
| (Some("pool_manage"), http::Method::POST, Some("batch_action_keys"))
|
| (Some("pool_manage"), http::Method::POST, Some("batch_action_keys"))
|
||||||
|
| (Some("pool_manage"), http::Method::PATCH, Some("batch_update_keys"))
|
||||||
| (Some("pool_manage"), http::Method::POST, Some("resolve_selection"))
|
| (Some("pool_manage"), http::Method::POST, Some("resolve_selection"))
|
||||||
| (Some("usage_manage"), http::Method::POST, Some("replay"))
|
| (Some("usage_manage"), http::Method::POST, Some("replay"))
|
||||||
| (Some("wallets_manage"), http::Method::POST, Some("adjust_balance"))
|
| (Some("wallets_manage"), http::Method::POST, Some("adjust_balance"))
|
||||||
|
|||||||
@@ -3615,19 +3615,26 @@ async fn gateway_batch_updates_shared_pool_key_configuration() {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![first_key, second_key],
|
vec![first_key, second_key],
|
||||||
));
|
));
|
||||||
let state = AppState::new()
|
let gateway = build_router_with_state(
|
||||||
.expect("gateway should build")
|
AppState::new()
|
||||||
.with_data_state_for_tests(
|
.expect("gateway should build")
|
||||||
GatewayDataState::with_provider_catalog_repository_for_tests(Arc::clone(
|
.with_data_state_for_tests(
|
||||||
&provider_catalog_repository,
|
GatewayDataState::with_provider_catalog_repository_for_tests(Arc::clone(
|
||||||
)),
|
&provider_catalog_repository,
|
||||||
);
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||||
|
|
||||||
let response = local_admin_pool_response(
|
let response = reqwest::Client::new()
|
||||||
&state,
|
.patch(format!(
|
||||||
http::Method::PATCH,
|
"{gateway_url}/api/admin/pool/provider-openai/keys/batch-update"
|
||||||
"/api/admin/pool/provider-openai/keys/batch-update",
|
))
|
||||||
Some(json!({
|
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||||
|
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||||
|
.json(&json!({
|
||||||
"key_ids": ["key-openai-b", "key-openai-a", "key-openai-a"],
|
"key_ids": ["key-openai-b", "key-openai-a", "key-openai-a"],
|
||||||
"patch": {
|
"patch": {
|
||||||
"api_formats": ["openai:responses"],
|
"api_formats": ["openai:responses"],
|
||||||
@@ -3638,17 +3645,13 @@ async fn gateway_batch_updates_shared_pool_key_configuration() {
|
|||||||
"locked_models": [],
|
"locked_models": [],
|
||||||
"note": null
|
"note": null
|
||||||
}
|
}
|
||||||
})),
|
}))
|
||||||
)
|
.send()
|
||||||
.await;
|
.await
|
||||||
|
.expect("request should succeed");
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
let payload: serde_json::Value = serde_json::from_slice(
|
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||||
&to_bytes(response.into_body(), usize::MAX)
|
|
||||||
.await
|
|
||||||
.expect("body should read"),
|
|
||||||
)
|
|
||||||
.expect("json body should parse");
|
|
||||||
assert_eq!(payload["affected"], json!(2));
|
assert_eq!(payload["affected"], json!(2));
|
||||||
assert_eq!(payload["model_sync"], serde_json::Value::Null);
|
assert_eq!(payload["model_sync"], serde_json::Value::Null);
|
||||||
|
|
||||||
@@ -3670,6 +3673,8 @@ async fn gateway_batch_updates_shared_pool_key_configuration() {
|
|||||||
assert_eq!(key.locked_models, None);
|
assert_eq!(key.locked_models, None);
|
||||||
assert_eq!(key.note, None);
|
assert_eq!(key.note, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gateway_handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1942,14 +1942,14 @@ async fn gateway_records_failed_usage_when_all_local_claude_cli_candidates_are_s
|
|||||||
stored_usage.user_id.as_deref(),
|
stored_usage.user_id.as_deref(),
|
||||||
Some("user-claude-cli-usage-local-miss-1")
|
Some("user-claude-cli-usage-local-miss-1")
|
||||||
);
|
);
|
||||||
assert_eq!(stored_usage.provider_name, "unknown");
|
assert_eq!(stored_usage.provider_name, "RightCode");
|
||||||
assert_eq!(stored_usage.model, "gpt-5.4");
|
assert_eq!(stored_usage.model, "gpt-5.4");
|
||||||
assert_eq!(stored_usage.api_format.as_deref(), Some("claude:messages"));
|
assert_eq!(stored_usage.api_format.as_deref(), Some("claude:messages"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stored_usage.endpoint_api_format.as_deref(),
|
stored_usage.endpoint_api_format.as_deref(),
|
||||||
Some("claude:messages")
|
Some("openai:responses")
|
||||||
);
|
);
|
||||||
assert_eq!(stored_usage.routing_key_name(), None);
|
assert_eq!(stored_usage.routing_key_name(), Some("codex"));
|
||||||
assert_eq!(stored_usage.routing_planner_kind(), Some("claude_cli_sync"));
|
assert_eq!(stored_usage.routing_planner_kind(), Some("claude_cli_sync"));
|
||||||
assert_eq!(stored_usage.routing_route_family(), Some("claude"));
|
assert_eq!(stored_usage.routing_route_family(), Some("claude"));
|
||||||
assert_eq!(stored_usage.routing_route_kind(), Some("messages"));
|
assert_eq!(stored_usage.routing_route_kind(), Some("messages"));
|
||||||
@@ -2011,7 +2011,14 @@ async fn gateway_records_failed_usage_when_all_local_claude_cli_candidates_are_s
|
|||||||
stored_candidates[0].skip_reason.as_deref(),
|
stored_candidates[0].skip_reason.as_deref(),
|
||||||
Some("format_conversion_disabled")
|
Some("format_conversion_disabled")
|
||||||
);
|
);
|
||||||
assert_eq!(stored_usage.routing_candidate_id(), None);
|
assert_eq!(
|
||||||
|
stored_usage.routing_candidate_id(),
|
||||||
|
Some(stored_candidates[0].id.as_str())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stored_usage.routing_candidate_skip_reason(),
|
||||||
|
Some("format_conversion_disabled")
|
||||||
|
);
|
||||||
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
||||||
|
|
||||||
gateway_handle.abort();
|
gateway_handle.abort();
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ pub use crate::formats::openai::{
|
|||||||
validate_openai_provider_request_contract, OpenAiProviderRequestContractViolation,
|
validate_openai_provider_request_contract, OpenAiProviderRequestContractViolation,
|
||||||
OpenAiProviderRequestFinalization,
|
OpenAiProviderRequestFinalization,
|
||||||
},
|
},
|
||||||
|
responses::{
|
||||||
|
openai_responses_synthetic_reasoning_item_id,
|
||||||
|
strip_incompatible_openai_responses_reasoning_items,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
pub use crate::formats::shared::error_body::{
|
pub use crate::formats::shared::error_body::{
|
||||||
build_core_error_body_for_client_format, is_core_error_finalize_kind, LocalCoreSyncErrorKind,
|
build_core_error_body_for_client_format, is_core_error_finalize_kind, LocalCoreSyncErrorKind,
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ use serde_json::{json, Value};
|
|||||||
|
|
||||||
use crate::formats::{
|
use crate::formats::{
|
||||||
context::FormatContext,
|
context::FormatContext,
|
||||||
openai::responses::response::ensure_modern_openai_responses_response_fields, registry,
|
openai::responses::{
|
||||||
|
openai_responses_synthetic_reasoning_item_id,
|
||||||
|
response::ensure_modern_openai_responses_response_fields,
|
||||||
|
},
|
||||||
|
registry,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -203,7 +207,7 @@ pub fn build_openai_responses_response_with_content(
|
|||||||
}
|
}
|
||||||
output.push(json!({
|
output.push(json!({
|
||||||
"type": "reasoning",
|
"type": "reasoning",
|
||||||
"id": format!("{response_id}_rs_{index}"),
|
"id": openai_responses_synthetic_reasoning_item_id(response_id, index),
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"summary": [{
|
"summary": [{
|
||||||
"type": "summary_text",
|
"type": "summary_text",
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
|
|
||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
|
|
||||||
use crate::formats::openai::responses::response::{
|
use crate::formats::openai::responses::{
|
||||||
ensure_modern_openai_responses_response_fields, openai_responses_current_timestamp,
|
openai_responses_synthetic_reasoning_item_id,
|
||||||
|
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::response::build_generated_tool_call_id;
|
||||||
use crate::formats::shared::sse::{encode_done_sse, encode_json_sse};
|
use crate::formats::shared::sse::{encode_done_sse, encode_json_sse};
|
||||||
@@ -2227,7 +2230,7 @@ impl OpenAIResponsesClientEmitter {
|
|||||||
fn reasoning_item_id(&self) -> String {
|
fn reasoning_item_id(&self) -> String {
|
||||||
self.reasoning_item_id
|
self.reasoning_item_id
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| format!("{}_rs_0", self.response_id()))
|
.unwrap_or_else(|| openai_responses_synthetic_reasoning_item_id(self.response_id(), 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tool_call_item_id(&self, index: usize) -> String {
|
fn tool_call_item_id(&self, index: usize) -> String {
|
||||||
@@ -2247,7 +2250,10 @@ impl OpenAIResponsesClientEmitter {
|
|||||||
|
|
||||||
fn ensure_reasoning_item_id(&mut self) -> String {
|
fn ensure_reasoning_item_id(&mut self) -> String {
|
||||||
if self.reasoning_item_id.is_none() {
|
if self.reasoning_item_id.is_none() {
|
||||||
self.reasoning_item_id = Some(format!("{}_rs_0", self.response_id()));
|
self.reasoning_item_id = Some(openai_responses_synthetic_reasoning_item_id(
|
||||||
|
self.response_id(),
|
||||||
|
0,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
self.reasoning_item_id()
|
self.reasoning_item_id()
|
||||||
}
|
}
|
||||||
@@ -5460,7 +5466,8 @@ mod tests {
|
|||||||
assert!(sse.contains("event: response.reasoning_summary_text.delta\n"));
|
assert!(sse.contains("event: response.reasoning_summary_text.delta\n"));
|
||||||
assert!(sse.contains("event: response.reasoning_summary_text.done\n"));
|
assert!(sse.contains("event: response.reasoning_summary_text.done\n"));
|
||||||
assert!(sse.contains("event: response.reasoning_summary_part.done\n"));
|
assert!(sse.contains("event: response.reasoning_summary_part.done\n"));
|
||||||
assert!(sse.contains("\"item_id\":\"resp_456_rs_0\""));
|
let reasoning_item_id = openai_responses_synthetic_reasoning_item_id("resp_456", 0);
|
||||||
|
assert!(sse.contains(&format!("\"item_id\":\"{reasoning_item_id}\"")));
|
||||||
assert!(sse.contains("\"type\":\"reasoning\""));
|
assert!(sse.contains("\"type\":\"reasoning\""));
|
||||||
assert_eq!(response_sequence_numbers(&sse), (1..=9).collect::<Vec<_>>());
|
assert_eq!(response_sequence_numbers(&sse), (1..=9).collect::<Vec<_>>());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ pub fn finalize_openai_provider_request_with_codex_model_capabilities(
|
|||||||
body,
|
body,
|
||||||
finalization.provider_api_format,
|
finalization.provider_api_format,
|
||||||
);
|
);
|
||||||
|
super::responses::strip_incompatible_openai_responses_reasoning_items(
|
||||||
|
body,
|
||||||
|
finalization.provider_api_format,
|
||||||
|
);
|
||||||
crate::enforce_request_body_stream_field(
|
crate::enforce_request_body_stream_field(
|
||||||
body,
|
body,
|
||||||
finalization.provider_api_format,
|
finalization.provider_api_format,
|
||||||
@@ -296,6 +300,42 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finalization_strips_non_replayable_responses_reasoning_history() {
|
||||||
|
let mut body = json!({
|
||||||
|
"model": "gpt-5.4",
|
||||||
|
"input": [
|
||||||
|
{"type": "reasoning", "id": "rs_provider_123", "summary": []},
|
||||||
|
{
|
||||||
|
"type": "reasoning",
|
||||||
|
"id": "item_72d3bd8d367d01977ace23f1",
|
||||||
|
"summary": []
|
||||||
|
},
|
||||||
|
{"type": "message", "role": "user", "content": "continue"}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
finalize_openai_provider_request(
|
||||||
|
&mut body,
|
||||||
|
OpenAiProviderRequestFinalization {
|
||||||
|
source_api_format: "openai:responses",
|
||||||
|
provider_api_format: "openai:responses",
|
||||||
|
provider_type: "openai",
|
||||||
|
provider_model: "gpt-5.4",
|
||||||
|
source_model: "gpt-5.4",
|
||||||
|
body_rules: None,
|
||||||
|
upstream_is_stream: false,
|
||||||
|
require_body_stream_field: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("foreign reasoning history should be sanitized before validation");
|
||||||
|
|
||||||
|
let input = body["input"].as_array().expect("input array");
|
||||||
|
assert_eq!(input.len(), 2);
|
||||||
|
assert_eq!(input[0]["id"], "rs_provider_123");
|
||||||
|
assert_eq!(input[1]["type"], "message");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn non_responses_sources_receive_codex_responses_reasoning_defaults() {
|
fn non_responses_sources_receive_codex_responses_reasoning_defaults() {
|
||||||
for source_api_format in ["openai:chat", "claude:messages", "gemini:generate_content"] {
|
for source_api_format in ["openai:chat", "claude:messages", "gemini:generate_content"] {
|
||||||
|
|||||||
@@ -8,6 +8,68 @@ pub mod spec;
|
|||||||
pub mod stream;
|
pub mod stream;
|
||||||
|
|
||||||
const TOOL_ERROR_PREFIX: &str = "[tool error]";
|
const TOOL_ERROR_PREFIX: &str = "[tool error]";
|
||||||
|
const AETHER_REASONING_ITEM_ID_PREFIX: &str = "rs_aether_";
|
||||||
|
|
||||||
|
/// Builds a stable, wire-compatible ID for a reasoning item synthesized by Aether.
|
||||||
|
///
|
||||||
|
/// The marker lets the outbound request sanitizer distinguish synthetic summaries from
|
||||||
|
/// provider-backed reasoning items. Synthetic items without encrypted reasoning state are useful
|
||||||
|
/// in client responses, but cannot be replayed as provider-owned reasoning state.
|
||||||
|
pub fn openai_responses_synthetic_reasoning_item_id(
|
||||||
|
response_id: &str,
|
||||||
|
output_index: usize,
|
||||||
|
) -> String {
|
||||||
|
let seed = format!("{response_id}:{output_index}");
|
||||||
|
format!(
|
||||||
|
"{AETHER_REASONING_ITEM_ID_PREFIX}{}",
|
||||||
|
uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, seed.as_bytes()).simple()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes reasoning history items that cannot be replayed against an OpenAI Responses backend.
|
||||||
|
///
|
||||||
|
/// Reasoning IDs are opaque provider references and must never be repaired by changing their
|
||||||
|
/// prefix. Foreign IDs (for example `item_...`) are therefore removed. Aether-synthesized
|
||||||
|
/// reasoning summaries are also removed unless they carry encrypted reasoning state that can be
|
||||||
|
/// replayed statelessly.
|
||||||
|
pub fn strip_incompatible_openai_responses_reasoning_items(
|
||||||
|
body: &mut Value,
|
||||||
|
provider_api_format: &str,
|
||||||
|
) -> usize {
|
||||||
|
if !aether_ai_formats::is_openai_responses_family_format(provider_api_format) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let Some(items) = body.get_mut("input").and_then(Value::as_array_mut) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let original_len = items.len();
|
||||||
|
items.retain(openai_responses_reasoning_item_is_replayable);
|
||||||
|
original_len.saturating_sub(items.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openai_responses_reasoning_item_is_replayable(item: &Value) -> bool {
|
||||||
|
let Some(object) = item.as_object() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if object.get("type").and_then(Value::as_str) != Some("reasoning") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let Some(id) = object
|
||||||
|
.get("id")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|id| id.starts_with("rs"))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !id.starts_with(AETHER_REASONING_ITEM_ID_PREFIX) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
object
|
||||||
|
.get("encrypted_content")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.is_some_and(|encrypted_content| !encrypted_content.trim().is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
/// Semantic operation carried by an OpenAI Responses request that asks the
|
/// Semantic operation carried by an OpenAI Responses request that asks the
|
||||||
/// service to compact a thread. The request still uses the Responses wire
|
/// service to compact a thread. The request still uses the Responses wire
|
||||||
@@ -57,7 +119,10 @@ fn encode_tool_result_error(output: Value, is_error: bool) -> Value {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::{openai_responses_request_operation, OPENAI_RESPONSES_OPERATION_COMPACT};
|
use super::{
|
||||||
|
openai_responses_request_operation, openai_responses_synthetic_reasoning_item_id,
|
||||||
|
strip_incompatible_openai_responses_reasoning_items, OPENAI_RESPONSES_OPERATION_COMPACT,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolves_compaction_trigger_as_compact_operation_on_responses_transport() {
|
fn resolves_compaction_trigger_as_compact_operation_on_responses_transport() {
|
||||||
@@ -89,4 +154,60 @@ mod tests {
|
|||||||
Some(OPENAI_RESPONSES_OPERATION_COMPACT)
|
Some(OPENAI_RESPONSES_OPERATION_COMPACT)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synthetic_reasoning_item_ids_are_stable_and_wire_compatible() {
|
||||||
|
let first = openai_responses_synthetic_reasoning_item_id("resp_123", 0);
|
||||||
|
let second = openai_responses_synthetic_reasoning_item_id("resp_123", 0);
|
||||||
|
let other = openai_responses_synthetic_reasoning_item_id("resp_123", 1);
|
||||||
|
|
||||||
|
assert!(first.starts_with("rs_aether_"));
|
||||||
|
assert_eq!(first, second);
|
||||||
|
assert_ne!(first, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strips_foreign_and_non_replayable_synthetic_reasoning_items() {
|
||||||
|
let portable_synthetic = openai_responses_synthetic_reasoning_item_id("resp_123", 1);
|
||||||
|
let local_synthetic = openai_responses_synthetic_reasoning_item_id("resp_123", 2);
|
||||||
|
let mut body = json!({
|
||||||
|
"input": [
|
||||||
|
{"type": "reasoning", "id": "rs_provider_123", "summary": []},
|
||||||
|
{"type": "reasoning", "id": "item_72d3bd8d367d01977ace23f1", "summary": []},
|
||||||
|
{"type": "reasoning", "id": "resp_123_rs_0", "summary": []},
|
||||||
|
{"type": "reasoning", "summary": []},
|
||||||
|
{
|
||||||
|
"type": "reasoning",
|
||||||
|
"id": portable_synthetic,
|
||||||
|
"summary": [],
|
||||||
|
"encrypted_content": "opaque"
|
||||||
|
},
|
||||||
|
{"type": "reasoning", "id": local_synthetic, "summary": []},
|
||||||
|
{"type": "message", "id": "item_message_123", "role": "user", "content": "hi"}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
strip_incompatible_openai_responses_reasoning_items(&mut body, "openai:responses"),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
let input = body["input"].as_array().expect("input array");
|
||||||
|
assert_eq!(input.len(), 3);
|
||||||
|
assert_eq!(input[0]["id"], "rs_provider_123");
|
||||||
|
assert_eq!(input[1]["encrypted_content"], "opaque");
|
||||||
|
assert_eq!(input[2]["id"], "item_message_123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reasoning_item_sanitizer_is_scoped_to_responses_targets() {
|
||||||
|
let mut body = json!({
|
||||||
|
"input": [{"type": "reasoning", "id": "item_foreign", "summary": []}]
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
strip_incompatible_openai_responses_reasoning_items(&mut body, "openai:chat"),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(body["input"].as_array().map(Vec::len), Some(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ use std::{
|
|||||||
|
|
||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
|
|
||||||
use super::{encode_tool_result_error, history::record_converted_response_history};
|
use super::{
|
||||||
|
encode_tool_result_error, history::record_converted_response_history,
|
||||||
|
openai_responses_synthetic_reasoning_item_id,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
formats::context::FormatContext,
|
formats::context::FormatContext,
|
||||||
@@ -201,7 +204,10 @@ pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value, compact: bo
|
|||||||
item.insert("type".to_string(), Value::String("reasoning".to_string()));
|
item.insert("type".to_string(), Value::String("reasoning".to_string()));
|
||||||
item.insert(
|
item.insert(
|
||||||
"id".to_string(),
|
"id".to_string(),
|
||||||
Value::String(format!("{}_rs_{}", response_id, output.len())),
|
Value::String(openai_responses_synthetic_reasoning_item_id(
|
||||||
|
&response_id,
|
||||||
|
output.len(),
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
item.insert("status".to_string(), Value::String("completed".to_string()));
|
item.insert("status".to_string(), Value::String("completed".to_string()));
|
||||||
if let Some(encrypted_content) = encrypted_content {
|
if let Some(encrypted_content) = encrypted_content {
|
||||||
@@ -715,6 +721,9 @@ mod tests {
|
|||||||
|
|
||||||
let rebuilt = to_raw(&canonical, &json!({}), false);
|
let rebuilt = to_raw(&canonical, &json!({}), false);
|
||||||
assert_eq!(rebuilt["output"][0]["type"], "reasoning");
|
assert_eq!(rebuilt["output"][0]["type"], "reasoning");
|
||||||
|
assert!(rebuilt["output"][0]["id"]
|
||||||
|
.as_str()
|
||||||
|
.is_some_and(|id| id.starts_with("rs")));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rebuilt["output"][0]["encrypted_content"],
|
rebuilt["output"][0]["encrypted_content"],
|
||||||
json!("openai-opaque")
|
json!("openai-opaque")
|
||||||
|
|||||||
@@ -153,6 +153,10 @@ pub fn build_standard_request_body_with_model_directives_and_request_headers(
|
|||||||
&mut provider_request_body,
|
&mut provider_request_body,
|
||||||
provider_api_format,
|
provider_api_format,
|
||||||
);
|
);
|
||||||
|
crate::formats::openai::responses::strip_incompatible_openai_responses_reasoning_items(
|
||||||
|
&mut provider_request_body,
|
||||||
|
provider_api_format,
|
||||||
|
);
|
||||||
strip_openai_responses_input_content_cache_control(
|
strip_openai_responses_input_content_cache_control(
|
||||||
&mut provider_request_body,
|
&mut provider_request_body,
|
||||||
provider_api_format,
|
provider_api_format,
|
||||||
|
|||||||
@@ -303,6 +303,10 @@ pub fn build_local_openai_responses_request_body_with_model_directives(
|
|||||||
require_streaming,
|
require_streaming,
|
||||||
require_body_stream_field,
|
require_body_stream_field,
|
||||||
);
|
);
|
||||||
|
crate::formats::openai::responses::strip_incompatible_openai_responses_reasoning_items(
|
||||||
|
&mut provider_request_body,
|
||||||
|
"openai:responses",
|
||||||
|
);
|
||||||
Some(provider_request_body)
|
Some(provider_request_body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use aether_ai_formats::formats::conversion::response::{
|
|||||||
convert_openai_chat_response_to_openai_responses,
|
convert_openai_chat_response_to_openai_responses,
|
||||||
convert_openai_responses_response_to_openai_chat,
|
convert_openai_responses_response_to_openai_chat,
|
||||||
};
|
};
|
||||||
|
use aether_ai_formats::formats::openai::responses::openai_responses_synthetic_reasoning_item_id;
|
||||||
use aether_ai_formats::formats::openai::responses::response::ensure_modern_openai_responses_response_fields;
|
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::formats::registry::{convert_response, FormatContext, FormatError};
|
||||||
use aether_ai_formats::{
|
use aether_ai_formats::{
|
||||||
@@ -3185,8 +3186,9 @@ fn materialize_openai_responses_reasoning_item(
|
|||||||
let mut item = state.item;
|
let mut item = state.item;
|
||||||
item.entry("type".to_string())
|
item.entry("type".to_string())
|
||||||
.or_insert_with(|| Value::String("reasoning".to_string()));
|
.or_insert_with(|| Value::String("reasoning".to_string()));
|
||||||
item.entry("id".to_string())
|
item.entry("id".to_string()).or_insert_with(|| {
|
||||||
.or_insert_with(|| Value::String(format!("{response_id}_rs_0")));
|
Value::String(openai_responses_synthetic_reasoning_item_id(response_id, 0))
|
||||||
|
});
|
||||||
item.entry("status".to_string())
|
item.entry("status".to_string())
|
||||||
.or_insert_with(|| Value::String("completed".to_string()));
|
.or_insert_with(|| Value::String("completed".to_string()));
|
||||||
if !state.summary_text.is_empty() {
|
if !state.summary_text.is_empty() {
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ pub use formats::openai::responses::request::{
|
|||||||
validate_openai_responses_request_contract, OpenAiResponsesRequestContractViolation,
|
validate_openai_responses_request_contract, OpenAiResponsesRequestContractViolation,
|
||||||
};
|
};
|
||||||
pub use formats::openai::responses::{
|
pub use formats::openai::responses::{
|
||||||
openai_responses_request_operation, OPENAI_RESPONSES_OPERATION_COMPACT,
|
openai_responses_request_operation, openai_responses_synthetic_reasoning_item_id,
|
||||||
|
strip_incompatible_openai_responses_reasoning_items, OPENAI_RESPONSES_OPERATION_COMPACT,
|
||||||
};
|
};
|
||||||
pub use formats::registry::{
|
pub use formats::registry::{
|
||||||
build_stream_transcoder, convert_request, convert_request_pure,
|
build_stream_transcoder, convert_request, convert_request_pure,
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ pub use report_context::{
|
|||||||
AiExecutionReportContextParts, AiRequestOrigin,
|
AiExecutionReportContextParts, AiRequestOrigin,
|
||||||
};
|
};
|
||||||
pub use request_body_diagnostics::{
|
pub use request_body_diagnostics::{
|
||||||
request_body_build_failure_extra_data, request_conversion_failure_extra_data,
|
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
|
||||||
same_format_provider_request_body_failure_extra_data,
|
request_conversion_failure_extra_data, same_format_provider_request_body_failure_extra_data,
|
||||||
};
|
};
|
||||||
pub use runtime_miss::{
|
pub use runtime_miss::{
|
||||||
apply_ai_runtime_candidate_evaluation_progress,
|
apply_ai_runtime_candidate_evaluation_progress,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use serde_json::Value;
|
|||||||
|
|
||||||
use aether_ai_formats::api::{
|
use aether_ai_formats::api::{
|
||||||
is_claude_messages_shaped_body_on_openai_chat_endpoint, is_openai_responses_family_format,
|
is_claude_messages_shaped_body_on_openai_chat_endpoint, is_openai_responses_family_format,
|
||||||
normalize_api_format_alias,
|
normalize_api_format_alias, OpenAiProviderRequestContractViolation,
|
||||||
};
|
};
|
||||||
use aether_ai_formats::{convert_request_pure_with_context, FormatContext, FormatError};
|
use aether_ai_formats::{convert_request_pure_with_context, FormatContext, FormatError};
|
||||||
|
|
||||||
@@ -51,6 +51,42 @@ pub fn request_conversion_failure_extra_data(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn openai_provider_request_contract_failure_extra_data(
|
||||||
|
violation: &OpenAiProviderRequestContractViolation,
|
||||||
|
client_api_format: &str,
|
||||||
|
provider_api_format: &str,
|
||||||
|
source: impl Into<String>,
|
||||||
|
) -> Value {
|
||||||
|
let (field, reason) = openai_provider_request_contract_violation_parts(violation);
|
||||||
|
CandidateFailureDiagnostic::new(
|
||||||
|
CandidateFailureDiagnosticKind::RequestBodyBuild,
|
||||||
|
field_to_json_path(field),
|
||||||
|
format!("上游请求体语义校验失败:{reason}"),
|
||||||
|
)
|
||||||
|
.formats(client_api_format, provider_api_format)
|
||||||
|
.source(source)
|
||||||
|
.to_extra_data()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openai_provider_request_contract_violation_parts(
|
||||||
|
violation: &OpenAiProviderRequestContractViolation,
|
||||||
|
) -> (&str, &str) {
|
||||||
|
match violation {
|
||||||
|
OpenAiProviderRequestContractViolation::CodexCompact(violation) => {
|
||||||
|
(violation.field, violation.reason)
|
||||||
|
}
|
||||||
|
OpenAiProviderRequestContractViolation::Responses(violation) => {
|
||||||
|
(violation.field, violation.reason)
|
||||||
|
}
|
||||||
|
OpenAiProviderRequestContractViolation::PromptCache(violation) => {
|
||||||
|
(violation.field.as_str(), violation.reason.as_str())
|
||||||
|
}
|
||||||
|
OpenAiProviderRequestContractViolation::Reasoning(violation) => {
|
||||||
|
(violation.field.as_str(), violation.reason.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn same_format_provider_request_body_failure_extra_data(
|
pub fn same_format_provider_request_body_failure_extra_data(
|
||||||
body_json: &Value,
|
body_json: &Value,
|
||||||
provider_api_format: &str,
|
provider_api_format: &str,
|
||||||
@@ -798,7 +834,10 @@ fn request_body_build_source(client_api_format: &str, provider_api_format: &str)
|
|||||||
mod tests {
|
mod tests {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::{request_body_build_failure_extra_data, request_conversion_failure_extra_data};
|
use super::{
|
||||||
|
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
|
||||||
|
request_conversion_failure_extra_data,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn openai_chat_to_claude_recognizes_compatible_claude_native_tool_shape() {
|
fn openai_chat_to_claude_recognizes_compatible_claude_native_tool_shape() {
|
||||||
@@ -950,6 +989,40 @@ mod tests {
|
|||||||
.contains("字段 n"));
|
.contains("字段 n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn provider_contract_failure_preserves_finalization_field_and_reason() {
|
||||||
|
let violation = aether_ai_formats::OpenAiProviderRequestContractViolation::Responses(
|
||||||
|
aether_ai_formats::OpenAiResponsesRequestContractViolation {
|
||||||
|
field: "reasoning.summary",
|
||||||
|
reason: "OpenAI multi-agent requests do not support reasoning summaries",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let diagnostic = openai_provider_request_contract_failure_extra_data(
|
||||||
|
&violation,
|
||||||
|
"openai:responses",
|
||||||
|
"openai:responses",
|
||||||
|
"openai_responses_request_finalization",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
diagnostic["failure_diagnostic"]["kind"],
|
||||||
|
"request_body_build"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
diagnostic["failure_diagnostic"]["path"],
|
||||||
|
"$.reasoning.summary"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
diagnostic["failure_diagnostic"]["source"],
|
||||||
|
"openai_responses_request_finalization"
|
||||||
|
);
|
||||||
|
assert!(diagnostic["request_body_build_error"]["message"]
|
||||||
|
.as_str()
|
||||||
|
.expect("message")
|
||||||
|
.contains("do not support reasoning summaries"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn same_format_provider_reports_non_object_body() {
|
fn same_format_provider_reports_non_object_body() {
|
||||||
let diagnostic = super::same_format_provider_request_body_failure_extra_data(
|
let diagnostic = super::same_format_provider_request_body_failure_extra_data(
|
||||||
|
|||||||
@@ -36,4 +36,5 @@ pub use types::{
|
|||||||
UsageTimeSeriesQuery, UsageWriteRepository, PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY,
|
UsageTimeSeriesQuery, UsageWriteRepository, PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY,
|
||||||
PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY, PROVIDER_REASONING_EFFORT_METADATA_KEY,
|
PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY, PROVIDER_REASONING_EFFORT_METADATA_KEY,
|
||||||
PROVIDER_SERVICE_TIER_METADATA_KEY, REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
PROVIDER_SERVICE_TIER_METADATA_KEY, REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
||||||
|
ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY, ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ pub const REQUESTED_REASONING_EFFORT_METADATA_KEY: &str = "requested_reasoning_e
|
|||||||
pub const PROVIDER_SERVICE_TIER_METADATA_KEY: &str = "provider_service_tier";
|
pub const PROVIDER_SERVICE_TIER_METADATA_KEY: &str = "provider_service_tier";
|
||||||
pub const PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY: &str = "provider_actual_service_tier";
|
pub const PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY: &str = "provider_actual_service_tier";
|
||||||
pub const PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY: &str = "provider_cache_ttl_minutes";
|
pub const PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY: &str = "provider_cache_ttl_minutes";
|
||||||
|
pub const ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY: &str = "routing_candidate_skip_reason";
|
||||||
|
pub const ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY: &str = "routing_failure_diagnostic";
|
||||||
|
|
||||||
pub fn extract_provider_reasoning_effort_from_body(value: Option<&Value>) -> Option<String> {
|
pub fn extract_provider_reasoning_effort_from_body(value: Option<&Value>) -> Option<String> {
|
||||||
let object = value.and_then(Value::as_object)?;
|
let object = value.and_then(Value::as_object)?;
|
||||||
@@ -827,6 +829,16 @@ impl StoredRequestUsageAudit {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.or_else(|| self.request_metadata_string("local_execution_runtime_miss_reason"))
|
.or_else(|| self.request_metadata_string("local_execution_runtime_miss_reason"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn routing_candidate_skip_reason(&self) -> Option<&str> {
|
||||||
|
self.request_metadata_string(ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routing_failure_diagnostic(&self) -> Option<&Value> {
|
||||||
|
self.request_metadata_object()
|
||||||
|
.and_then(|metadata| metadata.get(ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY))
|
||||||
|
.filter(|value| value.is_object())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -2695,7 +2707,12 @@ mod tests {
|
|||||||
"response_body_ref": "blob://legacy-response",
|
"response_body_ref": "blob://legacy-response",
|
||||||
"client_response_body_ref": "blob://legacy-client-response",
|
"client_response_body_ref": "blob://legacy-client-response",
|
||||||
"candidate_id": "cand-legacy",
|
"candidate_id": "cand-legacy",
|
||||||
"key_name": "primary-legacy"
|
"key_name": "primary-legacy",
|
||||||
|
"routing_candidate_skip_reason": "provider_request_body_build_failed",
|
||||||
|
"routing_failure_diagnostic": {
|
||||||
|
"path": "$.reasoning.summary",
|
||||||
|
"message": "invalid reasoning summary"
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2727,6 +2744,16 @@ mod tests {
|
|||||||
usage.routing_local_execution_runtime_miss_reason(),
|
usage.routing_local_execution_runtime_miss_reason(),
|
||||||
Some("all_candidates_skipped")
|
Some("all_candidates_skipped")
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
usage.routing_candidate_skip_reason(),
|
||||||
|
Some("provider_request_body_build_failed")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
usage
|
||||||
|
.routing_failure_diagnostic()
|
||||||
|
.and_then(|diagnostic| diagnostic.get("path")),
|
||||||
|
Some(&json!("$.reasoning.summary"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -436,6 +436,21 @@ fn build_same_format_provider_request_body_inner(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let stripped_reasoning_items =
|
||||||
|
aether_ai_formats::strip_incompatible_openai_responses_reasoning_items(
|
||||||
|
&mut provider_request_body,
|
||||||
|
input.provider_api_format,
|
||||||
|
);
|
||||||
|
if stripped_reasoning_items > 0 {
|
||||||
|
record_compatibility_edit(
|
||||||
|
&mut compatibility_edits,
|
||||||
|
"input[].id",
|
||||||
|
SameFormatProviderCompatibilityEditAction::ProviderCompatibilityRewrite,
|
||||||
|
format!(
|
||||||
|
"stripped {stripped_reasoning_items} non-replayable OpenAI Responses reasoning item(s)"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
let provider_model = provider_request_body
|
let provider_model = provider_request_body
|
||||||
.get("model")
|
.get("model")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
@@ -2003,6 +2018,50 @@ mod tests {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_format_responses_body_strips_foreign_reasoning_ids_and_reports_the_edit() {
|
||||||
|
let request_body = json!({
|
||||||
|
"model": "gpt-5.4",
|
||||||
|
"input": [
|
||||||
|
{"type": "reasoning", "id": "rs_provider_123", "summary": []},
|
||||||
|
{
|
||||||
|
"type": "reasoning",
|
||||||
|
"id": "item_72d3bd8d367d01977ace23f1",
|
||||||
|
"summary": []
|
||||||
|
},
|
||||||
|
{"type": "message", "role": "user", "content": "continue"}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
let output = build_same_format_provider_request_body_with_compatibility_report(
|
||||||
|
SameFormatProviderRequestBodyInput {
|
||||||
|
body_json: &request_body,
|
||||||
|
mapped_model: "gpt-5.4",
|
||||||
|
client_api_format: "openai:responses",
|
||||||
|
provider_api_format: "openai:responses",
|
||||||
|
source_model: Some("gpt-5.4"),
|
||||||
|
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 Responses body should build");
|
||||||
|
|
||||||
|
let input = output.body["input"].as_array().expect("input array");
|
||||||
|
assert_eq!(input.len(), 2);
|
||||||
|
assert_eq!(input[0]["id"], "rs_provider_123");
|
||||||
|
assert_eq!(input[1]["type"], "message");
|
||||||
|
assert!(output.compatibility_edits.iter().any(|edit| {
|
||||||
|
edit.field == "input[].id"
|
||||||
|
&& edit.action
|
||||||
|
== SameFormatProviderCompatibilityEditAction::ProviderCompatibilityRewrite
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn same_format_stream_policy_wins_after_body_rules() {
|
fn same_format_stream_policy_wins_after_body_rules() {
|
||||||
let body_rules = json!([
|
let body_rules = json!([
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ use aether_data_contracts::repository::usage::{
|
|||||||
normalize_provider_service_tier, resolve_provider_cache_ttl_minutes, UsageBodyCaptureState,
|
normalize_provider_service_tier, resolve_provider_cache_ttl_minutes, UsageBodyCaptureState,
|
||||||
PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY, PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY,
|
PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY, PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY,
|
||||||
PROVIDER_REASONING_EFFORT_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
|
PROVIDER_REASONING_EFFORT_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
|
||||||
REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
REQUESTED_REASONING_EFFORT_METADATA_KEY, ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY,
|
||||||
|
ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY,
|
||||||
};
|
};
|
||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
|
|
||||||
@@ -376,6 +377,8 @@ fn copy_allowed_metadata_fields(source: &Map<String, Value>, target: &mut Map<St
|
|||||||
copy_non_null_value(source, target, "dimensions");
|
copy_non_null_value(source, target, "dimensions");
|
||||||
copy_non_null_value(source, target, "billing_rule_snapshot");
|
copy_non_null_value(source, target, "billing_rule_snapshot");
|
||||||
copy_non_null_value(source, target, "scheduling_audit");
|
copy_non_null_value(source, target, "scheduling_audit");
|
||||||
|
copy_non_empty_string(source, target, ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY);
|
||||||
|
copy_non_null_value(source, target, ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY);
|
||||||
copy_non_null_value(source, target, "tls_fingerprint");
|
copy_non_null_value(source, target, "tls_fingerprint");
|
||||||
copy_number(source, target, "rate_multiplier");
|
copy_number(source, target, "rate_multiplier");
|
||||||
copy_bool(source, target, "is_free_tier");
|
copy_bool(source, target, "is_free_tier");
|
||||||
@@ -432,6 +435,12 @@ fn move_allowed_metadata_fields(mut source: Map<String, Value>, target: &mut Map
|
|||||||
remove_non_null_value(&mut source, target, "dimensions");
|
remove_non_null_value(&mut source, target, "dimensions");
|
||||||
remove_non_null_value(&mut source, target, "billing_rule_snapshot");
|
remove_non_null_value(&mut source, target, "billing_rule_snapshot");
|
||||||
remove_non_null_value(&mut source, target, "scheduling_audit");
|
remove_non_null_value(&mut source, target, "scheduling_audit");
|
||||||
|
remove_non_empty_string(
|
||||||
|
&mut source,
|
||||||
|
target,
|
||||||
|
ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY,
|
||||||
|
);
|
||||||
|
remove_non_null_value(&mut source, target, ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY);
|
||||||
remove_non_null_value(&mut source, target, "tls_fingerprint");
|
remove_non_null_value(&mut source, target, "tls_fingerprint");
|
||||||
remove_number(&mut source, target, "rate_multiplier");
|
remove_number(&mut source, target, "rate_multiplier");
|
||||||
remove_bool(&mut source, target, "is_free_tier");
|
remove_bool(&mut source, target, "is_free_tier");
|
||||||
@@ -800,6 +809,13 @@ mod tests {
|
|||||||
"global_model_id": "global-model-1",
|
"global_model_id": "global-model-1",
|
||||||
"global_model_name": "gpt-5",
|
"global_model_name": "gpt-5",
|
||||||
"dimensions": {"total_input_context": 10},
|
"dimensions": {"total_input_context": 10},
|
||||||
|
"routing_candidate_skip_reason": "provider_request_body_build_failed",
|
||||||
|
"routing_failure_diagnostic": {
|
||||||
|
"kind": "request_body_build",
|
||||||
|
"path": "$.reasoning.summary",
|
||||||
|
"message": "invalid reasoning summary",
|
||||||
|
"safe_to_show": true
|
||||||
|
},
|
||||||
"rate_multiplier": 1.25,
|
"rate_multiplier": 1.25,
|
||||||
"is_free_tier": false,
|
"is_free_tier": false,
|
||||||
"input_price_per_1m": 3.0,
|
"input_price_per_1m": 3.0,
|
||||||
@@ -844,6 +860,13 @@ mod tests {
|
|||||||
"global_model_id": "global-model-1",
|
"global_model_id": "global-model-1",
|
||||||
"global_model_name": "gpt-5",
|
"global_model_name": "gpt-5",
|
||||||
"dimensions": {"total_input_context": 10},
|
"dimensions": {"total_input_context": 10},
|
||||||
|
"routing_candidate_skip_reason": "provider_request_body_build_failed",
|
||||||
|
"routing_failure_diagnostic": {
|
||||||
|
"kind": "request_body_build",
|
||||||
|
"path": "$.reasoning.summary",
|
||||||
|
"message": "invalid reasoning summary",
|
||||||
|
"safe_to_show": true
|
||||||
|
},
|
||||||
"rate_multiplier": 1.25,
|
"rate_multiplier": 1.25,
|
||||||
"is_free_tier": false,
|
"is_free_tier": false,
|
||||||
"input_price_per_1m": 3.0,
|
"input_price_per_1m": 3.0,
|
||||||
|
|||||||
Reference in New Issue
Block a user