mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
Revert "Merge PR #504"
This reverts commitd216a9e219, reversing changes made to21e82abd54.
This commit is contained in:
@@ -73,10 +73,6 @@ impl AiRuntimeMissDiagnosticPort for GatewayRuntimeMissDiagnosticPort<'_> {
|
|||||||
candidate_count: None,
|
candidate_count: None,
|
||||||
skipped_candidate_count: None,
|
skipped_candidate_count: None,
|
||||||
skip_reasons: std::collections::BTreeMap::new(),
|
skip_reasons: std::collections::BTreeMap::new(),
|
||||||
provider_hint_id: None,
|
|
||||||
provider_hint_name: None,
|
|
||||||
endpoint_hint_id: None,
|
|
||||||
endpoint_hint_api_format: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -428,20 +428,6 @@ impl<'a> PoolKeyCursor<'a> {
|
|||||||
if record_runtime_miss_diagnostic {
|
if record_runtime_miss_diagnostic {
|
||||||
self.runtime_miss_trace_id = Some(trace_id.to_string());
|
self.runtime_miss_trace_id = Some(trace_id.to_string());
|
||||||
self.record_runtime_miss_diagnostic = true;
|
self.record_runtime_miss_diagnostic = true;
|
||||||
let provider_id = self.group.candidate.provider_id.clone();
|
|
||||||
let provider_name = self.group.transport.provider.name.clone();
|
|
||||||
let endpoint_id = self.group.candidate.endpoint_id.clone();
|
|
||||||
let endpoint_api_format = self.group.provider_api_format.clone();
|
|
||||||
self.state
|
|
||||||
.app()
|
|
||||||
.mutate_local_execution_runtime_miss_diagnostic(trace_id, move |diagnostic| {
|
|
||||||
diagnostic.provider_hint_id.get_or_insert(provider_id);
|
|
||||||
diagnostic.provider_hint_name.get_or_insert(provider_name);
|
|
||||||
diagnostic.endpoint_hint_id.get_or_insert(endpoint_id);
|
|
||||||
diagnostic
|
|
||||||
.endpoint_hint_api_format
|
|
||||||
.get_or_insert(endpoint_api_format);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -2572,14 +2558,6 @@ mod tests {
|
|||||||
assert_eq!(diagnostic.reason, "all_candidates_skipped");
|
assert_eq!(diagnostic.reason, "all_candidates_skipped");
|
||||||
assert_eq!(diagnostic.skipped_candidate_count, Some(1));
|
assert_eq!(diagnostic.skipped_candidate_count, Some(1));
|
||||||
assert_eq!(diagnostic.skip_reasons.get("pool_cooldown"), Some(&1));
|
assert_eq!(diagnostic.skip_reasons.get("pool_cooldown"), Some(&1));
|
||||||
assert_eq!(
|
|
||||||
diagnostic.provider_hint_id.as_deref(),
|
|
||||||
Some("provider-pool")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
diagnostic.provider_hint_name.as_deref(),
|
|
||||||
Some("provider-pool")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -274,7 +274,6 @@ pub(crate) async fn record_failed_usage_for_exhausted_request(
|
|||||||
diagnostic,
|
diagnostic,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
0,
|
|
||||||
);
|
);
|
||||||
data.request_metadata = Some(Value::Object(request_metadata));
|
data.request_metadata = Some(Value::Object(request_metadata));
|
||||||
|
|
||||||
@@ -413,7 +412,6 @@ pub(crate) async fn record_failed_usage_for_runtime_miss_request(
|
|||||||
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()),
|
||||||
context.persisted_candidate_count(),
|
|
||||||
);
|
);
|
||||||
data.request_metadata =
|
data.request_metadata =
|
||||||
(!request_metadata.is_empty()).then_some(Value::Object(request_metadata));
|
(!request_metadata.is_empty()).then_some(Value::Object(request_metadata));
|
||||||
@@ -985,7 +983,6 @@ fn apply_runtime_miss_usage_routing(
|
|||||||
diagnostic: Option<&LocalExecutionRuntimeMissDiagnostic>,
|
diagnostic: Option<&LocalExecutionRuntimeMissDiagnostic>,
|
||||||
route_family_fallback: Option<&str>,
|
route_family_fallback: Option<&str>,
|
||||||
route_kind_fallback: Option<&str>,
|
route_kind_fallback: Option<&str>,
|
||||||
persisted_candidate_count: usize,
|
|
||||||
) {
|
) {
|
||||||
data.candidate_id = data
|
data.candidate_id = data
|
||||||
.candidate_id
|
.candidate_id
|
||||||
@@ -1024,68 +1021,7 @@ fn apply_runtime_miss_usage_routing(
|
|||||||
.planner_kind
|
.planner_kind
|
||||||
.clone()
|
.clone()
|
||||||
.or_else(|| trimmed_non_empty(diagnostic.and_then(|value| value.plan_kind.as_deref())));
|
.or_else(|| trimmed_non_empty(diagnostic.and_then(|value| value.plan_kind.as_deref())));
|
||||||
if let Some(diagnostic) = diagnostic {
|
let _ = request_metadata;
|
||||||
insert_runtime_miss_diagnostic_metadata(
|
|
||||||
request_metadata,
|
|
||||||
diagnostic,
|
|
||||||
persisted_candidate_count,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_runtime_miss_diagnostic_metadata(
|
|
||||||
request_metadata: &mut Map<String, Value>,
|
|
||||||
diagnostic: &LocalExecutionRuntimeMissDiagnostic,
|
|
||||||
persisted_candidate_count: usize,
|
|
||||||
) {
|
|
||||||
let mut runtime_miss = Map::new();
|
|
||||||
if let Some(count) = diagnostic.candidate_count {
|
|
||||||
runtime_miss.insert("candidate_count".to_string(), json!(count));
|
|
||||||
}
|
|
||||||
runtime_miss.insert(
|
|
||||||
"persisted_candidate_count".to_string(),
|
|
||||||
json!(persisted_candidate_count),
|
|
||||||
);
|
|
||||||
if let Some(count) = diagnostic.skipped_candidate_count {
|
|
||||||
runtime_miss.insert("skipped_candidate_count".to_string(), json!(count));
|
|
||||||
}
|
|
||||||
if !diagnostic.skip_reasons.is_empty() {
|
|
||||||
runtime_miss.insert("skip_reasons".to_string(), json!(diagnostic.skip_reasons));
|
|
||||||
}
|
|
||||||
if let Some(requested_model) = trimmed_non_empty(diagnostic.requested_model.as_deref()) {
|
|
||||||
runtime_miss.insert(
|
|
||||||
"requested_model".to_string(),
|
|
||||||
Value::String(requested_model),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut provider_hint = Map::new();
|
|
||||||
if let Some(provider_id) = trimmed_provider_hint(diagnostic.provider_hint_id.as_deref()) {
|
|
||||||
provider_hint.insert("id".to_string(), Value::String(provider_id));
|
|
||||||
}
|
|
||||||
if let Some(provider_name) = trimmed_provider_hint(diagnostic.provider_hint_name.as_deref()) {
|
|
||||||
provider_hint.insert("name".to_string(), Value::String(provider_name));
|
|
||||||
}
|
|
||||||
if !provider_hint.is_empty() {
|
|
||||||
runtime_miss.insert("provider_hint".to_string(), Value::Object(provider_hint));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut endpoint_hint = Map::new();
|
|
||||||
if let Some(endpoint_id) = trimmed_non_empty(diagnostic.endpoint_hint_id.as_deref()) {
|
|
||||||
endpoint_hint.insert("id".to_string(), Value::String(endpoint_id));
|
|
||||||
}
|
|
||||||
if let Some(endpoint_api_format) =
|
|
||||||
trimmed_non_empty(diagnostic.endpoint_hint_api_format.as_deref())
|
|
||||||
{
|
|
||||||
endpoint_hint.insert("api_format".to_string(), Value::String(endpoint_api_format));
|
|
||||||
}
|
|
||||||
if !endpoint_hint.is_empty() {
|
|
||||||
runtime_miss.insert("endpoint_hint".to_string(), Value::Object(endpoint_hint));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !runtime_miss.is_empty() {
|
|
||||||
request_metadata.insert("runtime_miss".to_string(), Value::Object(runtime_miss));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trimmed_non_empty(value: Option<&str>) -> Option<String> {
|
fn trimmed_non_empty(value: Option<&str>) -> Option<String> {
|
||||||
@@ -1095,15 +1031,6 @@ fn trimmed_non_empty(value: Option<&str>) -> Option<String> {
|
|||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trimmed_provider_hint(value: Option<&str>) -> Option<String> {
|
|
||||||
trimmed_non_empty(value).filter(|value| {
|
|
||||||
!matches!(
|
|
||||||
value.to_ascii_lowercase().as_str(),
|
|
||||||
"unknown" | "unknow" | "pending"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
@@ -1118,7 +1045,6 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use aether_usage_runtime::UsageEventData;
|
use aether_usage_runtime::UsageEventData;
|
||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_execution_client_error_message_is_client_friendly() {
|
fn local_execution_client_error_message_is_client_friendly() {
|
||||||
@@ -1143,7 +1069,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_miss_routing_records_compact_diagnostic_metadata() {
|
fn runtime_miss_routing_moves_to_typed_usage_fields_and_keeps_metadata_lightweight() {
|
||||||
let mut data = UsageEventData::default();
|
let mut data = UsageEventData::default();
|
||||||
let mut request_metadata =
|
let mut request_metadata =
|
||||||
Map::from_iter([("trace_id".to_string(), Value::String("trace-1".to_string()))]);
|
Map::from_iter([("trace_id".to_string(), Value::String("trace-1".to_string()))]);
|
||||||
@@ -1160,19 +1086,10 @@ mod tests {
|
|||||||
route_family: Some("claude".to_string()),
|
route_family: Some("claude".to_string()),
|
||||||
route_kind: Some("cli".to_string()),
|
route_kind: Some("cli".to_string()),
|
||||||
plan_kind: Some("claude_cli_sync".to_string()),
|
plan_kind: Some("claude_cli_sync".to_string()),
|
||||||
requested_model: Some("gpt-5".to_string()),
|
|
||||||
candidate_count: Some(1),
|
|
||||||
skipped_candidate_count: Some(4),
|
|
||||||
skip_reasons: BTreeMap::from([("pool_cooldown".to_string(), 4)]),
|
|
||||||
provider_hint_id: Some("provider-google-api".to_string()),
|
|
||||||
provider_hint_name: Some("Google API".to_string()),
|
|
||||||
endpoint_hint_id: Some("endpoint-gemini".to_string()),
|
|
||||||
endpoint_hint_api_format: Some("gemini:generate_content".to_string()),
|
|
||||||
..LocalExecutionRuntimeMissDiagnostic::default()
|
..LocalExecutionRuntimeMissDiagnostic::default()
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
0,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(data.candidate_id.as_deref(), Some("cand-1"));
|
assert_eq!(data.candidate_id.as_deref(), Some("cand-1"));
|
||||||
@@ -1192,24 +1109,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
Value::Object(request_metadata),
|
Value::Object(request_metadata),
|
||||||
json!({
|
json!({
|
||||||
"trace_id": "trace-1",
|
"trace_id": "trace-1"
|
||||||
"runtime_miss": {
|
|
||||||
"candidate_count": 1,
|
|
||||||
"persisted_candidate_count": 0,
|
|
||||||
"skipped_candidate_count": 4,
|
|
||||||
"skip_reasons": {
|
|
||||||
"pool_cooldown": 4
|
|
||||||
},
|
|
||||||
"provider_hint": {
|
|
||||||
"id": "provider-google-api",
|
|
||||||
"name": "Google API"
|
|
||||||
},
|
|
||||||
"endpoint_hint": {
|
|
||||||
"id": "endpoint-gemini",
|
|
||||||
"api_format": "gemini:generate_content"
|
|
||||||
},
|
|
||||||
"requested_model": "gpt-5"
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ pub(crate) struct LocalExecutionRuntimeMissDiagnostic {
|
|||||||
pub(crate) candidate_count: Option<usize>,
|
pub(crate) candidate_count: Option<usize>,
|
||||||
pub(crate) skipped_candidate_count: Option<usize>,
|
pub(crate) skipped_candidate_count: Option<usize>,
|
||||||
pub(crate) skip_reasons: std::collections::BTreeMap<String, usize>,
|
pub(crate) skip_reasons: std::collections::BTreeMap<String, usize>,
|
||||||
pub(crate) provider_hint_id: Option<String>,
|
|
||||||
pub(crate) provider_hint_name: Option<String>,
|
|
||||||
pub(crate) endpoint_hint_id: Option<String>,
|
|
||||||
pub(crate) endpoint_hint_api_format: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalExecutionRuntimeMissDiagnostic {
|
impl LocalExecutionRuntimeMissDiagnostic {
|
||||||
|
|||||||
@@ -492,60 +492,6 @@ fn admin_usage_extract_local_runtime_miss_reason_summary(message: &str) -> Optio
|
|||||||
(!summary.is_empty()).then(|| summary.to_string())
|
(!summary.is_empty()).then(|| summary.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn admin_usage_runtime_miss_metadata(
|
|
||||||
item: &StoredRequestUsageAudit,
|
|
||||||
) -> Option<&serde_json::Map<String, Value>> {
|
|
||||||
item.request_metadata
|
|
||||||
.as_ref()
|
|
||||||
.and_then(Value::as_object)
|
|
||||||
.and_then(|metadata| metadata.get("runtime_miss"))
|
|
||||||
.and_then(Value::as_object)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn admin_usage_json_value_has_content(value: &Value) -> bool {
|
|
||||||
match value {
|
|
||||||
Value::Null => false,
|
|
||||||
Value::String(value) => !value.trim().is_empty(),
|
|
||||||
Value::Array(value) => !value.is_empty(),
|
|
||||||
Value::Object(value) => !value.is_empty(),
|
|
||||||
Value::Bool(_) | Value::Number(_) => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn maybe_insert_runtime_miss_field(
|
|
||||||
object: &mut serde_json::Map<String, Value>,
|
|
||||||
runtime_miss: &serde_json::Map<String, Value>,
|
|
||||||
key: &str,
|
|
||||||
) {
|
|
||||||
let Some(value) = runtime_miss
|
|
||||||
.get(key)
|
|
||||||
.filter(|value| admin_usage_json_value_has_content(value))
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
object.insert(key.to_string(), value.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn admin_usage_insert_runtime_miss_fields(
|
|
||||||
object: &mut serde_json::Map<String, Value>,
|
|
||||||
item: &StoredRequestUsageAudit,
|
|
||||||
) {
|
|
||||||
let Some(runtime_miss) = admin_usage_runtime_miss_metadata(item) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
for key in [
|
|
||||||
"candidate_count",
|
|
||||||
"persisted_candidate_count",
|
|
||||||
"skipped_candidate_count",
|
|
||||||
"skip_reasons",
|
|
||||||
"requested_model",
|
|
||||||
"provider_hint",
|
|
||||||
"endpoint_hint",
|
|
||||||
] {
|
|
||||||
maybe_insert_runtime_miss_field(object, runtime_miss, key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn admin_usage_scheduling_failure_json(
|
fn admin_usage_scheduling_failure_json(
|
||||||
item: &StoredRequestUsageAudit,
|
item: &StoredRequestUsageAudit,
|
||||||
client_error: &Value,
|
client_error: &Value,
|
||||||
@@ -568,38 +514,20 @@ fn admin_usage_scheduling_failure_json(
|
|||||||
let reason_summary =
|
let reason_summary =
|
||||||
raw_message.and_then(admin_usage_extract_local_runtime_miss_reason_summary);
|
raw_message.and_then(admin_usage_extract_local_runtime_miss_reason_summary);
|
||||||
|
|
||||||
let mut object = serde_json::Map::new();
|
json!({
|
||||||
object.insert(
|
"source": "local_execution_runtime_miss",
|
||||||
"source".to_string(),
|
"reason": reason,
|
||||||
Value::String("local_execution_runtime_miss".to_string()),
|
"reason_label": admin_usage_local_runtime_miss_reason_label(reason),
|
||||||
);
|
"title": format!("本地调度失败:{}", admin_usage_local_runtime_miss_reason_label(reason)),
|
||||||
object.insert("reason".to_string(), Value::String(reason.to_string()));
|
"message": message,
|
||||||
object.insert(
|
"reason_summary": reason_summary,
|
||||||
"reason_label".to_string(),
|
"status_code": item.status_code,
|
||||||
Value::String(admin_usage_local_runtime_miss_reason_label(reason).to_string()),
|
"no_upstream_attempt": item.candidate_id.is_none()
|
||||||
);
|
&& item.provider_api_key_id.is_none()
|
||||||
object.insert(
|
&& item.provider_request_headers.is_none()
|
||||||
"title".to_string(),
|
&& item.provider_request_body.is_none()
|
||||||
Value::String(format!(
|
&& item.provider_request_body_ref.is_none(),
|
||||||
"本地调度失败:{}",
|
})
|
||||||
admin_usage_local_runtime_miss_reason_label(reason)
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
object.insert("message".to_string(), json!(message));
|
|
||||||
object.insert("reason_summary".to_string(), json!(reason_summary));
|
|
||||||
object.insert("status_code".to_string(), json!(item.status_code));
|
|
||||||
object.insert(
|
|
||||||
"no_upstream_attempt".to_string(),
|
|
||||||
json!(
|
|
||||||
item.candidate_id.is_none()
|
|
||||||
&& item.provider_api_key_id.is_none()
|
|
||||||
&& item.provider_request_headers.is_none()
|
|
||||||
&& item.provider_request_body.is_none()
|
|
||||||
&& item.provider_request_body_ref.is_none()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
admin_usage_insert_runtime_miss_fields(&mut object, item);
|
|
||||||
Value::Object(object)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn admin_usage_extract_local_execution_request_mode(message: &str) -> Option<&str> {
|
fn admin_usage_extract_local_execution_request_mode(message: &str) -> Option<&str> {
|
||||||
@@ -1234,7 +1162,6 @@ fn admin_usage_active_request_json(
|
|||||||
"request_path": admin_usage_metadata_string(item, "request_path"),
|
"request_path": admin_usage_metadata_string(item, "request_path"),
|
||||||
"request_path_and_query": admin_usage_metadata_string(item, "request_path_and_query"),
|
"request_path_and_query": admin_usage_metadata_string(item, "request_path_and_query"),
|
||||||
"has_fallback": admin_usage_has_fallback(item),
|
"has_fallback": admin_usage_has_fallback(item),
|
||||||
"scheduling_failure": admin_usage_scheduling_failure_json(item, &Value::Null),
|
|
||||||
});
|
});
|
||||||
if let Some(api_format) = item.api_format.as_ref() {
|
if let Some(api_format) = item.api_format.as_ref() {
|
||||||
value["api_format"] = json!(api_format);
|
value["api_format"] = json!(api_format);
|
||||||
@@ -1324,7 +1251,6 @@ pub fn admin_usage_record_json(
|
|||||||
"api_key_name": api_key_name,
|
"api_key_name": api_key_name,
|
||||||
"provider_key_name": provider_key_name,
|
"provider_key_name": provider_key_name,
|
||||||
"model_version": Value::Null,
|
"model_version": Value::Null,
|
||||||
"scheduling_failure": admin_usage_scheduling_failure_json(item, &Value::Null),
|
|
||||||
});
|
});
|
||||||
let object = payload
|
let object = payload
|
||||||
.as_object_mut()
|
.as_object_mut()
|
||||||
@@ -3295,122 +3221,6 @@ mod tests {
|
|||||||
assert_eq!(payload["scheduling_failure"]["no_upstream_attempt"], true);
|
assert_eq!(payload["scheduling_failure"]["no_upstream_attempt"], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn detail_payload_exposes_runtime_miss_context_inside_scheduling_failure() {
|
|
||||||
let message = "找到 1 个支持模型 gemma-4-31b-it 的候选提供商,但本次同步请求全部不可用:provider_quota_blocked 1 次(原因代码: all_candidates_skipped)";
|
|
||||||
let item = StoredRequestUsageAudit {
|
|
||||||
provider_name: "unknown".to_string(),
|
|
||||||
provider_id: None,
|
|
||||||
provider_endpoint_id: None,
|
|
||||||
provider_api_key_id: None,
|
|
||||||
provider_request_headers: None,
|
|
||||||
provider_request_body: None,
|
|
||||||
provider_request_body_ref: None,
|
|
||||||
candidate_id: None,
|
|
||||||
execution_path: Some("local_execution_runtime_miss".to_string()),
|
|
||||||
local_execution_runtime_miss_reason: Some("all_candidates_skipped".to_string()),
|
|
||||||
request_metadata: Some(json!({
|
|
||||||
"runtime_miss": {
|
|
||||||
"candidate_count": 1,
|
|
||||||
"persisted_candidate_count": 0,
|
|
||||||
"skipped_candidate_count": 1,
|
|
||||||
"skip_reasons": {
|
|
||||||
"provider_quota_blocked": 1
|
|
||||||
},
|
|
||||||
"requested_model": "gemma-4-31b-it",
|
|
||||||
"provider_hint": {
|
|
||||||
"id": "provider-google-api",
|
|
||||||
"name": "Google API"
|
|
||||||
},
|
|
||||||
"endpoint_hint": {
|
|
||||||
"id": "endpoint-gemini",
|
|
||||||
"api_format": "gemini:generate_content"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
..sample_usage("failed", Some(503), Some(message))
|
|
||||||
};
|
|
||||||
|
|
||||||
let payload = build_admin_usage_detail_payload(
|
|
||||||
&item,
|
|
||||||
&BTreeMap::new(),
|
|
||||||
&BTreeMap::new(),
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
Some(json!({"model": "gemma-4-31b-it"})),
|
|
||||||
&BTreeMap::new(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
payload["scheduling_failure"]["provider_hint"]["name"],
|
|
||||||
"Google API"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
payload["scheduling_failure"]["endpoint_hint"]["api_format"],
|
|
||||||
"gemini:generate_content"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
payload["scheduling_failure"]["requested_model"],
|
|
||||||
"gemma-4-31b-it"
|
|
||||||
);
|
|
||||||
assert_eq!(payload["scheduling_failure"]["candidate_count"], 1);
|
|
||||||
assert_eq!(
|
|
||||||
payload["scheduling_failure"]["persisted_candidate_count"],
|
|
||||||
0
|
|
||||||
);
|
|
||||||
assert_eq!(payload["scheduling_failure"]["skipped_candidate_count"], 1);
|
|
||||||
assert_eq!(
|
|
||||||
payload["scheduling_failure"]["skip_reasons"]["provider_quota_blocked"],
|
|
||||||
1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn record_json_exposes_scheduling_failure_for_local_runtime_miss_records() {
|
|
||||||
let message = "没有可用提供商支持模型 gemma-4-31b-it 的同步请求。请检查模型映射、端点启用状态和 API Key 权限(原因代码: candidate_list_empty)";
|
|
||||||
let item = StoredRequestUsageAudit {
|
|
||||||
provider_name: "unknown".to_string(),
|
|
||||||
provider_id: None,
|
|
||||||
provider_endpoint_id: None,
|
|
||||||
provider_api_key_id: None,
|
|
||||||
provider_request_headers: None,
|
|
||||||
provider_request_body: None,
|
|
||||||
provider_request_body_ref: None,
|
|
||||||
candidate_id: None,
|
|
||||||
execution_path: Some("local_execution_runtime_miss".to_string()),
|
|
||||||
local_execution_runtime_miss_reason: Some("candidate_list_empty".to_string()),
|
|
||||||
request_metadata: Some(json!({
|
|
||||||
"runtime_miss": {
|
|
||||||
"candidate_count": 0,
|
|
||||||
"persisted_candidate_count": 0,
|
|
||||||
"requested_model": "gemma-4-31b-it"
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
..sample_usage("failed", Some(503), Some(message))
|
|
||||||
};
|
|
||||||
|
|
||||||
let payload = admin_usage_record_json(
|
|
||||||
&item,
|
|
||||||
&BTreeMap::new(),
|
|
||||||
&BTreeMap::new(),
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
payload["scheduling_failure"]["title"],
|
|
||||||
"本地调度失败:没有可调度候选"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
payload["scheduling_failure"]["requested_model"],
|
|
||||||
"gemma-4-31b-it"
|
|
||||||
);
|
|
||||||
assert_eq!(payload["scheduling_failure"]["no_upstream_attempt"], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detail_payload_preserves_legacy_body_capture_metadata_keys() {
|
fn detail_payload_preserves_legacy_body_capture_metadata_keys() {
|
||||||
let item = StoredRequestUsageAudit {
|
let item = StoredRequestUsageAudit {
|
||||||
|
|||||||
@@ -151,19 +151,6 @@ export interface RequestSchedulingFailure {
|
|||||||
reason_summary?: string | null
|
reason_summary?: string | null
|
||||||
status_code?: number | null
|
status_code?: number | null
|
||||||
no_upstream_attempt?: boolean | null
|
no_upstream_attempt?: boolean | null
|
||||||
requested_model?: string | null
|
|
||||||
candidate_count?: number | null
|
|
||||||
persisted_candidate_count?: number | null
|
|
||||||
skipped_candidate_count?: number | null
|
|
||||||
skip_reasons?: Record<string, number> | null
|
|
||||||
provider_hint?: {
|
|
||||||
id?: string | null
|
|
||||||
name?: string | null
|
|
||||||
} | null
|
|
||||||
endpoint_hint?: {
|
|
||||||
id?: string | null
|
|
||||||
api_format?: string | null
|
|
||||||
} | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestDetail {
|
export interface RequestDetail {
|
||||||
|
|||||||
@@ -547,38 +547,6 @@
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Local Scheduling Failure State -->
|
|
||||||
<Card
|
|
||||||
v-else-if="schedulingFailureNotice"
|
|
||||||
class="border-red-200 dark:border-red-800"
|
|
||||||
>
|
|
||||||
<div class="p-4 space-y-2">
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<Badge variant="destructive">
|
|
||||||
调度失败
|
|
||||||
</Badge>
|
|
||||||
<h4 class="text-sm font-semibold text-red-950 dark:text-red-100">
|
|
||||||
{{ schedulingFailureNotice.title }}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm leading-6 text-red-900 dark:text-red-100">
|
|
||||||
{{ schedulingFailureNotice.message }}
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
v-if="schedulingFailureNotice.meta.length > 0"
|
|
||||||
class="flex flex-wrap gap-1.5"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-for="item in schedulingFailureNotice.meta"
|
|
||||||
:key="item"
|
|
||||||
class="rounded-full border border-red-200 bg-white/70 px-2 py-0.5 text-[11px] font-mono text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-200"
|
|
||||||
>
|
|
||||||
{{ item }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<Card
|
<Card
|
||||||
v-else
|
v-else
|
||||||
@@ -607,7 +575,6 @@ import { parseApiError } from '@/utils/errorParser'
|
|||||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
import { useDarkMode } from '@/composables/useDarkMode'
|
import { useDarkMode } from '@/composables/useDarkMode'
|
||||||
import { resolveTimelineFinalStatus } from '../utils/status'
|
import { resolveTimelineFinalStatus } from '../utils/status'
|
||||||
import type { RequestSchedulingFailure } from '@/api/dashboard'
|
|
||||||
import {
|
import {
|
||||||
buildPoolGroupVisibleAttempts,
|
buildPoolGroupVisibleAttempts,
|
||||||
buildPoolParticipatedCandidates,
|
buildPoolParticipatedCandidates,
|
||||||
@@ -670,8 +637,6 @@ const props = defineProps<{
|
|||||||
usageData?: UsageData | null
|
usageData?: UsageData | null
|
||||||
/** 请求元数据(用于号池调度组装) */
|
/** 请求元数据(用于号池调度组装) */
|
||||||
requestMetadata?: Record<string, unknown> | null
|
requestMetadata?: Record<string, unknown> | null
|
||||||
/** 本地调度失败摘要;用于没有 trace candidate 时替代空态 */
|
|
||||||
schedulingFailure?: RequestSchedulingFailure | null
|
|
||||||
/** 已获取的追踪数据;传入时不再内部拉取 */
|
/** 已获取的追踪数据;传入时不再内部拉取 */
|
||||||
traceData?: RequestTrace | null
|
traceData?: RequestTrace | null
|
||||||
}>()
|
}>()
|
||||||
@@ -847,62 +812,6 @@ const computedFinalStatus = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const nonEmptyNoticeString = (value: string | null | undefined): string | null => {
|
|
||||||
const trimmed = value?.trim()
|
|
||||||
return trimmed ? trimmed : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueNoticeMeta = (values: Array<string | null | undefined>): string[] => {
|
|
||||||
return Array.from(new Set(values.map(value => value?.trim()).filter((value): value is string => Boolean(value))))
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUnknownProviderHint = (value: string | null): boolean => {
|
|
||||||
const normalized = value?.trim().toLowerCase()
|
|
||||||
return !normalized || ['unknown', 'unknow', 'pending'].includes(normalized)
|
|
||||||
}
|
|
||||||
|
|
||||||
const schedulingFailureProviderHint = (failure: RequestSchedulingFailure): string | null => {
|
|
||||||
const name = nonEmptyNoticeString(failure.provider_hint?.name)
|
|
||||||
if (name && !isUnknownProviderHint(name)) return name
|
|
||||||
|
|
||||||
const id = nonEmptyNoticeString(failure.provider_hint?.id)
|
|
||||||
if (id && !isUnknownProviderHint(id)) return id
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const schedulingFailureEndpointHint = (failure: RequestSchedulingFailure): string | null => {
|
|
||||||
return nonEmptyNoticeString(failure.endpoint_hint?.api_format)
|
|
||||||
?? nonEmptyNoticeString(failure.endpoint_hint?.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const schedulingFailureNotice = computed(() => {
|
|
||||||
const failure = props.schedulingFailure
|
|
||||||
if (!failure) return null
|
|
||||||
|
|
||||||
const title = nonEmptyNoticeString(failure.title) ?? '本地调度失败'
|
|
||||||
const message = nonEmptyNoticeString(failure.message)
|
|
||||||
?? nonEmptyNoticeString(failure.reason_summary)
|
|
||||||
?? nonEmptyNoticeString(failure.reason_label)
|
|
||||||
?? nonEmptyNoticeString(failure.reason)
|
|
||||||
?? '本地调度阶段没有选出可用上游提供商'
|
|
||||||
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
meta: uniqueNoticeMeta([
|
|
||||||
schedulingFailureProviderHint(failure),
|
|
||||||
schedulingFailureEndpointHint(failure),
|
|
||||||
nonEmptyNoticeString(failure.requested_model),
|
|
||||||
nonEmptyNoticeString(failure.reason_summary),
|
|
||||||
nonEmptyNoticeString(failure.reason_label),
|
|
||||||
nonEmptyNoticeString(failure.reason),
|
|
||||||
typeof failure.status_code === 'number' ? `HTTP ${failure.status_code}` : null,
|
|
||||||
failure.no_upstream_attempt ? '未进入上游执行' : null,
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const compareBySchedulingOrder = (a: CandidateRecord, b: CandidateRecord): number => {
|
const compareBySchedulingOrder = (a: CandidateRecord, b: CandidateRecord): number => {
|
||||||
if (a.candidate_index !== b.candidate_index) {
|
if (a.candidate_index !== b.candidate_index) {
|
||||||
return a.candidate_index - b.candidate_index
|
return a.candidate_index - b.candidate_index
|
||||||
|
|||||||
@@ -544,7 +544,6 @@
|
|||||||
:request-status="detail.status"
|
:request-status="detail.status"
|
||||||
:request-api-format="detail.api_format || null"
|
:request-api-format="detail.api_format || null"
|
||||||
:request-metadata="traceRequestMetadata"
|
:request-metadata="traceRequestMetadata"
|
||||||
:scheduling-failure="detail.scheduling_failure"
|
|
||||||
@trace-state="handleTraceState"
|
@trace-state="handleTraceState"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -584,18 +584,15 @@
|
|||||||
>
|
>
|
||||||
<div class="flex min-w-0 items-center gap-1">
|
<div class="flex min-w-0 items-center gap-1">
|
||||||
<div class="flex min-w-0 flex-col text-xs gap-0.5">
|
<div class="flex min-w-0 flex-col text-xs gap-0.5">
|
||||||
|
<span class="truncate">{{ record.provider }}</span>
|
||||||
<span
|
<span
|
||||||
class="truncate"
|
v-if="record.provider_key_name"
|
||||||
:title="getRecordProviderTitle(record)"
|
|
||||||
>{{ getRecordProviderDisplay(record) }}</span>
|
|
||||||
<span
|
|
||||||
v-if="getRecordProviderSecondaryText(record)"
|
|
||||||
class="text-muted-foreground truncate"
|
class="text-muted-foreground truncate"
|
||||||
:title="getRecordProviderSecondaryTitle(record)"
|
:title="record.provider_key_name"
|
||||||
>
|
>
|
||||||
{{ getRecordProviderSecondaryText(record) }}
|
{{ record.provider_key_name }}
|
||||||
<span
|
<span
|
||||||
v-if="record.provider_key_name && record.rate_multiplier && record.rate_multiplier !== 1.0"
|
v-if="record.rate_multiplier && record.rate_multiplier !== 1.0"
|
||||||
class="text-foreground/60"
|
class="text-foreground/60"
|
||||||
>({{ record.rate_multiplier }}x)</span>
|
>({{ record.rate_multiplier }}x)</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -1177,57 +1174,6 @@ function getDisplayStatus(record: UsageRecord) {
|
|||||||
return resolveDisplayRequestStatus(record)
|
return resolveDisplayRequestStatus(record)
|
||||||
}
|
}
|
||||||
|
|
||||||
function nonEmptyDisplayString(value: string | null | undefined): string | null {
|
|
||||||
const trimmed = value?.trim()
|
|
||||||
return trimmed ? trimmed : null
|
|
||||||
}
|
|
||||||
|
|
||||||
function isUnknownProvider(value: string | null | undefined): boolean {
|
|
||||||
const normalized = value?.trim().toLowerCase()
|
|
||||||
return !normalized || ['unknown', 'unknow', 'pending'].includes(normalized)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecordSchedulingProviderHint(record: UsageRecord): string | null {
|
|
||||||
const name = nonEmptyDisplayString(record.scheduling_failure?.provider_hint?.name)
|
|
||||||
if (name && !isUnknownProvider(name)) return name
|
|
||||||
|
|
||||||
const id = nonEmptyDisplayString(record.scheduling_failure?.provider_hint?.id)
|
|
||||||
if (id && !isUnknownProvider(id)) return id
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecordProviderDisplay(record: UsageRecord): string {
|
|
||||||
const provider = nonEmptyDisplayString(record.provider)
|
|
||||||
if (provider && !isUnknownProvider(provider)) return provider
|
|
||||||
|
|
||||||
const providerHint = getRecordSchedulingProviderHint(record)
|
|
||||||
if (providerHint) return providerHint
|
|
||||||
|
|
||||||
if (record.scheduling_failure) return '未选定提供商'
|
|
||||||
return provider ?? 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecordProviderTitle(record: UsageRecord): string {
|
|
||||||
const schedulingMessage = nonEmptyDisplayString(record.scheduling_failure?.message)
|
|
||||||
const providerDisplay = getRecordProviderDisplay(record)
|
|
||||||
return schedulingMessage ? `${providerDisplay}\n${schedulingMessage}` : providerDisplay
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecordProviderSecondaryText(record: UsageRecord): string | null {
|
|
||||||
return nonEmptyDisplayString(record.provider_key_name)
|
|
||||||
?? nonEmptyDisplayString(record.scheduling_failure?.reason_label)
|
|
||||||
?? nonEmptyDisplayString(record.scheduling_failure?.reason_summary)
|
|
||||||
?? nonEmptyDisplayString(record.scheduling_failure?.reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecordProviderSecondaryTitle(record: UsageRecord): string | undefined {
|
|
||||||
return nonEmptyDisplayString(record.provider_key_name)
|
|
||||||
?? nonEmptyDisplayString(record.scheduling_failure?.message)
|
|
||||||
?? getRecordProviderSecondaryText(record)
|
|
||||||
?? undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStreamModeLabel(record: UsageRecord): string {
|
function getStreamModeLabel(record: UsageRecord): string {
|
||||||
return formatUsageStreamLabel(record)
|
return formatUsageStreamLabel(record)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -350,40 +350,6 @@ describe('HorizontalRequestTimeline', () => {
|
|||||||
expect(nodeDot?.classList.contains('status-success')).toBe(false)
|
expect(nodeDot?.classList.contains('status-success')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows scheduling failure context instead of an empty trace state when no candidates exist', async () => {
|
|
||||||
const trace = buildTrace([])
|
|
||||||
trace.final_status = 'failed'
|
|
||||||
|
|
||||||
const root = mountTimeline(trace, {
|
|
||||||
requestStatus: 'failed',
|
|
||||||
overrideStatusCode: 503,
|
|
||||||
schedulingFailure: {
|
|
||||||
source: 'local_execution_runtime_miss',
|
|
||||||
reason: 'all_candidates_skipped',
|
|
||||||
reason_label: '所有候选均被跳过',
|
|
||||||
title: '本地调度失败:所有候选均被跳过',
|
|
||||||
message: '没有可用提供商支持模型 gemma-4-31b-it 的同步请求',
|
|
||||||
status_code: 503,
|
|
||||||
no_upstream_attempt: true,
|
|
||||||
provider_hint: {
|
|
||||||
id: 'provider-google-api',
|
|
||||||
name: 'Google API',
|
|
||||||
},
|
|
||||||
endpoint_hint: {
|
|
||||||
id: 'endpoint-gemini',
|
|
||||||
api_format: 'gemini:generate_content',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
expect(root.textContent).toContain('本地调度失败:所有候选均被跳过')
|
|
||||||
expect(root.textContent).toContain('没有可用提供商支持模型 gemma-4-31b-it 的同步请求')
|
|
||||||
expect(root.textContent).toContain('Google API')
|
|
||||||
expect(root.textContent).toContain('gemini:generate_content')
|
|
||||||
expect(root.textContent).not.toContain('暂无追踪数据')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('keeps emitted trace state active while the request lifecycle is still streaming', async () => {
|
it('keeps emitted trace state active while the request lifecycle is still streaming', async () => {
|
||||||
const onTraceState = vi.fn()
|
const onTraceState = vi.fn()
|
||||||
const trace = buildTrace([
|
const trace = buildTrace([
|
||||||
|
|||||||
@@ -255,31 +255,6 @@ describe('UsageRecordsTable', () => {
|
|||||||
expect(root.textContent).not.toContain('等待中')
|
expect(root.textContent).not.toContain('等待中')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses scheduling failure provider hints instead of rendering unknown provider', () => {
|
|
||||||
const root = mountUsageRecordsTable([buildRecord({
|
|
||||||
provider: 'unknown',
|
|
||||||
status: 'failed',
|
|
||||||
status_code: 503,
|
|
||||||
scheduling_failure: {
|
|
||||||
source: 'local_execution_runtime_miss',
|
|
||||||
reason: 'all_candidates_skipped',
|
|
||||||
reason_label: '所有候选均被跳过',
|
|
||||||
title: '本地调度失败:所有候选均被跳过',
|
|
||||||
message: '没有可用提供商支持模型 gemma-4-31b-it 的同步请求',
|
|
||||||
status_code: 503,
|
|
||||||
no_upstream_attempt: true,
|
|
||||||
provider_hint: {
|
|
||||||
id: 'provider-google-api',
|
|
||||||
name: 'Google API',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Partial<UsageRecord>)])
|
|
||||||
|
|
||||||
expect(root.textContent).toContain('Google API')
|
|
||||||
expect(root.textContent).toContain('所有候选均被跳过')
|
|
||||||
expect(root.textContent).not.toContain('unknown')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders output TPS in the non-admin usage table', () => {
|
it('renders output TPS in the non-admin usage table', () => {
|
||||||
const root = mountUsageRecordsTable([buildRecord()], { isAdmin: false })
|
const root = mountUsageRecordsTable([buildRecord()], { isAdmin: false })
|
||||||
|
|
||||||
|
|||||||
@@ -515,8 +515,7 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
api_key_name: existing.api_key_name || record.api_key_name,
|
api_key_name: existing.api_key_name || record.api_key_name,
|
||||||
provider_key_name: existing.provider_key_name || record.provider_key_name,
|
provider_key_name: existing.provider_key_name || record.provider_key_name,
|
||||||
rate_multiplier: existing.rate_multiplier ?? record.rate_multiplier,
|
rate_multiplier: existing.rate_multiplier ?? record.rate_multiplier,
|
||||||
target_model: existing.target_model || record.target_model,
|
target_model: existing.target_model || record.target_model
|
||||||
scheduling_failure: existing.scheduling_failure ?? record.scheduling_failure
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,8 +523,7 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
if (protectProvider) {
|
if (protectProvider) {
|
||||||
return {
|
return {
|
||||||
...record,
|
...record,
|
||||||
provider: existing.provider,
|
provider: existing.provider
|
||||||
scheduling_failure: existing.scheduling_failure ?? record.scheduling_failure
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { ImageProgress } from '@/api/requestTrace'
|
import type { ImageProgress } from '@/api/requestTrace'
|
||||||
import type { RequestSchedulingFailure } from '@/api/dashboard'
|
|
||||||
|
|
||||||
// 统计数据状态
|
// 统计数据状态
|
||||||
export interface UsageStatsState {
|
export interface UsageStatsState {
|
||||||
@@ -121,7 +120,6 @@ export interface UsageRecord {
|
|||||||
status_code?: number
|
status_code?: number
|
||||||
error_message?: string
|
error_message?: string
|
||||||
status?: RequestStatus // 请求状态: pending, streaming, completed, failed
|
status?: RequestStatus // 请求状态: pending, streaming, completed, failed
|
||||||
scheduling_failure?: RequestSchedulingFailure | null
|
|
||||||
created_at: string
|
created_at: string
|
||||||
has_fallback?: boolean
|
has_fallback?: boolean
|
||||||
has_retry?: boolean
|
has_retry?: boolean
|
||||||
|
|||||||
@@ -73,43 +73,6 @@ describe('request failure notice', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('includes scheduling failure provider and endpoint hints in metadata', () => {
|
|
||||||
const notice = resolveRequestFailureNotice(buildRequestDetail({
|
|
||||||
failure_summary: {
|
|
||||||
status_code: 503,
|
|
||||||
message: '没有可用提供商支持模型 gemma-4-31b-it 的同步请求',
|
|
||||||
},
|
|
||||||
scheduling_failure: {
|
|
||||||
source: 'local_execution_runtime_miss',
|
|
||||||
reason: 'all_candidates_skipped',
|
|
||||||
reason_label: '所有候选均被跳过',
|
|
||||||
title: '本地调度失败:所有候选均被跳过',
|
|
||||||
message: '没有可用提供商支持模型 gemma-4-31b-it 的同步请求',
|
|
||||||
status_code: 503,
|
|
||||||
no_upstream_attempt: true,
|
|
||||||
requested_model: 'gemma-4-31b-it',
|
|
||||||
provider_hint: {
|
|
||||||
id: 'provider-google-api',
|
|
||||||
name: 'Google API',
|
|
||||||
},
|
|
||||||
endpoint_hint: {
|
|
||||||
id: 'endpoint-gemini',
|
|
||||||
api_format: 'gemini:generate_content',
|
|
||||||
},
|
|
||||||
} as NonNullable<RequestDetail['scheduling_failure']>,
|
|
||||||
}))
|
|
||||||
|
|
||||||
expect(notice?.meta).toEqual([
|
|
||||||
'Google API',
|
|
||||||
'gemini:generate_content',
|
|
||||||
'gemma-4-31b-it',
|
|
||||||
'所有候选均被跳过',
|
|
||||||
'all_candidates_skipped',
|
|
||||||
'HTTP 503',
|
|
||||||
'未进入上游执行',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('falls back to the failure summary for upstream failures', () => {
|
it('falls back to the failure summary for upstream failures', () => {
|
||||||
const notice = resolveRequestFailureNotice(buildRequestDetail({
|
const notice = resolveRequestFailureNotice(buildRequestDetail({
|
||||||
failure_summary: {
|
failure_summary: {
|
||||||
|
|||||||
@@ -25,11 +25,6 @@ function uniqueMeta(values: Array<string | null | undefined>): string[] {
|
|||||||
return Array.from(new Set(values.map(value => value?.trim()).filter((value): value is string => Boolean(value))))
|
return Array.from(new Set(values.map(value => value?.trim()).filter((value): value is string => Boolean(value))))
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUnknownProviderHint(value: string | null): boolean {
|
|
||||||
const normalized = value?.trim().toLowerCase()
|
|
||||||
return !normalized || ['unknown', 'unknow', 'pending'].includes(normalized)
|
|
||||||
}
|
|
||||||
|
|
||||||
function schedulingFailureMessage(
|
function schedulingFailureMessage(
|
||||||
failure: RequestSchedulingFailure,
|
failure: RequestSchedulingFailure,
|
||||||
fallbackDomain: RequestErrorDomain | null,
|
fallbackDomain: RequestErrorDomain | null,
|
||||||
@@ -42,21 +37,6 @@ function schedulingFailureMessage(
|
|||||||
?? nonEmptyString(failure.reason)
|
?? nonEmptyString(failure.reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
function schedulingFailureProviderHint(failure: RequestSchedulingFailure): string | null {
|
|
||||||
const name = nonEmptyString(failure.provider_hint?.name)
|
|
||||||
if (name && !isUnknownProviderHint(name)) return name
|
|
||||||
|
|
||||||
const id = nonEmptyString(failure.provider_hint?.id)
|
|
||||||
if (id && !isUnknownProviderHint(id)) return id
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function schedulingFailureEndpointHint(failure: RequestSchedulingFailure): string | null {
|
|
||||||
return nonEmptyString(failure.endpoint_hint?.api_format)
|
|
||||||
?? nonEmptyString(failure.endpoint_hint?.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveRequestFailureNotice(detail: RequestDetail | null | undefined): RequestFailureNotice | null {
|
export function resolveRequestFailureNotice(detail: RequestDetail | null | undefined): RequestFailureNotice | null {
|
||||||
if (!detail) return null
|
if (!detail) return null
|
||||||
|
|
||||||
@@ -75,9 +55,6 @@ export function resolveRequestFailureNotice(detail: RequestDetail | null | undef
|
|||||||
message,
|
message,
|
||||||
isSchedulingFailure: true,
|
isSchedulingFailure: true,
|
||||||
meta: uniqueMeta([
|
meta: uniqueMeta([
|
||||||
schedulingFailureProviderHint(schedulingFailure),
|
|
||||||
schedulingFailureEndpointHint(schedulingFailure),
|
|
||||||
nonEmptyString(schedulingFailure.requested_model),
|
|
||||||
nonEmptyString(schedulingFailure.reason_summary),
|
nonEmptyString(schedulingFailure.reason_summary),
|
||||||
nonEmptyString(schedulingFailure.reason_label),
|
nonEmptyString(schedulingFailure.reason_label),
|
||||||
nonEmptyString(schedulingFailure.reason),
|
nonEmptyString(schedulingFailure.reason),
|
||||||
|
|||||||
Reference in New Issue
Block a user