2026-04-14 01:27:04 +08:00
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
|
|
|
|
|
use serde_json::Value;
|
|
|
|
|
|
|
|
|
|
use crate::ai_pipeline::planner::candidate_preparation::{
|
|
|
|
|
prepare_header_authenticated_candidate, OauthPreparationContext,
|
|
|
|
|
};
|
|
|
|
|
use crate::ai_pipeline::planner::common::force_upstream_streaming_for_provider;
|
|
|
|
|
use crate::ai_pipeline::planner::spec_metadata::local_standard_spec_metadata;
|
|
|
|
|
use crate::ai_pipeline::planner::standard::apply_codex_openai_cli_special_headers;
|
|
|
|
|
use crate::ai_pipeline::transport::apply_local_header_rules;
|
|
|
|
|
use crate::ai_pipeline::transport::auth::{
|
|
|
|
|
build_claude_passthrough_headers, build_openai_passthrough_headers, ensure_upstream_auth_header,
|
|
|
|
|
};
|
|
|
|
|
use crate::ai_pipeline::GatewayProviderTransportSnapshot;
|
|
|
|
|
use crate::AppState;
|
|
|
|
|
|
|
|
|
|
use super::payload::mark_skipped_local_standard_candidate;
|
|
|
|
|
use super::{LocalStandardCandidateAttempt, LocalStandardDecisionInput, LocalStandardSpec};
|
|
|
|
|
|
|
|
|
|
pub(crate) struct LocalStandardCandidatePayloadParts {
|
|
|
|
|
pub(super) auth_header: String,
|
|
|
|
|
pub(super) auth_value: String,
|
|
|
|
|
pub(super) mapped_model: String,
|
|
|
|
|
pub(super) provider_api_format: String,
|
|
|
|
|
pub(super) provider_request_body: Value,
|
|
|
|
|
pub(super) provider_request_headers: BTreeMap<String, String>,
|
|
|
|
|
pub(super) upstream_url: String,
|
|
|
|
|
pub(super) upstream_is_stream: bool,
|
|
|
|
|
pub(super) transport: GatewayProviderTransportSnapshot,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) async fn resolve_local_standard_candidate_payload_parts(
|
|
|
|
|
state: &AppState,
|
|
|
|
|
parts: &http::request::Parts,
|
|
|
|
|
trace_id: &str,
|
|
|
|
|
body_json: &serde_json::Value,
|
|
|
|
|
input: &LocalStandardDecisionInput,
|
|
|
|
|
attempt: &LocalStandardCandidateAttempt,
|
|
|
|
|
spec: LocalStandardSpec,
|
|
|
|
|
) -> Option<LocalStandardCandidatePayloadParts> {
|
|
|
|
|
let spec_metadata = local_standard_spec_metadata(spec);
|
|
|
|
|
let planner_state = crate::ai_pipeline::PlannerAppState::new(state);
|
|
|
|
|
let candidate = &attempt.eligible.candidate;
|
|
|
|
|
let transport = &attempt.eligible.transport;
|
|
|
|
|
let provider_api_format = attempt.eligible.provider_api_format.as_str();
|
|
|
|
|
let Some(conversion_kind) = crate::ai_pipeline::conversion::request_conversion_kind(
|
|
|
|
|
spec_metadata.api_format,
|
|
|
|
|
provider_api_format,
|
|
|
|
|
) else {
|
|
|
|
|
return None;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-14 11:50:52 +08:00
|
|
|
if let Some(skip_reason) =
|
|
|
|
|
crate::ai_pipeline::conversion::request_conversion_transport_unsupported_reason(
|
|
|
|
|
transport,
|
|
|
|
|
conversion_kind,
|
|
|
|
|
)
|
|
|
|
|
{
|
2026-04-14 01:27:04 +08:00
|
|
|
mark_skipped_local_standard_candidate(
|
|
|
|
|
state,
|
|
|
|
|
input,
|
|
|
|
|
trace_id,
|
|
|
|
|
candidate,
|
|
|
|
|
attempt.candidate_index,
|
|
|
|
|
&attempt.candidate_id,
|
2026-04-14 11:50:52 +08:00
|
|
|
skip_reason,
|
2026-04-14 01:27:04 +08:00
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let prepared_candidate = match prepare_header_authenticated_candidate(
|
|
|
|
|
planner_state,
|
|
|
|
|
transport,
|
|
|
|
|
candidate,
|
|
|
|
|
crate::ai_pipeline::conversion::request_conversion_direct_auth(transport, conversion_kind),
|
|
|
|
|
OauthPreparationContext {
|
|
|
|
|
trace_id,
|
|
|
|
|
api_format: provider_api_format,
|
|
|
|
|
operation: "standard_family_cross_format",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(prepared) => prepared,
|
|
|
|
|
Err(skip_reason) => {
|
|
|
|
|
mark_skipped_local_standard_candidate(
|
|
|
|
|
state,
|
|
|
|
|
input,
|
|
|
|
|
trace_id,
|
|
|
|
|
candidate,
|
|
|
|
|
attempt.candidate_index,
|
|
|
|
|
&attempt.candidate_id,
|
|
|
|
|
skip_reason,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let upstream_is_stream = spec_metadata.require_streaming
|
|
|
|
|
|| force_upstream_streaming_for_provider(
|
|
|
|
|
transport.provider.provider_type.as_str(),
|
|
|
|
|
provider_api_format,
|
|
|
|
|
);
|
|
|
|
|
let provider_request_body =
|
|
|
|
|
match crate::ai_pipeline::planner::standard::build_standard_request_body(
|
|
|
|
|
body_json,
|
|
|
|
|
spec_metadata.api_format,
|
|
|
|
|
&prepared_candidate.mapped_model,
|
|
|
|
|
transport.provider.provider_type.as_str(),
|
|
|
|
|
provider_api_format,
|
|
|
|
|
parts.uri.path(),
|
|
|
|
|
upstream_is_stream,
|
|
|
|
|
transport.endpoint.body_rules.as_ref(),
|
|
|
|
|
Some(input.auth_context.api_key_id.as_str()),
|
|
|
|
|
) {
|
|
|
|
|
Some(body) => body,
|
|
|
|
|
None => {
|
|
|
|
|
mark_skipped_local_standard_candidate(
|
|
|
|
|
state,
|
|
|
|
|
input,
|
|
|
|
|
trace_id,
|
|
|
|
|
candidate,
|
|
|
|
|
attempt.candidate_index,
|
|
|
|
|
&attempt.candidate_id,
|
|
|
|
|
"provider_request_body_missing",
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let upstream_url = match crate::ai_pipeline::planner::standard::build_standard_upstream_url(
|
|
|
|
|
parts,
|
|
|
|
|
transport,
|
|
|
|
|
&prepared_candidate.mapped_model,
|
|
|
|
|
provider_api_format,
|
|
|
|
|
upstream_is_stream,
|
|
|
|
|
) {
|
|
|
|
|
Some(url) => url,
|
|
|
|
|
None => {
|
|
|
|
|
mark_skipped_local_standard_candidate(
|
|
|
|
|
state,
|
|
|
|
|
input,
|
|
|
|
|
trace_id,
|
|
|
|
|
candidate,
|
|
|
|
|
attempt.candidate_index,
|
|
|
|
|
&attempt.candidate_id,
|
|
|
|
|
"upstream_url_missing",
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut provider_request_headers = if provider_api_format.starts_with("claude:") {
|
|
|
|
|
build_claude_passthrough_headers(
|
|
|
|
|
&parts.headers,
|
|
|
|
|
&prepared_candidate.auth_header,
|
|
|
|
|
&prepared_candidate.auth_value,
|
|
|
|
|
&BTreeMap::new(),
|
|
|
|
|
Some("application/json"),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
build_openai_passthrough_headers(
|
|
|
|
|
&parts.headers,
|
|
|
|
|
&prepared_candidate.auth_header,
|
|
|
|
|
&prepared_candidate.auth_value,
|
|
|
|
|
&BTreeMap::new(),
|
|
|
|
|
Some("application/json"),
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
if !apply_local_header_rules(
|
|
|
|
|
&mut provider_request_headers,
|
|
|
|
|
transport.endpoint.header_rules.as_ref(),
|
|
|
|
|
&[&prepared_candidate.auth_header, "content-type"],
|
|
|
|
|
&provider_request_body,
|
|
|
|
|
Some(body_json),
|
|
|
|
|
) {
|
|
|
|
|
mark_skipped_local_standard_candidate(
|
|
|
|
|
state,
|
|
|
|
|
input,
|
|
|
|
|
trace_id,
|
|
|
|
|
candidate,
|
|
|
|
|
attempt.candidate_index,
|
|
|
|
|
&attempt.candidate_id,
|
|
|
|
|
"transport_header_rules_apply_failed",
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
apply_codex_openai_cli_special_headers(
|
|
|
|
|
&mut provider_request_headers,
|
|
|
|
|
&provider_request_body,
|
|
|
|
|
&parts.headers,
|
|
|
|
|
transport.provider.provider_type.as_str(),
|
|
|
|
|
provider_api_format,
|
|
|
|
|
Some(trace_id),
|
|
|
|
|
transport.key.decrypted_auth_config.as_deref(),
|
|
|
|
|
);
|
|
|
|
|
ensure_upstream_auth_header(
|
|
|
|
|
&mut provider_request_headers,
|
|
|
|
|
&prepared_candidate.auth_header,
|
|
|
|
|
&prepared_candidate.auth_value,
|
|
|
|
|
);
|
|
|
|
|
if upstream_is_stream {
|
|
|
|
|
provider_request_headers
|
|
|
|
|
.entry("accept".to_string())
|
|
|
|
|
.or_insert_with(|| "text/event-stream".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Some(LocalStandardCandidatePayloadParts {
|
|
|
|
|
auth_header: prepared_candidate.auth_header,
|
|
|
|
|
auth_value: prepared_candidate.auth_value,
|
|
|
|
|
mapped_model: prepared_candidate.mapped_model,
|
|
|
|
|
provider_api_format: provider_api_format.to_string(),
|
|
|
|
|
provider_request_body,
|
|
|
|
|
provider_request_headers,
|
|
|
|
|
upstream_url,
|
|
|
|
|
upstream_is_stream,
|
|
|
|
|
transport: transport.clone(),
|
|
|
|
|
})
|
|
|
|
|
}
|