mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-11 11:49:50 +08:00
feat(gateway): 重构 usage 数据层、迁移系统与系统导入
数据库迁移: - 引入 baseline v2 bootstrap,空库首次启动自动初始化 - 服务启动不再自动执行迁移,需显式 `--migrate` 运行 - 新增 pending migration 检测,schema 落后时拒绝启动 Usage 数据层: - usage body 存储外部化为独立 blob 表 - 新增 HTTP audit 表拆分存储请求/响应头与 body ref - 后台清理任务支持 legacy body ref 元数据迁移 - usage runtime 写入迁移到专用 tokio runtime(独立线程池, 8MB 栈) 系统导入/导出: - 支持用户、API Keys、钱包数据的完整导入 - 兼容 legacy 与 v1.3+ 两种导出格式 其他改进: - executor outcome 增加 runtime miss 诊断上下文 - 主 tokio runtime 栈大小调整为 8MB - 前端 provider 管理支持 base URL 配置 - dev.sh 支持 --migrate 参数
This commit is contained in:
@@ -8,6 +8,7 @@ pub(crate) mod transport;
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{Response, Uri};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{usage::GatewaySyncReportRequest, AppState, GatewayError};
|
||||
|
||||
@@ -58,6 +59,10 @@ pub(crate) fn collect_control_headers(
|
||||
crate::headers::collect_control_headers(headers)
|
||||
}
|
||||
|
||||
pub(crate) fn build_report_context_original_request_echo(body_json: &Value) -> Option<Value> {
|
||||
(!body_json.is_null()).then(|| body_json.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn is_json_request(headers: &http::HeaderMap) -> bool {
|
||||
crate::headers::is_json_request(headers)
|
||||
}
|
||||
@@ -99,3 +104,26 @@ pub(crate) fn maybe_build_local_sync_finalize_response(
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
crate::execution_runtime::maybe_build_local_sync_finalize_response(trace_id, decision, payload)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_report_context_original_request_echo;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn build_report_context_original_request_echo_preserves_full_request_body() {
|
||||
let body = json!({
|
||||
"messages": [{"role": "user", "content": "large payload should be omitted"}],
|
||||
"service_tier": "default",
|
||||
"instructions": "Be concise.",
|
||||
"thinking": {"type": "enabled", "budget_tokens": 512},
|
||||
"metadata": {"trace": "keep"},
|
||||
"body_bytes_b64": "aGVsbG8=",
|
||||
});
|
||||
|
||||
let echo =
|
||||
build_report_context_original_request_echo(&body).expect("echo should be produced");
|
||||
|
||||
assert_eq!(echo, body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use tracing::debug;
|
||||
use url::Url;
|
||||
|
||||
use crate::ai_pipeline::planner::common::{
|
||||
@@ -155,6 +156,25 @@ async fn maybe_build_local_video_task_follow_up_sync_decision_payload(
|
||||
} else {
|
||||
ConversionMode::Bidirectional
|
||||
};
|
||||
let upstream_base_url = infer_upstream_base_url(&follow_up.plan.url);
|
||||
|
||||
debug!(
|
||||
event_name = "local_video_follow_up_sync_decision_payload_built",
|
||||
log_type = "debug",
|
||||
trace_id = %trace_id,
|
||||
request_id = %trace_id,
|
||||
candidate_id = ?follow_up.plan.candidate_id,
|
||||
provider_id = %follow_up.plan.provider_id,
|
||||
endpoint_id = %follow_up.plan.endpoint_id,
|
||||
key_id = %follow_up.plan.key_id,
|
||||
plan_kind,
|
||||
downstream_path = %parts.uri.path(),
|
||||
provider_api_format = %follow_up.plan.provider_api_format,
|
||||
client_api_format = %follow_up.plan.client_api_format,
|
||||
upstream_base_url = ?upstream_base_url,
|
||||
upstream_url = %follow_up.plan.url,
|
||||
"gateway built local video follow-up sync decision payload"
|
||||
);
|
||||
|
||||
Ok(Some(GatewayControlSyncDecisionResponse {
|
||||
action: EXECUTION_RUNTIME_SYNC_DECISION_ACTION.to_string(),
|
||||
@@ -167,7 +187,7 @@ async fn maybe_build_local_video_task_follow_up_sync_decision_payload(
|
||||
provider_id: Some(follow_up.plan.provider_id.clone()),
|
||||
endpoint_id: Some(follow_up.plan.endpoint_id.clone()),
|
||||
key_id: Some(follow_up.plan.key_id.clone()),
|
||||
upstream_base_url: infer_upstream_base_url(&follow_up.plan.url),
|
||||
upstream_base_url,
|
||||
upstream_url: Some(follow_up.plan.url.clone()),
|
||||
provider_request_method: Some(follow_up.plan.method.clone()),
|
||||
auth_header: auth_pair.as_ref().map(|(name, _)| name.clone()),
|
||||
@@ -219,5 +239,79 @@ fn infer_upstream_base_url(upstream_url: &str) -> Option<String> {
|
||||
base.push(':');
|
||||
base.push_str(port.to_string().as_str());
|
||||
}
|
||||
let base_path = infer_upstream_base_path(parsed.path());
|
||||
if !base_path.is_empty() {
|
||||
base.push_str(base_path);
|
||||
}
|
||||
Some(base)
|
||||
}
|
||||
|
||||
fn infer_upstream_base_path(path: &str) -> &str {
|
||||
let trimmed = path.trim_end_matches('/');
|
||||
if trimmed.is_empty() || trimmed == "/" {
|
||||
return "";
|
||||
}
|
||||
|
||||
for suffix in [
|
||||
"/responses/compact",
|
||||
"/responses",
|
||||
"/chat/completions",
|
||||
"/messages",
|
||||
] {
|
||||
if let Some(prefix) = trimmed.strip_suffix(suffix) {
|
||||
return normalize_inferred_base_path(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
for marker in ["/v1/videos", "/v1beta/"] {
|
||||
if let Some((prefix, _)) = trimmed.split_once(marker) {
|
||||
return normalize_inferred_base_path(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
normalize_inferred_base_path(trimmed)
|
||||
}
|
||||
|
||||
fn normalize_inferred_base_path(path: &str) -> &str {
|
||||
let trimmed = path.trim_end_matches('/');
|
||||
if trimmed.is_empty() || trimmed == "/" {
|
||||
""
|
||||
} else {
|
||||
trimmed
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::infer_upstream_base_url;
|
||||
|
||||
#[test]
|
||||
fn infer_upstream_base_url_preserves_codex_base_path() {
|
||||
assert_eq!(
|
||||
infer_upstream_base_url("https://tiger.bookapi.cc/codex/responses").as_deref(),
|
||||
Some("https://tiger.bookapi.cc/codex")
|
||||
);
|
||||
assert_eq!(
|
||||
infer_upstream_base_url("https://chatgpt.com/backend-api/codex/responses").as_deref(),
|
||||
Some("https://chatgpt.com/backend-api/codex")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infer_upstream_base_url_preserves_nested_v1_prefix() {
|
||||
assert_eq!(
|
||||
infer_upstream_base_url("https://api.openai.example/custom/v1/chat/completions?mode=1")
|
||||
.as_deref(),
|
||||
Some("https://api.openai.example/custom/v1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infer_upstream_base_url_strips_video_operation_path() {
|
||||
assert_eq!(
|
||||
infer_upstream_base_url("https://video.example/nested/v1/videos/task-123/content")
|
||||
.as_deref(),
|
||||
Some("https://video.example/nested")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::ai_pipeline::GatewayControlDecision;
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
use crate::{
|
||||
AppState, GatewayControlSyncDecisionResponse, GatewayError, LocalExecutionRuntimeMissDiagnostic,
|
||||
};
|
||||
|
||||
use super::super::plans::{resolve_stream_spec, resolve_sync_spec};
|
||||
use super::candidates::{
|
||||
@@ -8,6 +10,52 @@ use super::candidates::{
|
||||
};
|
||||
use super::payload::maybe_build_local_same_format_provider_decision_payload_for_candidate;
|
||||
|
||||
fn extract_requested_model(
|
||||
parts: &http::request::Parts,
|
||||
body_json: &serde_json::Value,
|
||||
spec: crate::ai_pipeline::LocalSameFormatProviderSpec,
|
||||
) -> Option<String> {
|
||||
match spec.family {
|
||||
crate::ai_pipeline::LocalSameFormatProviderFamily::Standard => body_json
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned),
|
||||
crate::ai_pipeline::LocalSameFormatProviderFamily::Gemini => {
|
||||
let marker = "/models/";
|
||||
let start = parts.uri.path().find(marker)? + marker.len();
|
||||
let tail = &parts.uri.path()[start..];
|
||||
let end = tail.find(':').unwrap_or(tail.len());
|
||||
let model = tail[..end].trim();
|
||||
if model.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(model.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_local_same_format_miss_diagnostic(
|
||||
decision: &GatewayControlDecision,
|
||||
spec: crate::ai_pipeline::LocalSameFormatProviderSpec,
|
||||
requested_model: Option<&str>,
|
||||
reason: &str,
|
||||
) -> LocalExecutionRuntimeMissDiagnostic {
|
||||
LocalExecutionRuntimeMissDiagnostic {
|
||||
reason: reason.to_string(),
|
||||
route_family: decision.route_family.clone(),
|
||||
route_kind: decision.route_kind.clone(),
|
||||
public_path: Some(decision.public_path.clone()),
|
||||
plan_kind: Some(spec.decision_kind.to_string()),
|
||||
requested_model: requested_model.map(ToOwned::to_owned),
|
||||
candidate_count: None,
|
||||
skipped_candidate_count: None,
|
||||
skip_reasons: std::collections::BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_build_sync_local_same_format_provider_decision_payload(
|
||||
state: &AppState,
|
||||
parts: &http::request::Parts,
|
||||
@@ -25,12 +73,42 @@ pub(crate) async fn maybe_build_sync_local_same_format_provider_decision_payload
|
||||
)
|
||||
.await
|
||||
else {
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_same_format_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
extract_requested_model(parts, body_json, spec).as_deref(),
|
||||
"decision_input_unavailable",
|
||||
),
|
||||
);
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let attempts =
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_same_format_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
Some(input.requested_model.as_str()),
|
||||
"candidate_evaluation_incomplete",
|
||||
),
|
||||
);
|
||||
let (attempts, candidate_count) =
|
||||
materialize_local_same_format_provider_candidate_attempts(state, trace_id, &input, spec)
|
||||
.await?;
|
||||
let preserve_existing_candidate_signal = candidate_count == 0
|
||||
&& state.local_execution_runtime_miss_diagnostic_has_candidate_signal(trace_id);
|
||||
if !preserve_existing_candidate_signal {
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
diagnostic.candidate_count = Some(candidate_count);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else {
|
||||
"candidate_evaluation_incomplete".to_string()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
for attempt in attempts {
|
||||
if let Some(payload) =
|
||||
@@ -43,6 +121,18 @@ pub(crate) async fn maybe_build_sync_local_same_format_provider_decision_payload
|
||||
}
|
||||
}
|
||||
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
let candidate_count = diagnostic.candidate_count.unwrap_or(0);
|
||||
let skipped_candidate_count = diagnostic.skipped_candidate_count.unwrap_or(0);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else if skipped_candidate_count >= candidate_count {
|
||||
"all_candidates_skipped".to_string()
|
||||
} else {
|
||||
"no_local_sync_plans".to_string()
|
||||
};
|
||||
});
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -63,12 +153,42 @@ pub(crate) async fn maybe_build_stream_local_same_format_provider_decision_paylo
|
||||
)
|
||||
.await
|
||||
else {
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_same_format_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
extract_requested_model(parts, body_json, spec).as_deref(),
|
||||
"decision_input_unavailable",
|
||||
),
|
||||
);
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let attempts =
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_same_format_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
Some(input.requested_model.as_str()),
|
||||
"candidate_evaluation_incomplete",
|
||||
),
|
||||
);
|
||||
let (attempts, candidate_count) =
|
||||
materialize_local_same_format_provider_candidate_attempts(state, trace_id, &input, spec)
|
||||
.await?;
|
||||
let preserve_existing_candidate_signal = candidate_count == 0
|
||||
&& state.local_execution_runtime_miss_diagnostic_has_candidate_signal(trace_id);
|
||||
if !preserve_existing_candidate_signal {
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
diagnostic.candidate_count = Some(candidate_count);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else {
|
||||
"candidate_evaluation_incomplete".to_string()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
for attempt in attempts {
|
||||
if let Some(payload) =
|
||||
@@ -81,5 +201,17 @@ pub(crate) async fn maybe_build_stream_local_same_format_provider_decision_paylo
|
||||
}
|
||||
}
|
||||
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
let candidate_count = diagnostic.candidate_count.unwrap_or(0);
|
||||
let skipped_candidate_count = diagnostic.skipped_candidate_count.unwrap_or(0);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else if skipped_candidate_count >= candidate_count {
|
||||
"all_candidates_skipped".to_string()
|
||||
} else {
|
||||
"no_local_stream_plans".to_string()
|
||||
};
|
||||
});
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
+3
-2
@@ -85,7 +85,7 @@ pub(crate) async fn materialize_local_same_format_provider_candidate_attempts(
|
||||
trace_id: &str,
|
||||
input: &LocalSameFormatProviderDecisionInput,
|
||||
spec: LocalSameFormatProviderSpec,
|
||||
) -> Result<Vec<LocalSameFormatProviderCandidateAttempt>, GatewayError> {
|
||||
) -> Result<(Vec<LocalSameFormatProviderCandidateAttempt>, usize), GatewayError> {
|
||||
let planner_state = PlannerAppState::new(state);
|
||||
let candidates = planner_state
|
||||
.list_selectable_candidates(
|
||||
@@ -104,6 +104,7 @@ pub(crate) async fn materialize_local_same_format_provider_candidate_attempts(
|
||||
input.required_capabilities.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let candidate_count = candidates.len();
|
||||
|
||||
let created_at_unix_ms = current_unix_ms();
|
||||
let mut attempts = Vec::with_capacity(candidates.len());
|
||||
@@ -160,5 +161,5 @@ pub(crate) async fn materialize_local_same_format_provider_candidate_attempts(
|
||||
});
|
||||
}
|
||||
|
||||
Ok(attempts)
|
||||
Ok((attempts, candidate_count))
|
||||
}
|
||||
|
||||
@@ -287,9 +287,8 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
|
||||
"upstream_url": upstream_url,
|
||||
"provider_request_method": serde_json::Value::Null,
|
||||
"provider_request_headers": provider_request_headers,
|
||||
"provider_request_body": provider_request_body,
|
||||
"original_headers": collect_control_headers(&parts.headers),
|
||||
"original_request_body": body_json,
|
||||
"original_request_body": crate::ai_pipeline::build_report_context_original_request_echo(body_json),
|
||||
"has_envelope": is_kiro || is_antigravity,
|
||||
"envelope_name": if is_kiro {
|
||||
Some(KIRO_ENVELOPE_NAME)
|
||||
@@ -359,6 +358,13 @@ pub(super) async fn mark_skipped_local_same_format_provider_candidate(
|
||||
candidate_id: &str,
|
||||
skip_reason: &'static str,
|
||||
) {
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
*diagnostic
|
||||
.skip_reasons
|
||||
.entry(skip_reason.to_string())
|
||||
.or_insert(0) += 1;
|
||||
*diagnostic.skipped_candidate_count.get_or_insert(0) += 1;
|
||||
});
|
||||
PlannerAppState::new(state)
|
||||
.persist_skipped_local_candidate(
|
||||
trace_id,
|
||||
|
||||
@@ -16,6 +16,53 @@ use crate::ai_pipeline::planner::plan_builders::{
|
||||
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
|
||||
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
|
||||
};
|
||||
use crate::LocalExecutionRuntimeMissDiagnostic;
|
||||
|
||||
fn extract_requested_model(
|
||||
parts: &http::request::Parts,
|
||||
body_json: &serde_json::Value,
|
||||
spec: LocalSameFormatProviderSpec,
|
||||
) -> Option<String> {
|
||||
match spec.family {
|
||||
LocalSameFormatProviderFamily::Standard => body_json
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned),
|
||||
LocalSameFormatProviderFamily::Gemini => {
|
||||
let marker = "/models/";
|
||||
let start = parts.uri.path().find(marker)? + marker.len();
|
||||
let tail = &parts.uri.path()[start..];
|
||||
let end = tail.find(':').unwrap_or(tail.len());
|
||||
let model = tail[..end].trim();
|
||||
if model.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(model.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_local_same_format_miss_diagnostic(
|
||||
decision: &GatewayControlDecision,
|
||||
spec: LocalSameFormatProviderSpec,
|
||||
requested_model: Option<&str>,
|
||||
reason: &str,
|
||||
) -> LocalExecutionRuntimeMissDiagnostic {
|
||||
LocalExecutionRuntimeMissDiagnostic {
|
||||
reason: reason.to_string(),
|
||||
route_family: decision.route_family.clone(),
|
||||
route_kind: decision.route_kind.clone(),
|
||||
public_path: Some(decision.public_path.clone()),
|
||||
plan_kind: Some(spec.decision_kind.to_string()),
|
||||
requested_model: requested_model.map(ToOwned::to_owned),
|
||||
candidate_count: None,
|
||||
skipped_candidate_count: None,
|
||||
skip_reasons: std::collections::BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn build_local_sync_plan_and_reports(
|
||||
state: &AppState,
|
||||
@@ -30,12 +77,44 @@ pub(crate) async fn build_local_sync_plan_and_reports(
|
||||
)
|
||||
.await
|
||||
else {
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_same_format_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
extract_requested_model(parts, body_json, spec).as_deref(),
|
||||
"decision_input_unavailable",
|
||||
),
|
||||
);
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
let attempts =
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_same_format_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
Some(input.requested_model.as_str()),
|
||||
"candidate_evaluation_incomplete",
|
||||
),
|
||||
);
|
||||
let (attempts, candidate_count) =
|
||||
materialize_local_same_format_provider_candidate_attempts(state, trace_id, &input, spec)
|
||||
.await?;
|
||||
let preserve_existing_candidate_signal = candidate_count == 0
|
||||
&& state.local_execution_runtime_miss_diagnostic_has_candidate_signal(trace_id);
|
||||
if !preserve_existing_candidate_signal {
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
diagnostic.candidate_count = Some(candidate_count);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else {
|
||||
"candidate_evaluation_incomplete".to_string()
|
||||
};
|
||||
});
|
||||
}
|
||||
if candidate_count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut plans = Vec::new();
|
||||
for attempt in attempts {
|
||||
@@ -70,6 +149,16 @@ pub(crate) async fn build_local_sync_plan_and_reports(
|
||||
}
|
||||
}
|
||||
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
let candidate_count = diagnostic.candidate_count.unwrap_or(0);
|
||||
let skipped_candidate_count = diagnostic.skipped_candidate_count.unwrap_or(0);
|
||||
diagnostic.reason = if candidate_count > 0 && skipped_candidate_count >= candidate_count {
|
||||
"all_candidates_skipped".to_string()
|
||||
} else {
|
||||
"no_local_sync_plans".to_string()
|
||||
};
|
||||
});
|
||||
|
||||
Ok(plans)
|
||||
}
|
||||
|
||||
@@ -86,12 +175,44 @@ pub(crate) async fn build_local_stream_plan_and_reports(
|
||||
)
|
||||
.await
|
||||
else {
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_same_format_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
extract_requested_model(parts, body_json, spec).as_deref(),
|
||||
"decision_input_unavailable",
|
||||
),
|
||||
);
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
let attempts =
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_same_format_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
Some(input.requested_model.as_str()),
|
||||
"candidate_evaluation_incomplete",
|
||||
),
|
||||
);
|
||||
let (attempts, candidate_count) =
|
||||
materialize_local_same_format_provider_candidate_attempts(state, trace_id, &input, spec)
|
||||
.await?;
|
||||
let preserve_existing_candidate_signal = candidate_count == 0
|
||||
&& state.local_execution_runtime_miss_diagnostic_has_candidate_signal(trace_id);
|
||||
if !preserve_existing_candidate_signal {
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
diagnostic.candidate_count = Some(candidate_count);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else {
|
||||
"candidate_evaluation_incomplete".to_string()
|
||||
};
|
||||
});
|
||||
}
|
||||
if candidate_count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut plans = Vec::new();
|
||||
for attempt in attempts {
|
||||
@@ -126,5 +247,15 @@ pub(crate) async fn build_local_stream_plan_and_reports(
|
||||
}
|
||||
}
|
||||
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
let candidate_count = diagnostic.candidate_count.unwrap_or(0);
|
||||
let skipped_candidate_count = diagnostic.skipped_candidate_count.unwrap_or(0);
|
||||
diagnostic.reason = if candidate_count > 0 && skipped_candidate_count >= candidate_count {
|
||||
"all_candidates_skipped".to_string()
|
||||
} else {
|
||||
"no_local_stream_plans".to_string()
|
||||
};
|
||||
});
|
||||
|
||||
Ok(plans)
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
|
||||
"provider_api_format": GEMINI_FILES_CLIENT_API_FORMAT,
|
||||
"client_api_format": GEMINI_FILES_CLIENT_API_FORMAT,
|
||||
"original_headers": collect_control_headers(&parts.headers),
|
||||
"original_request_body": original_request_body,
|
||||
"original_request_body": crate::ai_pipeline::build_report_context_original_request_echo(&original_request_body),
|
||||
"has_envelope": false,
|
||||
"needs_conversion": false,
|
||||
})),
|
||||
|
||||
@@ -263,7 +263,7 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat
|
||||
"client_api_format": spec.api_format,
|
||||
"mapped_model": mapped_model,
|
||||
"original_headers": collect_control_headers(&parts.headers),
|
||||
"original_request_body": body_json,
|
||||
"original_request_body": crate::ai_pipeline::build_report_context_original_request_echo(body_json),
|
||||
"has_envelope": false,
|
||||
"needs_conversion": false,
|
||||
})),
|
||||
|
||||
@@ -6,7 +6,9 @@ use crate::ai_pipeline::planner::plan_builders::{
|
||||
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
|
||||
};
|
||||
use crate::ai_pipeline::GatewayControlDecision;
|
||||
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
|
||||
use crate::{
|
||||
AppState, GatewayControlSyncDecisionResponse, GatewayError, LocalExecutionRuntimeMissDiagnostic,
|
||||
};
|
||||
|
||||
use super::candidates::{
|
||||
materialize_local_standard_candidate_attempts, resolve_local_standard_decision_input,
|
||||
@@ -14,6 +16,52 @@ use super::candidates::{
|
||||
use super::payload::maybe_build_local_standard_decision_payload_for_candidate;
|
||||
use super::{LocalStandardSourceFamily, LocalStandardSpec};
|
||||
|
||||
fn extract_requested_model(
|
||||
parts: &http::request::Parts,
|
||||
body_json: &serde_json::Value,
|
||||
spec: LocalStandardSpec,
|
||||
) -> Option<String> {
|
||||
match spec.family {
|
||||
LocalStandardSourceFamily::Standard => body_json
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned),
|
||||
LocalStandardSourceFamily::Gemini => {
|
||||
let marker = "/models/";
|
||||
let start = parts.uri.path().find(marker)? + marker.len();
|
||||
let tail = &parts.uri.path()[start..];
|
||||
let end = tail.find(':').unwrap_or(tail.len());
|
||||
let model = tail[..end].trim();
|
||||
if model.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(model.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_local_standard_miss_diagnostic(
|
||||
decision: &GatewayControlDecision,
|
||||
spec: LocalStandardSpec,
|
||||
requested_model: Option<&str>,
|
||||
reason: &str,
|
||||
) -> LocalExecutionRuntimeMissDiagnostic {
|
||||
LocalExecutionRuntimeMissDiagnostic {
|
||||
reason: reason.to_string(),
|
||||
route_family: decision.route_family.clone(),
|
||||
route_kind: decision.route_kind.clone(),
|
||||
public_path: Some(decision.public_path.clone()),
|
||||
plan_kind: Some(spec.decision_kind.to_string()),
|
||||
requested_model: requested_model.map(ToOwned::to_owned),
|
||||
candidate_count: None,
|
||||
skipped_candidate_count: None,
|
||||
skip_reasons: std::collections::BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_build_sync_via_standard_family_payload(
|
||||
state: &AppState,
|
||||
parts: &http::request::Parts,
|
||||
@@ -34,8 +82,25 @@ pub(crate) async fn maybe_build_sync_via_standard_family_payload(
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let attempts =
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_standard_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
Some(input.requested_model.as_str()),
|
||||
"candidate_evaluation_incomplete",
|
||||
),
|
||||
);
|
||||
let (attempts, candidate_count) =
|
||||
materialize_local_standard_candidate_attempts(state, trace_id, &input, spec).await?;
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
diagnostic.candidate_count = Some(candidate_count);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else {
|
||||
"candidate_evaluation_incomplete".to_string()
|
||||
};
|
||||
});
|
||||
|
||||
for attempt in attempts {
|
||||
if let Some(payload) = maybe_build_local_standard_decision_payload_for_candidate(
|
||||
@@ -47,6 +112,18 @@ pub(crate) async fn maybe_build_sync_via_standard_family_payload(
|
||||
}
|
||||
}
|
||||
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
let candidate_count = diagnostic.candidate_count.unwrap_or(0);
|
||||
let skipped_candidate_count = diagnostic.skipped_candidate_count.unwrap_or(0);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else if skipped_candidate_count >= candidate_count {
|
||||
"all_candidates_skipped".to_string()
|
||||
} else {
|
||||
"no_local_sync_plans".to_string()
|
||||
};
|
||||
});
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -70,8 +147,25 @@ pub(crate) async fn maybe_build_stream_via_standard_family_payload(
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let attempts =
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_standard_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
Some(input.requested_model.as_str()),
|
||||
"candidate_evaluation_incomplete",
|
||||
),
|
||||
);
|
||||
let (attempts, candidate_count) =
|
||||
materialize_local_standard_candidate_attempts(state, trace_id, &input, spec).await?;
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
diagnostic.candidate_count = Some(candidate_count);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else {
|
||||
"candidate_evaluation_incomplete".to_string()
|
||||
};
|
||||
});
|
||||
|
||||
for attempt in attempts {
|
||||
if let Some(payload) = maybe_build_local_standard_decision_payload_for_candidate(
|
||||
@@ -83,6 +177,18 @@ pub(crate) async fn maybe_build_stream_via_standard_family_payload(
|
||||
}
|
||||
}
|
||||
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
let candidate_count = diagnostic.candidate_count.unwrap_or(0);
|
||||
let skipped_candidate_count = diagnostic.skipped_candidate_count.unwrap_or(0);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else if skipped_candidate_count >= candidate_count {
|
||||
"all_candidates_skipped".to_string()
|
||||
} else {
|
||||
"no_local_stream_plans".to_string()
|
||||
};
|
||||
});
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -98,10 +204,39 @@ pub(crate) async fn build_local_sync_plan_and_reports(
|
||||
resolve_local_standard_decision_input(state, parts, trace_id, decision, body_json, spec)
|
||||
.await
|
||||
else {
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_standard_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
extract_requested_model(parts, body_json, spec).as_deref(),
|
||||
"decision_input_unavailable",
|
||||
),
|
||||
);
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let attempts =
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_standard_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
Some(input.requested_model.as_str()),
|
||||
"candidate_evaluation_incomplete",
|
||||
),
|
||||
);
|
||||
let (attempts, candidate_count) =
|
||||
materialize_local_standard_candidate_attempts(state, trace_id, &input, spec).await?;
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
diagnostic.candidate_count = Some(candidate_count);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else {
|
||||
"candidate_evaluation_incomplete".to_string()
|
||||
};
|
||||
});
|
||||
if candidate_count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut plans = Vec::new();
|
||||
for attempt in attempts {
|
||||
let Some(payload) = maybe_build_local_standard_decision_payload_for_candidate(
|
||||
@@ -132,6 +267,15 @@ pub(crate) async fn build_local_sync_plan_and_reports(
|
||||
}
|
||||
}
|
||||
}
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
let candidate_count = diagnostic.candidate_count.unwrap_or(0);
|
||||
let skipped_candidate_count = diagnostic.skipped_candidate_count.unwrap_or(0);
|
||||
diagnostic.reason = if candidate_count > 0 && skipped_candidate_count >= candidate_count {
|
||||
"all_candidates_skipped".to_string()
|
||||
} else {
|
||||
"no_local_sync_plans".to_string()
|
||||
};
|
||||
});
|
||||
Ok(plans)
|
||||
}
|
||||
|
||||
@@ -147,10 +291,39 @@ pub(crate) async fn build_local_stream_plan_and_reports(
|
||||
resolve_local_standard_decision_input(state, parts, trace_id, decision, body_json, spec)
|
||||
.await
|
||||
else {
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_standard_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
extract_requested_model(parts, body_json, spec).as_deref(),
|
||||
"decision_input_unavailable",
|
||||
),
|
||||
);
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let attempts =
|
||||
state.set_local_execution_runtime_miss_diagnostic(
|
||||
trace_id,
|
||||
build_local_standard_miss_diagnostic(
|
||||
decision,
|
||||
spec,
|
||||
Some(input.requested_model.as_str()),
|
||||
"candidate_evaluation_incomplete",
|
||||
),
|
||||
);
|
||||
let (attempts, candidate_count) =
|
||||
materialize_local_standard_candidate_attempts(state, trace_id, &input, spec).await?;
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
diagnostic.candidate_count = Some(candidate_count);
|
||||
diagnostic.reason = if candidate_count == 0 {
|
||||
"candidate_list_empty".to_string()
|
||||
} else {
|
||||
"candidate_evaluation_incomplete".to_string()
|
||||
};
|
||||
});
|
||||
if candidate_count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut plans = Vec::new();
|
||||
for attempt in attempts {
|
||||
let Some(payload) = maybe_build_local_standard_decision_payload_for_candidate(
|
||||
@@ -181,5 +354,14 @@ pub(crate) async fn build_local_stream_plan_and_reports(
|
||||
}
|
||||
}
|
||||
}
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
let candidate_count = diagnostic.candidate_count.unwrap_or(0);
|
||||
let skipped_candidate_count = diagnostic.skipped_candidate_count.unwrap_or(0);
|
||||
diagnostic.reason = if candidate_count > 0 && skipped_candidate_count >= candidate_count {
|
||||
"all_candidates_skipped".to_string()
|
||||
} else {
|
||||
"no_local_stream_plans".to_string()
|
||||
};
|
||||
});
|
||||
Ok(plans)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ pub(super) async fn materialize_local_standard_candidate_attempts(
|
||||
trace_id: &str,
|
||||
input: &LocalStandardDecisionInput,
|
||||
spec: LocalStandardSpec,
|
||||
) -> Result<Vec<LocalStandardCandidateAttempt>, GatewayError> {
|
||||
) -> Result<(Vec<LocalStandardCandidateAttempt>, usize), GatewayError> {
|
||||
let planner_state = PlannerAppState::new(state);
|
||||
let mut seen_candidates = BTreeSet::new();
|
||||
let mut candidates = Vec::new();
|
||||
@@ -144,6 +144,7 @@ pub(super) async fn materialize_local_standard_candidate_attempts(
|
||||
input.required_capabilities.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let candidate_count = candidates.len();
|
||||
|
||||
let created_at_unix_ms = current_unix_ms();
|
||||
let mut attempts = Vec::with_capacity(candidates.len());
|
||||
@@ -257,7 +258,7 @@ pub(super) async fn materialize_local_standard_candidate_attempts(
|
||||
});
|
||||
}
|
||||
|
||||
Ok(attempts)
|
||||
Ok((attempts, candidate_count))
|
||||
}
|
||||
|
||||
fn auth_snapshot_allows_cross_format_candidate(
|
||||
|
||||
@@ -358,9 +358,8 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
||||
"upstream_url": upstream_url,
|
||||
"provider_request_method": serde_json::Value::Null,
|
||||
"provider_request_headers": provider_request_headers,
|
||||
"provider_request_body": provider_request_body,
|
||||
"original_headers": collect_control_headers(&parts.headers),
|
||||
"original_request_body": body_json,
|
||||
"original_request_body": crate::ai_pipeline::build_report_context_original_request_echo(body_json),
|
||||
"has_envelope": false,
|
||||
"needs_conversion": true,
|
||||
}),
|
||||
@@ -384,6 +383,13 @@ pub(super) async fn mark_skipped_local_standard_candidate(
|
||||
candidate_id: &str,
|
||||
skip_reason: &'static str,
|
||||
) {
|
||||
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
||||
*diagnostic
|
||||
.skip_reasons
|
||||
.entry(skip_reason.to_string())
|
||||
.or_insert(0) += 1;
|
||||
*diagnostic.skipped_candidate_count.get_or_insert(0) += 1;
|
||||
});
|
||||
PlannerAppState::new(state)
|
||||
.persist_skipped_local_candidate(
|
||||
trace_id,
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
use aether_provider_transport::snapshot::{
|
||||
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
|
||||
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||
};
|
||||
use http::Request;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::{build_cross_format_openai_cli_request_body, build_local_openai_cli_request_body};
|
||||
use super::{
|
||||
build_cross_format_openai_cli_request_body, build_local_openai_cli_request_body,
|
||||
build_local_openai_cli_upstream_url,
|
||||
};
|
||||
|
||||
fn object_keys(value: &Value) -> Vec<&str> {
|
||||
value
|
||||
@@ -11,6 +19,59 @@ fn object_keys(value: &Value) -> Vec<&str> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn sample_transport(base_url: &str, api_format: &str) -> GatewayProviderTransportSnapshot {
|
||||
GatewayProviderTransportSnapshot {
|
||||
provider: GatewayProviderTransportProvider {
|
||||
id: "provider-codex".to_string(),
|
||||
name: "codex".to_string(),
|
||||
provider_type: "codex".to_string(),
|
||||
website: None,
|
||||
is_active: true,
|
||||
keep_priority_on_conversion: false,
|
||||
enable_format_conversion: false,
|
||||
concurrent_limit: None,
|
||||
max_retries: None,
|
||||
proxy: None,
|
||||
request_timeout_secs: None,
|
||||
stream_first_byte_timeout_secs: None,
|
||||
config: None,
|
||||
},
|
||||
endpoint: GatewayProviderTransportEndpoint {
|
||||
id: "endpoint-codex".to_string(),
|
||||
provider_id: "provider-codex".to_string(),
|
||||
api_format: api_format.to_string(),
|
||||
api_family: Some("openai".to_string()),
|
||||
endpoint_kind: Some("cli".to_string()),
|
||||
is_active: true,
|
||||
base_url: base_url.to_string(),
|
||||
header_rules: None,
|
||||
body_rules: None,
|
||||
max_retries: None,
|
||||
custom_path: None,
|
||||
config: None,
|
||||
format_acceptance_config: None,
|
||||
proxy: None,
|
||||
},
|
||||
key: GatewayProviderTransportKey {
|
||||
id: "key-codex".to_string(),
|
||||
provider_id: "provider-codex".to_string(),
|
||||
name: "oauth".to_string(),
|
||||
auth_type: "oauth".to_string(),
|
||||
is_active: true,
|
||||
api_formats: Some(vec![api_format.to_string()]),
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
global_priority_by_format: None,
|
||||
expires_at_unix_secs: None,
|
||||
proxy: None,
|
||||
fingerprint: None,
|
||||
decrypted_api_key: "__placeholder__".to_string(),
|
||||
decrypted_auth_config: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_openai_chat_cross_format_request_body_from_openai_cli_source() {
|
||||
let body_json = json!({
|
||||
@@ -103,6 +164,25 @@ fn local_openai_compact_wrapper_strips_store_for_same_format_requests() {
|
||||
assert!(provider_request_body.get("store").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_openai_cli_upstream_url_preserves_codex_base_path() {
|
||||
let request = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/responses")
|
||||
.body(())
|
||||
.expect("request should build");
|
||||
let (parts, _) = request.into_parts();
|
||||
|
||||
let upstream_url = build_local_openai_cli_upstream_url(
|
||||
&parts,
|
||||
&sample_transport("https://tiger.bookapi.cc/codex", "openai:cli"),
|
||||
false,
|
||||
)
|
||||
.expect("openai cli upstream url should build");
|
||||
|
||||
assert_eq!(upstream_url, "https://tiger.bookapi.cc/codex/responses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_metadata_for_codex_openai_cli_requests() {
|
||||
let body_json = json!({
|
||||
|
||||
+1
-2
@@ -310,9 +310,8 @@ pub(super) async fn build_cross_format_local_openai_chat_decision_payload_for_ca
|
||||
"upstream_url": upstream_url,
|
||||
"provider_request_method": serde_json::Value::Null,
|
||||
"provider_request_headers": provider_request_headers,
|
||||
"provider_request_body": provider_request_body,
|
||||
"original_headers": collect_control_headers(&parts.headers),
|
||||
"original_request_body": body_json,
|
||||
"original_request_body": crate::ai_pipeline::build_report_context_original_request_echo(body_json),
|
||||
"has_envelope": false,
|
||||
"needs_conversion": true,
|
||||
}),
|
||||
|
||||
+1
-2
@@ -254,9 +254,8 @@ pub(super) async fn build_same_format_local_openai_chat_decision_payload_for_can
|
||||
"upstream_url": upstream_url,
|
||||
"provider_request_method": serde_json::Value::Null,
|
||||
"provider_request_headers": provider_request_headers,
|
||||
"provider_request_body": provider_request_body,
|
||||
"original_headers": collect_control_headers(&parts.headers),
|
||||
"original_request_body": body_json,
|
||||
"original_request_body": crate::ai_pipeline::build_report_context_original_request_echo(body_json),
|
||||
"has_envelope": false,
|
||||
"needs_conversion": false,
|
||||
}),
|
||||
|
||||
+28
-3
@@ -1,6 +1,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::json;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::ai_pipeline::collect_control_headers;
|
||||
use crate::ai_pipeline::transport::{
|
||||
@@ -53,6 +54,31 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
|
||||
let proxy =
|
||||
resolve_transport_proxy_snapshot_with_tunnel_affinity(state, &resolved.transport).await;
|
||||
let tls_profile = resolve_transport_tls_profile(&resolved.transport);
|
||||
let timeouts = resolve_transport_execution_timeouts(&resolved.transport);
|
||||
|
||||
debug!(
|
||||
event_name = "local_openai_cli_decision_payload_built",
|
||||
log_type = "debug",
|
||||
trace_id = %trace_id,
|
||||
candidate_id = %candidate_id,
|
||||
candidate_index,
|
||||
provider_name = %resolved.transport.provider.name,
|
||||
provider_id = %candidate.provider_id,
|
||||
endpoint_id = %candidate.endpoint_id,
|
||||
key_id = %candidate.key_id,
|
||||
decision_kind = spec.decision_kind,
|
||||
execution_strategy = resolved.execution_strategy.as_str(),
|
||||
conversion_mode = resolved.conversion_mode.as_str(),
|
||||
client_api_format = spec.api_format,
|
||||
provider_api_format = %resolved.provider_api_format,
|
||||
request_path = %parts.uri.path(),
|
||||
request_query = ?parts.uri.query(),
|
||||
upstream_base_url = %resolved.transport.endpoint.base_url,
|
||||
upstream_url = %resolved.upstream_url,
|
||||
upstream_is_stream = resolved.upstream_is_stream,
|
||||
has_envelope = resolved.is_antigravity,
|
||||
"gateway built local openai cli decision payload"
|
||||
);
|
||||
|
||||
Some(GatewayControlSyncDecisionResponse {
|
||||
action: if spec.require_streaming {
|
||||
@@ -89,7 +115,7 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
|
||||
content_type: Some("application/json".to_string()),
|
||||
proxy,
|
||||
tls_profile,
|
||||
timeouts: resolve_transport_execution_timeouts(&resolved.transport),
|
||||
timeouts,
|
||||
upstream_is_stream: resolved.upstream_is_stream,
|
||||
report_kind: Some(spec.report_kind.to_string()),
|
||||
report_context: Some(append_local_failover_policy_to_value(
|
||||
@@ -115,9 +141,8 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
|
||||
"upstream_url": resolved.upstream_url,
|
||||
"provider_request_method": serde_json::Value::Null,
|
||||
"provider_request_headers": resolved.provider_request_headers,
|
||||
"provider_request_body": resolved.provider_request_body,
|
||||
"original_headers": collect_control_headers(&parts.headers),
|
||||
"original_request_body": body_json,
|
||||
"original_request_body": crate::ai_pipeline::build_report_context_original_request_echo(body_json),
|
||||
"has_envelope": resolved.is_antigravity,
|
||||
"envelope_name": if resolved.is_antigravity {
|
||||
Some("antigravity:v1internal")
|
||||
|
||||
+25
-1
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
|
||||
use serde_json::Value;
|
||||
use tracing::warn;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::ai_pipeline::conversion::{
|
||||
request_conversion_direct_auth, request_conversion_kind,
|
||||
@@ -430,6 +430,30 @@ pub(crate) async fn resolve_local_openai_cli_candidate_payload_parts(
|
||||
ConversionMode::None
|
||||
};
|
||||
|
||||
debug!(
|
||||
event_name = "local_openai_cli_upstream_url_resolved",
|
||||
log_type = "debug",
|
||||
trace_id = %trace_id,
|
||||
candidate_id = %candidate_id,
|
||||
candidate_index,
|
||||
provider_id = %candidate.provider_id,
|
||||
endpoint_id = %candidate.endpoint_id,
|
||||
key_id = %candidate.key_id,
|
||||
provider_type = %transport.provider.provider_type,
|
||||
client_api_format = spec.api_format,
|
||||
provider_api_format = %provider_api_format,
|
||||
execution_strategy = execution_strategy.as_str(),
|
||||
conversion_mode = conversion_mode.as_str(),
|
||||
base_url = %transport.endpoint.base_url,
|
||||
custom_path = ?transport.endpoint.custom_path,
|
||||
request_path = %parts.uri.path(),
|
||||
request_query = ?parts.uri.query(),
|
||||
mapped_model = %mapped_model,
|
||||
upstream_url = %upstream_url,
|
||||
upstream_is_stream,
|
||||
"gateway resolved local openai cli upstream url"
|
||||
);
|
||||
|
||||
Some(LocalOpenAiCliCandidatePayloadParts {
|
||||
auth_header,
|
||||
auth_value,
|
||||
|
||||
+36
-9
@@ -1,4 +1,5 @@
|
||||
use aether_contracts::{ExecutionPlan, RequestBody};
|
||||
use tracing::debug;
|
||||
|
||||
use super::super::{
|
||||
augment_sync_report_context, generic_decision_missing_exact_provider_request,
|
||||
@@ -263,12 +264,12 @@ pub(crate) fn build_openai_cli_stream_plan_from_decision(
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let url = if let Some(upstream_url) = payload
|
||||
let (url, url_source) = if let Some(upstream_url) = payload
|
||||
.upstream_url
|
||||
.clone()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
upstream_url
|
||||
(upstream_url, "upstream_url")
|
||||
} else {
|
||||
let Some(upstream_base_url) = payload
|
||||
.upstream_base_url
|
||||
@@ -277,7 +278,10 @@ pub(crate) fn build_openai_cli_stream_plan_from_decision(
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
build_openai_cli_url(&upstream_base_url, parts.uri.query(), compact)
|
||||
(
|
||||
build_openai_cli_url(&upstream_base_url, parts.uri.query(), compact),
|
||||
"upstream_base_url",
|
||||
)
|
||||
};
|
||||
let Some(provider_request_body_value) = payload.provider_request_body.clone() else {
|
||||
return Ok(None);
|
||||
@@ -330,6 +334,27 @@ pub(crate) fn build_openai_cli_stream_plan_from_decision(
|
||||
timeouts: payload.timeouts.clone(),
|
||||
};
|
||||
|
||||
debug!(
|
||||
event_name = "local_openai_cli_stream_plan_built",
|
||||
log_type = "debug",
|
||||
request_id = %plan.request_id,
|
||||
candidate_id = ?plan.candidate_id,
|
||||
provider_id = %plan.provider_id,
|
||||
endpoint_id = %plan.endpoint_id,
|
||||
key_id = %plan.key_id,
|
||||
downstream_path = %parts.uri.path(),
|
||||
downstream_query = ?parts.uri.query(),
|
||||
url_source,
|
||||
decision_upstream_base_url = ?payload.upstream_base_url,
|
||||
decision_upstream_url = ?payload.upstream_url,
|
||||
plan_url = %plan.url,
|
||||
client_api_format = %plan.client_api_format,
|
||||
provider_api_format = %plan.provider_api_format,
|
||||
upstream_is_stream = payload.upstream_is_stream,
|
||||
compact,
|
||||
"gateway built local openai cli stream execution plan"
|
||||
);
|
||||
|
||||
Ok(Some(LocalStreamPlanAndReport {
|
||||
plan,
|
||||
report_kind: payload.report_kind,
|
||||
@@ -446,12 +471,14 @@ mod tests {
|
||||
"prompt_cache_key",
|
||||
]
|
||||
);
|
||||
let report_context = built
|
||||
.report_context
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("provider_request_body"))
|
||||
.expect("report context should contain provider request body");
|
||||
assert_eq!(object_keys(report_context), object_keys(plan_body));
|
||||
assert!(
|
||||
built
|
||||
.report_context
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("provider_request_body"))
|
||||
.is_none(),
|
||||
"report context should not duplicate provider request body"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use aether_contracts::{ExecutionPlan, RequestBody};
|
||||
use tracing::debug;
|
||||
|
||||
use super::super::{
|
||||
augment_sync_report_context, generic_decision_missing_exact_provider_request,
|
||||
@@ -267,12 +268,12 @@ pub(crate) fn build_openai_cli_sync_plan_from_decision(
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let url = if let Some(upstream_url) = payload
|
||||
let (url, url_source) = if let Some(upstream_url) = payload
|
||||
.upstream_url
|
||||
.clone()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
upstream_url
|
||||
(upstream_url, "upstream_url")
|
||||
} else {
|
||||
let Some(upstream_base_url) = payload
|
||||
.upstream_base_url
|
||||
@@ -281,7 +282,10 @@ pub(crate) fn build_openai_cli_sync_plan_from_decision(
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
build_openai_cli_url(&upstream_base_url, parts.uri.query(), compact)
|
||||
(
|
||||
build_openai_cli_url(&upstream_base_url, parts.uri.query(), compact),
|
||||
"upstream_base_url",
|
||||
)
|
||||
};
|
||||
let Some(provider_request_body_value) = payload.provider_request_body.clone() else {
|
||||
return Ok(None);
|
||||
@@ -324,6 +328,27 @@ pub(crate) fn build_openai_cli_sync_plan_from_decision(
|
||||
timeouts: payload.timeouts.clone(),
|
||||
};
|
||||
|
||||
debug!(
|
||||
event_name = "local_openai_cli_sync_plan_built",
|
||||
log_type = "debug",
|
||||
request_id = %plan.request_id,
|
||||
candidate_id = ?plan.candidate_id,
|
||||
provider_id = %plan.provider_id,
|
||||
endpoint_id = %plan.endpoint_id,
|
||||
key_id = %plan.key_id,
|
||||
downstream_path = %parts.uri.path(),
|
||||
downstream_query = ?parts.uri.query(),
|
||||
url_source,
|
||||
decision_upstream_base_url = ?payload.upstream_base_url,
|
||||
decision_upstream_url = ?payload.upstream_url,
|
||||
plan_url = %plan.url,
|
||||
client_api_format = %plan.client_api_format,
|
||||
provider_api_format = %plan.provider_api_format,
|
||||
upstream_is_stream = payload.upstream_is_stream,
|
||||
compact,
|
||||
"gateway built local openai cli sync execution plan"
|
||||
);
|
||||
|
||||
Ok(Some(LocalSyncPlanAndReport {
|
||||
plan,
|
||||
report_kind: payload.report_kind,
|
||||
@@ -440,12 +465,14 @@ mod tests {
|
||||
"prompt_cache_key",
|
||||
]
|
||||
);
|
||||
let report_context = built
|
||||
.report_context
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("provider_request_body"))
|
||||
.expect("report context should contain provider request body");
|
||||
assert_eq!(object_keys(report_context), object_keys(plan_body));
|
||||
assert!(
|
||||
built
|
||||
.report_context
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("provider_request_body"))
|
||||
.is_none(),
|
||||
"report context should not duplicate provider request body"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user