mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00: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,
|
||||
skipped_candidate_count: None,
|
||||
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 {
|
||||
self.runtime_miss_trace_id = Some(trace_id.to_string());
|
||||
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
|
||||
}
|
||||
@@ -2572,14 +2558,6 @@ mod tests {
|
||||
assert_eq!(diagnostic.reason, "all_candidates_skipped");
|
||||
assert_eq!(diagnostic.skipped_candidate_count, 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]
|
||||
|
||||
@@ -274,7 +274,6 @@ pub(crate) async fn record_failed_usage_for_exhausted_request(
|
||||
diagnostic,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
data.request_metadata = Some(Value::Object(request_metadata));
|
||||
|
||||
@@ -413,7 +412,6 @@ pub(crate) async fn record_failed_usage_for_runtime_miss_request(
|
||||
diagnostic,
|
||||
decision.and_then(|value| value.route_family.as_deref()),
|
||||
decision.and_then(|value| value.route_kind.as_deref()),
|
||||
context.persisted_candidate_count(),
|
||||
);
|
||||
data.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>,
|
||||
route_family_fallback: Option<&str>,
|
||||
route_kind_fallback: Option<&str>,
|
||||
persisted_candidate_count: usize,
|
||||
) {
|
||||
data.candidate_id = data
|
||||
.candidate_id
|
||||
@@ -1024,68 +1021,7 @@ fn apply_runtime_miss_usage_routing(
|
||||
.planner_kind
|
||||
.clone()
|
||||
.or_else(|| trimmed_non_empty(diagnostic.and_then(|value| value.plan_kind.as_deref())));
|
||||
if let Some(diagnostic) = diagnostic {
|
||||
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));
|
||||
}
|
||||
let _ = request_metadata;
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
use super::{
|
||||
@@ -1118,7 +1045,6 @@ mod tests {
|
||||
};
|
||||
use aether_usage_runtime::UsageEventData;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn local_execution_client_error_message_is_client_friendly() {
|
||||
@@ -1143,7 +1069,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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 request_metadata =
|
||||
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_kind: Some("cli".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()
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(data.candidate_id.as_deref(), Some("cand-1"));
|
||||
@@ -1192,24 +1109,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
Value::Object(request_metadata),
|
||||
json!({
|
||||
"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"
|
||||
}
|
||||
"trace_id": "trace-1"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,10 +30,6 @@ pub(crate) struct LocalExecutionRuntimeMissDiagnostic {
|
||||
pub(crate) candidate_count: Option<usize>,
|
||||
pub(crate) skipped_candidate_count: Option<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 {
|
||||
|
||||
@@ -492,60 +492,6 @@ fn admin_usage_extract_local_runtime_miss_reason_summary(message: &str) -> Optio
|
||||
(!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(
|
||||
item: &StoredRequestUsageAudit,
|
||||
client_error: &Value,
|
||||
@@ -568,38 +514,20 @@ fn admin_usage_scheduling_failure_json(
|
||||
let reason_summary =
|
||||
raw_message.and_then(admin_usage_extract_local_runtime_miss_reason_summary);
|
||||
|
||||
let mut object = serde_json::Map::new();
|
||||
object.insert(
|
||||
"source".to_string(),
|
||||
Value::String("local_execution_runtime_miss".to_string()),
|
||||
);
|
||||
object.insert("reason".to_string(), Value::String(reason.to_string()));
|
||||
object.insert(
|
||||
"reason_label".to_string(),
|
||||
Value::String(admin_usage_local_runtime_miss_reason_label(reason).to_string()),
|
||||
);
|
||||
object.insert(
|
||||
"title".to_string(),
|
||||
Value::String(format!(
|
||||
"本地调度失败:{}",
|
||||
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)
|
||||
json!({
|
||||
"source": "local_execution_runtime_miss",
|
||||
"reason": reason,
|
||||
"reason_label": admin_usage_local_runtime_miss_reason_label(reason),
|
||||
"title": format!("本地调度失败:{}", admin_usage_local_runtime_miss_reason_label(reason)),
|
||||
"message": message,
|
||||
"reason_summary": reason_summary,
|
||||
"status_code": item.status_code,
|
||||
"no_upstream_attempt": 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(),
|
||||
})
|
||||
}
|
||||
|
||||
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_and_query": admin_usage_metadata_string(item, "request_path_and_query"),
|
||||
"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() {
|
||||
value["api_format"] = json!(api_format);
|
||||
@@ -1324,7 +1251,6 @@ pub fn admin_usage_record_json(
|
||||
"api_key_name": api_key_name,
|
||||
"provider_key_name": provider_key_name,
|
||||
"model_version": Value::Null,
|
||||
"scheduling_failure": admin_usage_scheduling_failure_json(item, &Value::Null),
|
||||
});
|
||||
let object = payload
|
||||
.as_object_mut()
|
||||
@@ -3295,122 +3221,6 @@ mod tests {
|
||||
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]
|
||||
fn detail_payload_preserves_legacy_body_capture_metadata_keys() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
|
||||
@@ -151,19 +151,6 @@ export interface RequestSchedulingFailure {
|
||||
reason_summary?: string | null
|
||||
status_code?: number | 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 {
|
||||
|
||||
@@ -547,38 +547,6 @@
|
||||
</Card>
|
||||
</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 -->
|
||||
<Card
|
||||
v-else
|
||||
@@ -607,7 +575,6 @@ import { parseApiError } from '@/utils/errorParser'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { useDarkMode } from '@/composables/useDarkMode'
|
||||
import { resolveTimelineFinalStatus } from '../utils/status'
|
||||
import type { RequestSchedulingFailure } from '@/api/dashboard'
|
||||
import {
|
||||
buildPoolGroupVisibleAttempts,
|
||||
buildPoolParticipatedCandidates,
|
||||
@@ -670,8 +637,6 @@ const props = defineProps<{
|
||||
usageData?: UsageData | null
|
||||
/** 请求元数据(用于号池调度组装) */
|
||||
requestMetadata?: Record<string, unknown> | null
|
||||
/** 本地调度失败摘要;用于没有 trace candidate 时替代空态 */
|
||||
schedulingFailure?: RequestSchedulingFailure | 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 => {
|
||||
if (a.candidate_index !== b.candidate_index) {
|
||||
return a.candidate_index - b.candidate_index
|
||||
|
||||
@@ -544,7 +544,6 @@
|
||||
:request-status="detail.status"
|
||||
:request-api-format="detail.api_format || null"
|
||||
:request-metadata="traceRequestMetadata"
|
||||
:scheduling-failure="detail.scheduling_failure"
|
||||
@trace-state="handleTraceState"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -584,18 +584,15 @@
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<div class="flex min-w-0 flex-col text-xs gap-0.5">
|
||||
<span class="truncate">{{ record.provider }}</span>
|
||||
<span
|
||||
class="truncate"
|
||||
:title="getRecordProviderTitle(record)"
|
||||
>{{ getRecordProviderDisplay(record) }}</span>
|
||||
<span
|
||||
v-if="getRecordProviderSecondaryText(record)"
|
||||
v-if="record.provider_key_name"
|
||||
class="text-muted-foreground truncate"
|
||||
:title="getRecordProviderSecondaryTitle(record)"
|
||||
:title="record.provider_key_name"
|
||||
>
|
||||
{{ getRecordProviderSecondaryText(record) }}
|
||||
{{ record.provider_key_name }}
|
||||
<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"
|
||||
>({{ record.rate_multiplier }}x)</span>
|
||||
</span>
|
||||
@@ -1177,57 +1174,6 @@ function getDisplayStatus(record: UsageRecord) {
|
||||
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 {
|
||||
return formatUsageStreamLabel(record)
|
||||
}
|
||||
|
||||
@@ -350,40 +350,6 @@ describe('HorizontalRequestTimeline', () => {
|
||||
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 () => {
|
||||
const onTraceState = vi.fn()
|
||||
const trace = buildTrace([
|
||||
|
||||
@@ -255,31 +255,6 @@ describe('UsageRecordsTable', () => {
|
||||
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', () => {
|
||||
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,
|
||||
provider_key_name: existing.provider_key_name || record.provider_key_name,
|
||||
rate_multiplier: existing.rate_multiplier ?? record.rate_multiplier,
|
||||
target_model: existing.target_model || record.target_model,
|
||||
scheduling_failure: existing.scheduling_failure ?? record.scheduling_failure
|
||||
target_model: existing.target_model || record.target_model
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,8 +523,7 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
if (protectProvider) {
|
||||
return {
|
||||
...record,
|
||||
provider: existing.provider,
|
||||
scheduling_failure: existing.scheduling_failure ?? record.scheduling_failure
|
||||
provider: existing.provider
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ImageProgress } from '@/api/requestTrace'
|
||||
import type { RequestSchedulingFailure } from '@/api/dashboard'
|
||||
|
||||
// 统计数据状态
|
||||
export interface UsageStatsState {
|
||||
@@ -121,7 +120,6 @@ export interface UsageRecord {
|
||||
status_code?: number
|
||||
error_message?: string
|
||||
status?: RequestStatus // 请求状态: pending, streaming, completed, failed
|
||||
scheduling_failure?: RequestSchedulingFailure | null
|
||||
created_at: string
|
||||
has_fallback?: 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', () => {
|
||||
const notice = resolveRequestFailureNotice(buildRequestDetail({
|
||||
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))))
|
||||
}
|
||||
|
||||
function isUnknownProviderHint(value: string | null): boolean {
|
||||
const normalized = value?.trim().toLowerCase()
|
||||
return !normalized || ['unknown', 'unknow', 'pending'].includes(normalized)
|
||||
}
|
||||
|
||||
function schedulingFailureMessage(
|
||||
failure: RequestSchedulingFailure,
|
||||
fallbackDomain: RequestErrorDomain | null,
|
||||
@@ -42,21 +37,6 @@ function schedulingFailureMessage(
|
||||
?? 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 {
|
||||
if (!detail) return null
|
||||
|
||||
@@ -75,9 +55,6 @@ export function resolveRequestFailureNotice(detail: RequestDetail | null | undef
|
||||
message,
|
||||
isSchedulingFailure: true,
|
||||
meta: uniqueMeta([
|
||||
schedulingFailureProviderHint(schedulingFailure),
|
||||
schedulingFailureEndpointHint(schedulingFailure),
|
||||
nonEmptyString(schedulingFailure.requested_model),
|
||||
nonEmptyString(schedulingFailure.reason_summary),
|
||||
nonEmptyString(schedulingFailure.reason_label),
|
||||
nonEmptyString(schedulingFailure.reason),
|
||||
|
||||
Reference in New Issue
Block a user