2026-02-27 13:58:58 +08:00
|
|
|
"""Pool scheduling trace -- per-candidate decision records.
|
|
|
|
|
|
|
|
|
|
Collects scheduling decisions made during pool-level candidate selection
|
|
|
|
|
without adding any extra Redis round-trips. Trace data is later written
|
|
|
|
|
to ``RequestCandidate.extra_data`` and ``Usage.request_metadata``.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(slots=True)
|
|
|
|
|
class PoolCandidateTrace:
|
|
|
|
|
"""Single candidate scheduling decision in a pool context."""
|
|
|
|
|
|
|
|
|
|
key_id: str
|
|
|
|
|
reason: str = "" # sticky / lru / random / tiebreak
|
|
|
|
|
sticky_hit: bool = False
|
|
|
|
|
lru_score: float = 0.0
|
|
|
|
|
cost_window_usage: int = 0
|
|
|
|
|
cost_limit: int | None = None
|
|
|
|
|
cost_soft_threshold: bool = False
|
|
|
|
|
skipped: bool = False
|
2026-03-04 22:06:19 +08:00
|
|
|
skip_type: str | None = None # cooldown / cost_exhausted / account_blocked / upstream
|
2026-02-27 13:58:58 +08:00
|
|
|
cooldown_reason: str | None = None
|
|
|
|
|
cooldown_ttl: int | None = None
|
2026-03-04 22:06:19 +08:00
|
|
|
account_block_code: str | None = None
|
|
|
|
|
account_block_label: str | None = None
|
|
|
|
|
account_block_reason: str | None = None
|
|
|
|
|
latency_avg_ms: float = 0.0
|
|
|
|
|
health_score: float = 1.0
|
|
|
|
|
composite_score: float = 0.0
|
|
|
|
|
scoring_mode: str = "lru"
|
2026-02-27 13:58:58 +08:00
|
|
|
|
|
|
|
|
def to_extra_data(self) -> dict[str, Any]:
|
|
|
|
|
"""Build dict to merge into ``RequestCandidate.extra_data``."""
|
|
|
|
|
if self.skipped:
|
|
|
|
|
skip_info: dict[str, Any] = {"type": self.skip_type}
|
|
|
|
|
if self.cooldown_reason is not None:
|
|
|
|
|
skip_info["cooldown_reason"] = self.cooldown_reason
|
|
|
|
|
if self.cooldown_ttl is not None:
|
|
|
|
|
skip_info["cooldown_ttl"] = self.cooldown_ttl
|
2026-03-04 22:06:19 +08:00
|
|
|
if self.account_block_code is not None:
|
|
|
|
|
skip_info["account_block_code"] = self.account_block_code
|
|
|
|
|
if self.account_block_label is not None:
|
|
|
|
|
skip_info["account_block_label"] = self.account_block_label
|
|
|
|
|
if self.account_block_reason is not None:
|
|
|
|
|
skip_info["account_block_reason"] = self.account_block_reason
|
2026-02-27 13:58:58 +08:00
|
|
|
if self.cost_window_usage:
|
|
|
|
|
skip_info["cost_window_usage"] = self.cost_window_usage
|
2026-03-04 22:06:19 +08:00
|
|
|
if self.scoring_mode:
|
|
|
|
|
skip_info["scoring_mode"] = self.scoring_mode
|
2026-02-27 13:58:58 +08:00
|
|
|
return {"pool_skip": skip_info}
|
|
|
|
|
|
|
|
|
|
sel: dict[str, Any] = {"reason": self.reason}
|
|
|
|
|
if self.sticky_hit:
|
|
|
|
|
sel["sticky_hit"] = True
|
|
|
|
|
if self.lru_score:
|
|
|
|
|
sel["lru_score"] = self.lru_score
|
|
|
|
|
if self.cost_window_usage:
|
|
|
|
|
sel["cost_window_usage"] = self.cost_window_usage
|
|
|
|
|
if self.cost_limit is not None:
|
|
|
|
|
sel["cost_limit"] = self.cost_limit
|
|
|
|
|
if self.cost_soft_threshold:
|
|
|
|
|
sel["cost_soft_threshold"] = True
|
2026-03-04 22:06:19 +08:00
|
|
|
if self.latency_avg_ms > 0:
|
|
|
|
|
sel["latency_avg_ms"] = round(self.latency_avg_ms, 2)
|
|
|
|
|
if self.health_score < 1.0:
|
|
|
|
|
sel["health_score"] = round(self.health_score, 4)
|
|
|
|
|
if self.reason == "multi_score":
|
|
|
|
|
sel["composite_score"] = round(self.composite_score, 6)
|
|
|
|
|
if self.scoring_mode:
|
|
|
|
|
sel["scoring_mode"] = self.scoring_mode
|
2026-02-27 13:58:58 +08:00
|
|
|
return {"pool_selection": sel}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(slots=True)
|
|
|
|
|
class PoolSchedulingTrace:
|
|
|
|
|
"""Aggregated scheduling trace for one pool-provider dispatch."""
|
|
|
|
|
|
|
|
|
|
provider_id: str
|
|
|
|
|
total_keys: int = 0
|
|
|
|
|
sticky_session_used: bool = False
|
|
|
|
|
session_uuid: str | None = None
|
|
|
|
|
candidate_traces: dict[str, PoolCandidateTrace] = field(default_factory=dict)
|
|
|
|
|
|
2026-03-03 09:22:20 +08:00
|
|
|
def build_summary(
|
|
|
|
|
self,
|
|
|
|
|
success_key_id: str | None = None,
|
|
|
|
|
*,
|
|
|
|
|
attempted_key_ids: set[str] | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
2026-02-27 13:58:58 +08:00
|
|
|
"""Build compact dict for ``Usage.request_metadata["pool_summary"]``."""
|
|
|
|
|
skipped_cooldown = 0
|
|
|
|
|
skipped_cost = 0
|
2026-03-04 22:06:19 +08:00
|
|
|
skipped_account_blocked = 0
|
2026-02-27 13:58:58 +08:00
|
|
|
attempted = 0
|
|
|
|
|
for t in self.candidate_traces.values():
|
|
|
|
|
if t.skipped:
|
|
|
|
|
if t.skip_type == "cooldown":
|
|
|
|
|
skipped_cooldown += 1
|
|
|
|
|
elif t.skip_type == "cost_exhausted":
|
|
|
|
|
skipped_cost += 1
|
2026-03-04 22:06:19 +08:00
|
|
|
elif t.skip_type == "account_blocked":
|
|
|
|
|
skipped_account_blocked += 1
|
2026-03-03 09:22:20 +08:00
|
|
|
|
|
|
|
|
if attempted_key_ids is None:
|
|
|
|
|
# Backward-compatible behavior: count all schedulable keys.
|
|
|
|
|
attempted = sum(1 for t in self.candidate_traces.values() if not t.skipped)
|
|
|
|
|
else:
|
|
|
|
|
# Preferred behavior: count only keys that were actually executed.
|
|
|
|
|
attempted = sum(
|
|
|
|
|
1
|
|
|
|
|
for kid in attempted_key_ids
|
|
|
|
|
if kid in self.candidate_traces and not self.candidate_traces[kid].skipped
|
|
|
|
|
)
|
2026-02-27 13:58:58 +08:00
|
|
|
|
|
|
|
|
success_reason: str | None = None
|
|
|
|
|
if success_key_id and success_key_id in self.candidate_traces:
|
|
|
|
|
success_reason = self.candidate_traces[success_key_id].reason
|
|
|
|
|
|
|
|
|
|
summary: dict[str, Any] = {
|
|
|
|
|
"enabled": True,
|
|
|
|
|
"total_keys": self.total_keys,
|
|
|
|
|
"attempted": attempted,
|
|
|
|
|
"skipped_cooldown": skipped_cooldown,
|
|
|
|
|
"skipped_cost": skipped_cost,
|
2026-03-04 22:06:19 +08:00
|
|
|
"skipped_account_blocked": skipped_account_blocked,
|
2026-02-27 13:58:58 +08:00
|
|
|
"sticky_session": self.sticky_session_used,
|
|
|
|
|
}
|
|
|
|
|
if success_key_id:
|
|
|
|
|
summary["success_key_id"] = success_key_id[:8]
|
|
|
|
|
if success_reason:
|
|
|
|
|
summary["success_reason"] = success_reason
|
|
|
|
|
return summary
|