fix: 修正时间戳精度、TTL 定价匹配逻辑及账单快照展示

- 将 created_at_unix_ms 从 current_unix_secs 改为 current_unix_ms,修正候选尝试和跳过记录的时间戳精度
- formula_engine: TTL 定价从「<=上限」模糊匹配改为精确匹配,null 值改为回退到基础价格而非透传
- 新增 ttl_pricing_requires_exact_match 和 ttl_pricing_null_value_falls_back_to_base_tier_value 测试用例
- service: 新增 5min/1h cache TTL 的端到端计费验证测试
- RequestDetailDrawer: 优先从 billing_snapshot 读取已解析的价格和费用,正确展示当前 TTL 对应的缓存创建/读取价格,修正输入/输出/缓存费用列的数据来源
This commit is contained in:
fawney19
2026-04-10 18:50:59 +08:00
parent 010ab127e2
commit d5b8583d6b
11 changed files with 542 additions and 76 deletions

View File

@@ -9,7 +9,7 @@ use crate::ai_pipeline::{
resolve_local_decision_execution_runtime_auth_context, ConversionMode, ExecutionStrategy,
GatewayControlDecision, PlannerAppState,
};
use crate::clock::current_unix_secs;
use crate::clock::{current_unix_ms, current_unix_secs};
use crate::{append_execution_contract_fields_to_value, AppState, GatewayError};
use super::{
@@ -105,7 +105,7 @@ pub(crate) async fn materialize_local_same_format_provider_candidate_attempts(
)
.await;
let created_at_unix_ms = current_unix_secs();
let created_at_unix_ms = current_unix_ms();
let mut attempts = Vec::with_capacity(candidates.len());
let mut affinity_remembered = false;
for (candidate_index, candidate) in candidates.into_iter().enumerate() {

View File

@@ -21,7 +21,7 @@ use crate::ai_pipeline::transport::{
use crate::ai_pipeline::{
collect_control_headers, ConversionMode, ExecutionStrategy, PlannerAppState,
};
use crate::clock::current_unix_secs;
use crate::clock::current_unix_ms;
use crate::{
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
@@ -365,7 +365,7 @@ pub(super) async fn mark_skipped_local_same_format_provider_candidate(
candidate_id,
input.required_capabilities.as_ref(),
skip_reason,
current_unix_secs(),
current_unix_ms(),
"gateway local same-format decision failed to persist skipped candidate",
)
.await;

View File

@@ -11,7 +11,7 @@ use crate::ai_pipeline::{
resolve_local_decision_execution_runtime_auth_context, GatewayControlDecision,
};
use crate::ai_pipeline::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::clock::current_unix_secs;
use crate::clock::{current_unix_ms, current_unix_secs};
use crate::{AppState, GatewayError};
pub(super) const GEMINI_FILES_CANDIDATE_API_FORMAT: &str = "gemini:chat";
@@ -102,7 +102,7 @@ pub(super) async fn materialize_local_gemini_files_candidate_attempts(
)
.await;
let created_at_unix_ms = current_unix_secs();
let created_at_unix_ms = current_unix_ms();
let mut attempts = Vec::with_capacity(candidates.len());
let mut affinity_remembered = false;
for (candidate_index, candidate) in candidates.into_iter().enumerate() {
@@ -174,7 +174,7 @@ pub(super) async fn mark_skipped_local_gemini_files_candidate(
candidate_id,
input.required_capabilities.as_ref(),
skip_reason,
current_unix_secs(),
current_unix_ms(),
"gateway local gemini files failed to persist skipped candidate",
)
.await;

View File

@@ -12,7 +12,7 @@ use crate::ai_pipeline::{
resolve_local_decision_execution_runtime_auth_context, GatewayControlDecision,
};
use crate::ai_pipeline::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::clock::current_unix_secs;
use crate::clock::{current_unix_ms, current_unix_secs};
use crate::AppState;
#[derive(Debug, Clone)]
@@ -148,7 +148,7 @@ async fn materialize_local_video_create_candidate_attempts(
input.required_capabilities.as_ref(),
)
.await;
let created_at_unix_ms = current_unix_secs();
let created_at_unix_ms = current_unix_ms();
let mut attempts = Vec::with_capacity(candidates.len());
let mut affinity_remembered = false;
@@ -220,7 +220,7 @@ pub(super) async fn mark_skipped_local_video_candidate(
candidate_id,
input.required_capabilities.as_ref(),
skip_reason,
current_unix_secs(),
current_unix_ms(),
"gateway local video decision failed to persist skipped candidate",
)
.await;

View File

@@ -17,7 +17,7 @@ use crate::ai_pipeline::{
GatewayControlDecision,
};
use crate::ai_pipeline::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::clock::current_unix_secs;
use crate::clock::{current_unix_ms, current_unix_secs};
use crate::{append_execution_contract_fields_to_value, AppState, GatewayError};
use super::{
@@ -145,7 +145,7 @@ pub(super) async fn materialize_local_standard_candidate_attempts(
)
.await;
let created_at_unix_ms = current_unix_secs();
let created_at_unix_ms = current_unix_ms();
let mut attempts = Vec::with_capacity(candidates.len());
let mut affinity_remembered = false;
for (candidate_index, candidate) in candidates.into_iter().enumerate() {

View File

@@ -14,7 +14,7 @@ use crate::ai_pipeline::transport::{
};
use crate::ai_pipeline::{collect_control_headers, ConversionMode, ExecutionStrategy};
use crate::ai_pipeline::{LocalResolvedOAuthRequestAuth, PlannerAppState};
use crate::clock::current_unix_secs;
use crate::clock::current_unix_ms;
use crate::{
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
@@ -390,7 +390,7 @@ pub(super) async fn mark_skipped_local_standard_candidate(
candidate_id,
input.required_capabilities.as_ref(),
skip_reason,
current_unix_secs(),
current_unix_ms(),
"gateway local standard decision failed to persist skipped candidate",
)
.await;

View File

@@ -12,7 +12,7 @@ use crate::ai_pipeline::planner::candidate_affinity::{
};
use crate::ai_pipeline::GatewayAuthApiKeySnapshot;
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy, PlannerAppState};
use crate::clock::current_unix_secs;
use crate::clock::{current_unix_ms, current_unix_secs};
use crate::{append_execution_contract_fields_to_value, AppState};
#[derive(Debug, Clone)]
@@ -57,7 +57,7 @@ pub(crate) async fn mark_skipped_local_openai_chat_candidate(
candidate_id,
input.required_capabilities.as_ref(),
skip_reason,
current_unix_secs(),
current_unix_ms(),
"gateway local openai chat decision failed to persist skipped candidate",
)
.await;
@@ -77,7 +77,7 @@ pub(crate) async fn materialize_local_openai_chat_candidate_attempts(
input.required_capabilities.as_ref(),
)
.await;
let created_at_unix_ms = current_unix_secs();
let created_at_unix_ms = current_unix_ms();
let mut attempts = Vec::with_capacity(candidates.len());
let mut affinity_remembered = false;

View File

@@ -18,7 +18,7 @@ use crate::ai_pipeline::{
GatewayControlDecision,
};
use crate::ai_pipeline::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::clock::current_unix_secs;
use crate::clock::{current_unix_ms, current_unix_secs};
use crate::{append_execution_contract_fields_to_value, AppState, GatewayError};
use super::LocalOpenAiCliSpec;
@@ -152,7 +152,7 @@ pub(crate) async fn materialize_local_openai_cli_candidate_attempts(
)
.await;
let created_at_unix_ms = current_unix_secs();
let created_at_unix_ms = current_unix_ms();
let mut attempts = Vec::with_capacity(candidates.len());
let mut affinity_remembered = false;
for (candidate_index, candidate) in candidates.into_iter().enumerate() {
@@ -314,7 +314,7 @@ pub(crate) async fn mark_skipped_local_openai_cli_candidate(
candidate_id,
input.required_capabilities.as_ref(),
skip_reason,
current_unix_secs(),
current_unix_ms(),
"gateway local openai cli decision failed to persist skipped candidate",
)
.await;