Fix reasoning model directive response identity

This commit is contained in:
fawney19
2026-05-11 22:48:44 +08:00
parent 7ae38b6c43
commit 0fa97595bf
9 changed files with 465 additions and 25 deletions

View File

@@ -267,6 +267,9 @@ where
self.state,
self.trace_id,
self.persistence_policy.available,
self.persistence_policy
.skipped
.record_runtime_miss_diagnostic,
candidates,
self.sticky_session_token,
self.requested_model,
@@ -528,6 +531,8 @@ where
state,
candidates,
0,
Some(trace_id),
persistence_policy.skipped.record_runtime_miss_diagnostic,
sticky_session_token,
requested_model,
request_auth_channel,
@@ -543,6 +548,8 @@ fn build_logical_candidate_items<'a>(
state: PlannerAppState<'a>,
candidates: Vec<EligibleLocalExecutionCandidate>,
starting_candidate_index: u32,
trace_id: Option<&str>,
record_runtime_miss_diagnostic: bool,
sticky_session_token: Option<&str>,
requested_model: Option<&str>,
request_auth_channel: Option<&str>,
@@ -563,14 +570,20 @@ fn build_logical_candidate_items<'a>(
}
}
LocalExecutionCandidateKind::PoolGroup => {
let cursor = PoolKeyCursor::new(
state,
candidate,
sticky_session_token,
requested_model,
request_auth_channel,
);
let cursor = if let Some(trace_id) = trace_id {
cursor.with_runtime_miss_diagnostic(trace_id, record_runtime_miss_diagnostic)
} else {
cursor
};
items.push_back(LocalExecutionCandidateAttemptSourceItem::Pool {
cursor: PoolKeyCursor::new(
state,
candidate,
sticky_session_token,
requested_model,
request_auth_channel,
),
cursor,
candidate_index,
pending_attempts: VecDeque::new(),
});
@@ -757,6 +770,8 @@ impl<'a> RequestedModelAttemptPageCursor<'a> {
self.state,
candidates,
self.next_candidate_index,
Some(&self.trace_id),
self.record_runtime_miss_diagnostic,
self.sticky_session_token.as_deref(),
Some(&self.requested_model),
self.request_auth_channel.as_deref(),
@@ -922,6 +937,7 @@ async fn materialize_logical_local_execution_candidate_attempts<F>(
state: PlannerAppState<'_>,
trace_id: &str,
context: LocalAvailableCandidatePersistenceContext<'_>,
record_runtime_miss_diagnostic: bool,
candidates: Vec<EligibleLocalExecutionCandidate>,
sticky_session_token: Option<&str>,
requested_model: Option<&str>,
@@ -956,7 +972,8 @@ where
sticky_session_token,
requested_model,
request_auth_channel,
);
)
.with_runtime_miss_diagnostic(trace_id, record_runtime_miss_diagnostic);
let attempt_count_before_pool = attempts.len();
while let Some(candidate) = cursor.next_key().await {
attempts.extend(build_unpersisted_local_execution_candidate_attempts(
@@ -1525,6 +1542,7 @@ mod tests {
required_capabilities: None,
error_context: "persist should not fail",
},
false,
vec![pool_group, sample_eligible("normal-key", None)],
None,
Some("gpt-5"),

View File

@@ -19,6 +19,7 @@ use crate::ai_serving::planner::candidate_resolution::{
candidate_auth_channel_skip_reason, read_candidate_transport_snapshot,
EligibleLocalExecutionCandidate, LocalExecutionCandidateKind, SkippedLocalExecutionCandidate,
};
use crate::ai_serving::planner::runtime_miss::record_local_runtime_candidate_skip_reason;
use crate::ai_serving::{
candidate_common_transport_skip_reason, CandidateTransportPolicyFacts, PlannerAppState,
};
@@ -171,6 +172,8 @@ pub(crate) struct PoolKeyCursor<'a> {
sticky_session_token: Option<String>,
requested_model: Option<String>,
request_auth_channel: Option<String>,
runtime_miss_trace_id: Option<String>,
record_runtime_miss_diagnostic: bool,
pool_key_order: StoredPoolKeyCandidateOrder,
next_offset: u32,
scanned_keys: u32,
@@ -183,6 +186,8 @@ pub(crate) struct PoolKeyCursor<'a> {
queued_candidates: VecDeque<EligibleLocalExecutionCandidate>,
skipped_candidates: Vec<SkippedLocalExecutionCandidate>,
exhausted_logged: bool,
returned_key_count: u32,
exhaustion_skip_recorded: bool,
}
impl<'a> PoolKeyCursor<'a> {
@@ -200,6 +205,8 @@ impl<'a> PoolKeyCursor<'a> {
sticky_session_token: sticky_session_token.map(str::to_string),
requested_model: requested_model.map(str::to_string),
request_auth_channel: request_auth_channel.map(str::to_string),
runtime_miss_trace_id: None,
record_runtime_miss_diagnostic: false,
pool_key_order,
next_offset: 0,
scanned_keys: 0,
@@ -212,12 +219,27 @@ impl<'a> PoolKeyCursor<'a> {
queued_candidates: VecDeque::new(),
skipped_candidates: Vec::new(),
exhausted_logged: false,
returned_key_count: 0,
exhaustion_skip_recorded: false,
}
}
pub(crate) fn with_runtime_miss_diagnostic(
mut self,
trace_id: &str,
record_runtime_miss_diagnostic: bool,
) -> Self {
if record_runtime_miss_diagnostic {
self.runtime_miss_trace_id = Some(trace_id.to_string());
self.record_runtime_miss_diagnostic = true;
}
self
}
pub(crate) async fn next_key(&mut self) -> Option<EligibleLocalExecutionCandidate> {
loop {
if let Some(candidate) = self.next_queued_candidate().await {
self.returned_key_count = self.returned_key_count.saturating_add(1);
return Some(candidate);
}
@@ -254,6 +276,37 @@ impl<'a> PoolKeyCursor<'a> {
skip_reason_counts = ?self.skip_reason_counts,
"gateway pool scheduler exhausted pool group without a schedulable key"
);
self.record_runtime_miss_pool_exhaustion_skip_reason();
}
fn record_runtime_miss_pool_exhaustion_skip_reason(&mut self) {
if self.exhaustion_skip_recorded
|| !self.record_runtime_miss_diagnostic
|| self.returned_key_count > 0
{
return;
}
let Some(trace_id) = self.runtime_miss_trace_id.as_deref() else {
return;
};
self.exhaustion_skip_recorded = true;
record_local_runtime_candidate_skip_reason(
self.state.app(),
trace_id,
self.runtime_miss_pool_exhaustion_skip_reason(),
);
}
fn runtime_miss_pool_exhaustion_skip_reason(&self) -> &'static str {
let mut selected_reason = "pool_group_exhausted";
let mut selected_count = 0;
for (reason, count) in &self.skip_reason_counts {
if *count > selected_count {
selected_reason = *reason;
selected_count = *count;
}
}
selected_reason
}
async fn next_page_candidates(&mut self) -> Option<Vec<EligibleLocalExecutionCandidate>> {
@@ -963,6 +1016,7 @@ mod tests {
use crate::ai_serving::planner::candidate_resolution::{
EligibleLocalExecutionCandidate, LocalExecutionCandidateKind,
};
use crate::ai_serving::planner::runtime_miss::apply_local_runtime_candidate_terminal_reason;
use crate::ai_serving::PlannerAppState;
use crate::data::GatewayDataState;
use crate::handlers::shared::provider_pool::{
@@ -970,7 +1024,7 @@ mod tests {
try_claim_admin_provider_pool_key, AdminProviderPoolRuntimeState,
};
use crate::orchestration::LocalExecutionCandidateMetadata;
use crate::AppState;
use crate::{AppState, LocalExecutionRuntimeMissDiagnostic};
use aether_ai_serving::{normalize_enabled_ai_pool_presets, AiPoolSchedulingPreset};
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
@@ -1739,6 +1793,43 @@ mod tests {
));
}
#[test]
fn pool_key_cursor_records_runtime_miss_when_exhausted_without_returning_key() {
let app = AppState::new().expect("state should build");
let trace_id = "trace-pool-exhausted-runtime-miss";
app.set_local_execution_runtime_miss_diagnostic(
trace_id,
LocalExecutionRuntimeMissDiagnostic {
reason: "candidate_evaluation_incomplete".to_string(),
requested_model: Some("gpt-5".to_string()),
candidate_count: Some(1),
..LocalExecutionRuntimeMissDiagnostic::default()
},
);
let group = sample_eligible_candidate(
"provider-pool",
"endpoint-1",
"pool-group",
10,
Some(json!({ "pool_advanced": { "lru_enabled": true } })),
);
let mut cursor = PoolKeyCursor::new(PlannerAppState::new(&app), group, None, None, None)
.with_runtime_miss_diagnostic(trace_id, true);
cursor.record_skip_reason("pool_key_lease_busy");
cursor.record_skip_reason("pool_key_lease_busy");
cursor.record_skip_reason("transport_snapshot_missing");
cursor.log_exhausted();
apply_local_runtime_candidate_terminal_reason(&app, trace_id, "no_local_sync_plans");
let diagnostic = app
.take_local_execution_runtime_miss_diagnostic(trace_id)
.expect("runtime miss diagnostic should exist");
assert_eq!(diagnostic.reason, "all_candidates_skipped");
assert_eq!(diagnostic.skipped_candidate_count, Some(1));
assert_eq!(diagnostic.skip_reasons.get("pool_key_lease_busy"), Some(&1));
}
#[tokio::test]
async fn pool_key_cursor_revalidates_queued_candidates_before_return() {
let app = AppState::new().expect("state should build");

View File

@@ -1656,6 +1656,7 @@ fn local_execution_runtime_miss_skip_reasons_summary(
fn local_execution_runtime_miss_skip_reason_label(reason: &str) -> &str {
match reason {
"api_key_concurrency_limit_reached" => "API Key 并发已达上限",
"auth_channel_mismatch" => "认证通道不匹配",
"auth_snapshot_missing" => "API Key 本地执行配置缺失",
"endpoint_api_format_changed" => "端点 API 格式已变更",
"endpoint_inactive" => "端点未启用",
@@ -1664,6 +1665,10 @@ fn local_execution_runtime_miss_skip_reason_label(reason: &str) -> &str {
"key_inactive" => "API Key 未启用",
"key_model_disabled" => "API Key 未允许该模型",
"mapped_model_missing" => "模型映射缺失",
"pool_cooldown" => "池内账号处于冷却中",
"pool_cost_limit_reached" => "池内账号成本额度已用尽",
"pool_group_exhausted" => "池化提供商没有可调度账号",
"pool_key_lease_busy" => "池内账号正被其他请求占用",
"provider_inactive" => "提供商未启用",
"provider_request_body_missing" => "无法构建上游请求体",
"provider_request_body_build_failed" => "上游请求体转换失败",