@@ -377,6 +378,9 @@ function StateBadge({ node, state, protectedNode }: { node: EgressNodeDTO; state
if (state?.disabled_by_guard) return
{t("qualityGuard.quarantined")};
if (protectedNode) return
{t("qualityGuard.fixedFallback")};
if (!node.enabled) return
{t("common.disabled")};
+ if (state?.quarantined_lease_count) return
{t("qualityGuard.leaseQuarantined", { count: state.quarantined_lease_count })};
+ if (state?.observe_only) return
{t("qualityGuard.leaseScopedObserveOnly")};
+ if (node.accountBoundProxy) return
{t("qualityGuard.leaseScoped")};
if (state?.error_strikes) return
{t("qualityGuard.probeFailed")};
if (state?.last_classification === "hard" || state?.last_classification === "soft") return
{t("qualityGuard.suspect")};
if (state?.last_classification === "healthy") return
{t("qualityGuard.healthy")};
@@ -389,13 +393,29 @@ function EventList({ events, locale }: { events: QualityGuardEvent[]; locale: st
{t("qualityGuard.events")}
{events.length === 0 ?
{t("qualityGuard.noEvents")}
:
{[...events].reverse().slice(0, 10).map((event, index) =>
-
{event.node_name || `ID ${event.node_id}`} · {t(`qualityGuard.eventTypes.${event.event}`)}
{t(`qualityGuard.reasons.${event.reason || "unknown"}`)}{event.output_tps ? ` · ${formatTPS(event.output_tps)}` : ""}
+
{event.node_name || `ID ${event.node_id}`} · {t(eventLabelKey(event.event))}
{t(reasonLabelKey(event.reason))}{event.account_id ? ` · ${t("qualityGuard.accountLease", { id: event.account_id })}` : ""}{event.request_id ? ` · ${event.request_id}` : ""}{event.cooldown_until ? ` · ${t("qualityGuard.leaseUntil", { time: formatTime(event.cooldown_until, locale) })}` : ""}{event.output_tps ? ` · ${formatTPS(event.output_tps)}` : ""}
)}
}
;
}
+function eventLabelKey(event: string): string {
+ if (event === "lease_scoped_quarantine_suppressed") return "qualityGuard.leaseScopedQuarantineSuppressedEvent";
+ if (event === "lease_scoped_guard_released") return "qualityGuard.leaseScopedGuardReleasedEvent";
+ if (event === "lease_quarantined") return "qualityGuard.leaseQuarantinedEvent";
+ if (event === "lease_restored") return "qualityGuard.leaseRestoredEvent";
+ if (event === "lease_quarantine_extended") return "qualityGuard.leaseQuarantineExtendedEvent";
+ if (event === "lease_quarantine_failed" || event === "lease_quarantine_suppressed") return "qualityGuard.leaseQuarantineFailedEvent";
+ return `qualityGuard.eventTypes.${event}`;
+}
+
+function reasonLabelKey(reason: string): string {
+ if (reason === "lease_scoped_node") return "qualityGuard.leaseScopedNodeReason";
+ if (reason === "fixed_fallback_node") return "qualityGuard.fixedFallback";
+ return `qualityGuard.reasons.${reason || "unknown"}`;
+}
+
function Policy({ status, onEdit }: { status: QualityGuardStatus; onEdit: () => void }) {
const { t } = useTranslation();
const config = status.config;
@@ -535,6 +555,7 @@ function qualityTestState(result: QualityTestResult, status: QualityGuardStatus)
else if (result.outputTokensPerSecond >= softTPS) { classification = "soft"; reason = "soft_tps"; }
const now = Date.now() / 1000;
return {
+ observe_only: false, observe_only_reason: "", quarantined_lease_count: 0,
active_soft_strikes: classification === "soft" ? 1 : classification === "hard" ? (status.config?.consecutive_soft ?? 2) : 0,
passive_soft_strikes: 0, error_strikes: 0, quarantined_until: 0, disabled_by_guard: false,
last_reason: reason, last_probe_at: now, last_observed_at: now, last_source: "active",
diff --git a/frontend/src/shared/i18n/index.ts b/frontend/src/shared/i18n/index.ts
index 8dea5ba8f..f1e07eee2 100644
--- a/frontend/src/shared/i18n/index.ts
+++ b/frontend/src/shared/i18n/index.ts
@@ -471,17 +471,32 @@ const resources = {
},
qualityGuard: {
title: "质量守护", description: "监测 Grok 出口质量并在异常时自动隔离节点。", overview: "质量守护概览",
- serviceStatus: "守护服务", running: "运行正常", stale: "状态滞后", mode: "检测模式", availableNodes: "已启用节点", quarantinedNodes: "已隔离节点",
+ serviceStatus: "守护服务", running: "运行正常", stale: "状态滞后", mode: "检测模式", availableNodes: "已启用节点", quarantinedNodes: "已隔离节点", quarantinedTargets: "隔离对象",
modes: { active: "主动检测", passive: "被动审计", hybrid: "混合模式" },
nodes: "节点质量", nodesHelp: "速度与 grok2api 面板同口径:输出 Token 包含推理 Token。首字后窗口短于首字等待且不足 1 秒时改用全程,避免加密思考被挤进最后几十毫秒。完整用户请求出现速度异常后会立即隔离,并在隔离期结束后使用受控探针复测。", updatedAt: "状态更新于 {{time}}",
node: "节点", state: "状态", outputTPS: "面板输出速度", firstToken: "首字延迟", source: "数据来源", strikes: "打击计数", lastObserved: "最近观测", test: "检测",
sources: { active: "主动探针", passive: "请求审计" }, quarantined: "已隔离", fixedFallback: "固定回退(受保护)", suspect: "可疑", healthy: "正常", pending: "待观测", probeFailed: "检测失败",
- events: "最近事件", noEvents: "暂无异常或恢复事件", eventTypes: { node_quarantined: "节点已隔离", node_restored: "节点已恢复", node_rotated: "节点已更换 IP", passive_audit_anomaly: "检测到异常请求" },
+ events: "最近事件", noEvents: "暂无异常或恢复事件", eventTypes: { node_quarantined: "节点已隔离", node_restored: "节点已恢复", node_rotated: "节点已更换 IP", passive_audit_anomaly: "检测到异常请求", lease_quarantined: "账号租约已隔离", lease_restored: "账号租约已恢复", lease_quarantine_extended: "账号租约隔离已延长", lease_quarantine_failed: "账号租约隔离未执行" },
statistics: "自动检测统计", statisticsSince: "自 {{time}} 开始累计,不含手动检测。", statisticsChecks: "有效检测总数", statisticsChecksHelp: "主动探测与有效被动审计", statisticsActive: "主动探测", statisticsActiveDetail: "正常 {{healthy}},错误 {{errors}}", statisticsPassive: "被动审计", statisticsPassiveDetail: "正常 {{healthy}},来自真实请求", statisticsTokens: "主动探测输出 Token", statisticsTokensHelp: "包含推理 Token,不代表代理流量", statisticsAnomalies: "异常命中", statisticsAnomalyDetail: "软异常 {{soft}},硬异常 {{hard}}", statisticsQuarantines: "执行隔离", statisticsActionDetail: "已恢复 {{restored}},受保护未隔离 {{suppressed}}",
reasons: { unknown: "未记录原因", hard_tps: "超过硬阈值", soft_tps: "超过软阈值", buffered_burst: "短窗口输出突增,等待原 IP 复测", missing_thinking: "输出缺少 thinking", passive_hard_tps: "请求速度超过硬阈值", passive_soft_tps: "请求速度超过软阈值", quality_probe_healthy: "模型质量检测恢复正常", expected_marker_missing: "响应标记缺失", insufficient_output_tokens: "输出 Token 不足", insufficient_visible_tokens: "可见 Token 不足", insufficient_generation_window: "有效生成窗口不足", probe_errors: "主动检测连续失败", probe_no_account: "暂无可调度账号,已延后复测", recovery_probe_error: "恢复检测失败", rotation_error: "更换 IP 失败" },
policy: "当前策略", editPolicy: "编辑策略", editPolicyTitle: "编辑质量守护策略", editPolicyDescription: "保存后由守护进程热加载,无需重启服务。", restoreDefaults: "恢复默认值", policySaved: "策略已保存,正在热加载", invalidPolicyValue: "数值超出允许范围", softThresholdMustBeLower: "软阈值必须低于硬阈值", activeIntervalSeconds: "主动检测间隔(秒)", passiveIntervalSeconds: "被动审计间隔(秒)", consecutiveSoft: "主动软异常连续次数", consecutiveErrors: "检测错误连续次数", quarantineSeconds: "隔离时长(秒)", softThreshold: "软阈值", hardThreshold: "硬阈值", activeInterval: "主动间隔", passiveInterval: "审计间隔", quarantineDuration: "隔离时长", minimumNodes: "最少保留节点",
unavailable: "质量守护尚未连接", unavailableHelp: "在 config.yaml 中启用 qualityGuard,并启动 quality-guard Compose profile 后,这里会显示实时状态。", testing: "正在检测节点质量", testComplete: "检测完成:{{speed}}", testFailed: "质量检测暂不可用,请稍后重试",
nodesTab: "节点质量",
+ leaseScopedObserveOnly: "租约级(仅观测)",
+ leaseScoped: "租约级",
+ leaseQuarantined: "租约隔离 {{count}}",
+ leaseScopedHelp: "该节点按账号生成不同粘性租约;异常请求只隔离对应账号租约,不会停用整个共享节点。",
+ leaseScopedObserveOnlyHelp: "当前异常缺少账号身份或租约接口不可用,因此仅记录观测,不会停用整个共享节点。",
+ leaseQuarantinedEvent: "账号租约已隔离",
+ leaseRestoredEvent: "账号租约已恢复",
+ leaseQuarantineExtendedEvent: "账号租约隔离已延长",
+ leaseQuarantineFailedEvent: "账号租约隔离未执行",
+ leaseScopedQuarantineSuppressedEvent: "已阻止整节点隔离",
+ leaseScopedGuardReleasedEvent: "已解除旧版整节点隔离",
+ leaseScopedNodeReason: "节点包含多个账号粘性租约",
+ accountLease: "账号 {{id}}",
+ leaseUntil: "隔离至 {{time}}",
+ statisticsSuppressedActionDetail: "已恢复 {{restored}},策略抑制 {{suppressed}}",
profilesTab: "探针方案",
profilesHelp: "主动质量探测用的 Prompt 与预期标记。标记缺失记为硬异常。",
profileActive: "当前使用",
@@ -1557,6 +1572,22 @@ const resources = {
shell: { appearance: "Appearance", dark: "Dark", light: "Light", system: "System", language: "Language", navigation: "Navigation", openNavigation: "Open navigation" },
qualityGuard: { title: "Quality guard", description: "Monitor Grok egress quality and quarantine anomalous nodes automatically.", overview: "Quality guard overview", serviceStatus: "Guard service", running: "Running", stale: "Status stale", mode: "Detection mode", availableNodes: "Enabled nodes", quarantinedNodes: "Quarantined", modes: { active: "Active probes", passive: "Passive audits", hybrid: "Hybrid" }, nodes: "Node quality", nodesHelp: "Speed matches the grok2api panel: output tokens include reasoning tokens. A tail shorter than the first-token wait and under 1s uses the full request duration so encrypted thinking is not crushed into the flush. A completed user request with anomalous throughput is isolated immediately, then verified with a controlled probe after the hold.", updatedAt: "Updated {{time}}", node: "Node", state: "State", outputTPS: "Panel output speed", firstToken: "First token", source: "Source", strikes: "Strikes", lastObserved: "Last observed", test: "Test", sources: { active: "Active probe", passive: "Request audit" }, quarantined: "Quarantined", fixedFallback: "Fixed fallback (protected)", suspect: "Suspect", healthy: "Healthy", pending: "Pending", probeFailed: "Probe failed", events: "Recent events", noEvents: "No anomaly or recovery events", eventTypes: { node_quarantined: "Node quarantined", node_restored: "Node restored", node_rotated: "Node IP rotated", passive_audit_anomaly: "Anomalous request detected" }, statistics: "Automatic detection statistics", statisticsSince: "Accumulated since {{time}}. Manual tests are excluded.", statisticsChecks: "Valid checks", statisticsChecksHelp: "Active probes and valid passive audits", statisticsActive: "Active probes", statisticsActiveDetail: "Healthy {{healthy}}, errors {{errors}}", statisticsPassive: "Passive audits", statisticsPassiveDetail: "Healthy {{healthy}}, from real requests", statisticsTokens: "Active output tokens", statisticsTokensHelp: "Includes reasoning tokens; not proxy traffic", statisticsAnomalies: "Anomaly hits", statisticsAnomalyDetail: "Soft {{soft}}, hard {{hard}}", statisticsQuarantines: "Quarantines applied", statisticsActionDetail: "Restored {{restored}}, protected {{suppressed}}", reasons: { unknown: "No reason recorded", hard_tps: "Hard threshold exceeded", soft_tps: "Soft threshold exceeded", buffered_burst: "Short-window output burst; retesting the same IP", missing_thinking: "Missing thinking tokens", passive_hard_tps: "Request exceeded hard threshold", passive_soft_tps: "Request exceeded soft threshold", quality_probe_healthy: "Model quality probe recovered", expected_marker_missing: "Expected marker missing", insufficient_output_tokens: "Too few output tokens", insufficient_visible_tokens: "Too few visible tokens", insufficient_generation_window: "Generation window too short", probe_errors: "Repeated probe errors", probe_no_account: "No schedulable probe account; retry deferred", recovery_probe_error: "Recovery probe failed", rotation_error: "IP rotation failed" }, policy: "Current policy", editPolicy: "Edit policy", editPolicyTitle: "Edit quality guard policy", editPolicyDescription: "The guard hot-reloads saved changes without a service restart.", restoreDefaults: "Restore defaults", policySaved: "Policy saved and queued for hot reload", invalidPolicyValue: "Value is outside the allowed range", softThresholdMustBeLower: "The soft threshold must be lower than the hard threshold", activeIntervalSeconds: "Active interval (seconds)", passiveIntervalSeconds: "Passive interval (seconds)", consecutiveSoft: "Consecutive active soft strikes", consecutiveErrors: "Consecutive probe errors", quarantineSeconds: "Quarantine (seconds)", softThreshold: "Soft threshold", hardThreshold: "Hard threshold", activeInterval: "Active interval", passiveInterval: "Audit interval", quarantineDuration: "Quarantine", minimumNodes: "Minimum nodes", unavailable: "Quality guard is not connected", unavailableHelp: "Enable qualityGuard in config.yaml and start the quality-guard Compose profile to display live status here.", testing: "Testing node quality", testComplete: "Test complete: {{speed}}", testFailed: "Quality test is temporarily unavailable. Try again shortly.", refreshNodes: "Refresh nodes", nodeEnabled: "Node enabled", nodeDisabled: "Node disabled", nodesEnabled: "Selected nodes enabled", nodesDisabled: "Selected nodes disabled", enableNode: "Enable node {{name}}", disableNode: "Disable node {{name}}", nodeEditorDescription: "Manage Grok Build egress used by the quality guard. Proxy URLs are write-only; leave the field blank while editing to keep the current value.", nodeCapacityHelp: "Maximum number of bound accounts; 0 means unlimited.", deleteNodeTitle: "Delete proxy node?", deleteNodeDescription: "Node “{{name}}” will be permanently deleted. This action cannot be undone.", deleteNodesTitle: "Delete {{count}} selected nodes?", deleteNodesDescription: "The selected proxy nodes will be permanently deleted. This action cannot be undone.",
nodesTab: "Node quality",
+ quarantinedTargets: "Quarantined targets",
+ leaseScopedObserveOnly: "Lease-scoped (observe only)",
+ leaseScoped: "Lease-scoped",
+ leaseQuarantined: "{{count}} lease quarantined",
+ leaseScopedHelp: "This node renders a different sticky lease per account. An anomalous request quarantines only that account lease, never the shared node.",
+ leaseScopedObserveOnlyHelp: "The anomaly lacks an account identity or the lease API is unavailable, so it is observed without disabling the shared node.",
+ leaseQuarantinedEvent: "Account lease quarantined",
+ leaseRestoredEvent: "Account lease restored",
+ leaseQuarantineExtendedEvent: "Account lease quarantine extended",
+ leaseQuarantineFailedEvent: "Account lease quarantine not applied",
+ leaseScopedQuarantineSuppressedEvent: "Whole-node quarantine prevented",
+ leaseScopedGuardReleasedEvent: "Legacy whole-node quarantine released",
+ leaseScopedNodeReason: "Node contains multiple account-specific sticky leases",
+ accountLease: "Account {{id}}",
+ leaseUntil: "Quarantined until {{time}}",
+ statisticsSuppressedActionDetail: "Restored {{restored}}, suppressed {{suppressed}}",
profilesTab: "Probe profiles",
profilesHelp: "Prompt and expected marker for active quality probes. A missing marker is a hard failure.",
profileActive: "In use",
diff --git a/tools/egress-quality-guard/README.md b/tools/egress-quality-guard/README.md
index 2b37b06c5..7c5d254b5 100644
--- a/tools/egress-quality-guard/README.md
+++ b/tools/egress-quality-guard/README.md
@@ -43,6 +43,20 @@ your own traffic before allowing automatic quarantine.
records a generic connectivity probe for diagnosis, then uses the real
model-quality probe as the authority before re-enabling the node.
+Account-bound proxy templates such as Resin usernames containing `{account}`
+render a distinct sticky lease for each account. Scheduled node probes remain
+suppressed because one lease cannot represent its siblings. A passive anomaly
+removes only the audited account lease; after the hold, recovery pins a probe to
+that same account and node, renews an unhealthy hold, and clears the durable
+marker only with a matching CAS version. Routing stops enforcing a hold after
+its deadline, so a stopped sidecar cannot strand an account indefinitely.
+Rebinding an account atomically removes its old marker. If identity or the lease
+API is unavailable, the guard falls back to observation and never disables the
+shared node. Rendered proxy usernames and credentials never cross the API.
+Lease reconciliation uses opaque keyset pagination and scans the complete
+durable set. Recovery probes are capped per cycle and retry with exponential
+backoff, so a large expired queue cannot monopolize one guard cycle.
+
The public inference API cannot request a specific egress node or bypass a
disabled node. This capability is confined to the authenticated internal route.
Ambiguous probe-only 403 responses do not cool borrowed accounts; definitive
@@ -112,6 +126,10 @@ probe prompt, or model response body.
- Never deletes a node or changes account bindings.
- Never restores a node disabled by an operator.
+- Never applies whole-node quarantine to an account-bound `{account}` proxy. A
+ legacy quarantine still owned by the guard is released during reconciliation.
+- Lease recovery is pinned to the same account and node and uses an opaque CAS
+ version so stale probes cannot clear a newer quarantine.
- Refuses to quarantine below `qualityGuard.minimumHealthyNodes`.
- Strict mode overrides that floor rather than scheduling an unverified exit.
- Uses an exclusive process lock to prevent duplicate guards.
diff --git a/tools/egress-quality-guard/README.zh-CN.md b/tools/egress-quality-guard/README.zh-CN.md
index 6f4b07b14..2e8d2e239 100644
--- a/tools/egress-quality-guard/README.zh-CN.md
+++ b/tools/egress-quality-guard/README.zh-CN.md
@@ -23,6 +23,15 @@ Token/s,因此建议先观察 JSON 日志,再根据实际流量调整阈值
6. 隔离节点仍可接受管理员探测,但不会承载普通用户请求。
7. 冷却结束后记录一次通用连接探测用于诊断,再以真实模型质量探测作为恢复判据,账号绑定保持不变。
+Resin 用户名等代理模板包含 `{account}` 时,同一逻辑节点会按账号生成不同的粘性租约。
+这类节点不会执行无法代表全部租约的定时节点探测;被动审计出现异常时,后端只临时移出该
+审计关联的账号租约。冷却到期后,恢复探针固定使用同一账号与同一节点;异常会续期,健康时
+通过版本校验清理持久化标记。路由在隔离期限到期后不再强制摘流,避免 sidecar 停止时让孤儿
+状态永久卡住账号。账号换绑会原子清理旧标记;sidecar 或接口异常时只记录观测,绝不回退为
+整节点禁用。内部接口只传账号 ID、节点 ID 和随机版本号,不返回渲染后的 Resin 用户名或代理凭据。
+租约对账采用不透明游标完整分页;每轮恢复探针有固定上限,失败后按指数退避,避免大量到期租约
+长期占满单次守护循环。
+
普通 `/v1/*` 请求不能指定出口节点,也不能绕过节点禁用状态。
仅发生在质量探测中的模糊 403 不会冷却借用账号;明确的凭据失效、账号封禁和额度信号仍按原有规则处理。
@@ -70,6 +79,7 @@ Webhook,确认出口发生变化,再执行一次真实模型质量检测;
- 不删除节点,不修改账号绑定。
- 不会恢复管理员手动禁用的节点。
+- 不会对账号绑定的 `{account}` 代理执行整节点隔离;升级前仍由守护程序持有的旧隔离会在状态对账时解除。
- 启用节点数低于 `qualityGuard.minimumHealthyNodes` 时拒绝继续隔离。
- 严格模式会覆盖最低健康节点保护:无法确认质量时宁可无可用节点,也不调度可疑出口。
- 使用进程锁防止重复运行。
diff --git a/tools/egress-quality-guard/quality_guard.py b/tools/egress-quality-guard/quality_guard.py
index 584d10b6a..0eb25be73 100755
--- a/tools/egress-quality-guard/quality_guard.py
+++ b/tools/egress-quality-guard/quality_guard.py
@@ -47,6 +47,11 @@
QUALITY_MARKER_PROFILE_ID = "quality-marker"
THROUGHPUT_PROFILE_ID = "throughput"
THINKING_GUARD_MIN_OUTPUT_TOKENS = 64
+LEASE_PAGE_SIZE = 1000
+LEASE_SCAN_MAX_PAGES = 1000
+LEASE_RECOVERY_MAX_PER_CYCLE = 8
+LEASE_RECOVERY_BACKOFF_BASE_SECONDS = 30
+LEASE_RECOVERY_BACKOFF_MAX_SECONDS = 1800
class GuardDisabled(RuntimeError):
@@ -321,8 +326,10 @@ def fixed_fallback_node_ids(self) -> set[str]:
result.add(node_id)
return result
- def quality_test(self, node_id: str, profile_id: str = "") -> dict[str, Any]:
+ def quality_test(self, node_id: str, profile_id: str = "", account_id: str = "") -> dict[str, Any]:
body = {"profileId": profile_id} if profile_id else {}
+ if account_id:
+ body["accountId"] = account_id
return self._request("POST", f"{INTERNAL_API_PREFIX}/egress-nodes/{node_id}/quality-test", body or None)
def connectivity_test(self, node_id: str) -> dict[str, Any]:
@@ -342,6 +349,42 @@ def set_enabled(self, node_id: str, enabled: bool) -> int:
result = self._request("PATCH", f"{INTERNAL_API_PREFIX}/egress-nodes/batch", {"ids": [node_id], "enabled": enabled})
return int(result.get("updated") or 0)
+ def list_leases(self) -> list[dict[str, Any]]:
+ values: list[dict[str, Any]] = []
+ cursor = ""
+ seen_cursors: set[str] = set()
+ for _page in range(LEASE_SCAN_MAX_PAGES):
+ query = {"limit": LEASE_PAGE_SIZE}
+ if cursor:
+ query["cursor"] = cursor
+ payload = self._request("GET", f"{INTERNAL_API_PREFIX}/egress-leases?{urllib.parse.urlencode(query)}")
+ items = list(payload.get("items") or [])
+ values.extend(items)
+ if not payload.get("hasMore"):
+ return values
+ next_cursor = str(payload.get("nextCursor") or "")
+ if not next_cursor or next_cursor == cursor or next_cursor in seen_cursors:
+ raise RuntimeError("lease pagination did not advance")
+ seen_cursors.add(next_cursor)
+ cursor = next_cursor
+ raise RuntimeError("lease pagination exceeded the safety limit")
+
+ def quarantine_lease(self, node_id: str, account_id: str, reason: str) -> dict[str, Any]:
+ return self._request("POST", f"{INTERNAL_API_PREFIX}/egress-leases/quarantine", {
+ "nodeId": node_id,
+ "accountId": account_id,
+ "reason": reason,
+ "quarantineSeconds": self.config.quarantine_seconds,
+ })
+
+ def restore_lease(self, node_id: str, account_id: str, version: str) -> bool:
+ result = self._request("POST", f"{INTERNAL_API_PREFIX}/egress-leases/restore", {
+ "nodeId": node_id,
+ "accountId": account_id,
+ "version": version,
+ })
+ return bool(result.get("restored"))
+
def rotate_node(self, node_id: str, old_exit_ip: str = "") -> dict[str, Any]:
if not self.config.rotation_url:
raise RuntimeError("rotation endpoint is not configured")
@@ -517,6 +560,9 @@ def generation_window_ms(first_token_ms: int, duration_ms: int, reasoning_tokens
def default_node_state() -> dict[str, Any]:
return {
+ "observe_only": False,
+ "observe_only_reason": "",
+ "quarantined_lease_count": 0,
"active_soft_strikes": 0,
"passive_soft_strikes": 0,
"error_strikes": 0,
@@ -630,6 +676,8 @@ def __init__(self, config: Config, api: ApiClient):
self._resolved_node_ids = list(config.node_ids)
self.state.setdefault("started_at", time.time())
self.state.setdefault("recent_events", [])
+ self.state.setdefault("leases", {})
+ self.state.setdefault("lease_recovery", {})
ensure_statistics(self.state)
self._update_guard_metadata()
self._save()
@@ -678,6 +726,11 @@ def _state_for(self, node_id: str) -> dict[str, Any]:
current.setdefault(key, value)
return current
+ @staticmethod
+ def _is_lease_scoped(node: dict[str, Any]) -> bool:
+ """Return whether one logical node expands to account-specific sticky leases."""
+ return bool(node.get("accountBoundProxy"))
+
def _defer_no_account(self, state: dict[str, Any], node: dict[str, Any], now: float, event: str, **fields: Any) -> None:
state["last_probe_at"] = now
state["last_reason"] = "probe_no_account"
@@ -732,9 +785,213 @@ def _should_rotate(self, node_id: str, reason: str) -> bool:
def _probe_account_unavailable(exc: Exception) -> bool:
return isinstance(exc, ApiError) and exc.code == "egressQualityProbeNoAccount"
+ @staticmethod
+ def _lease_key(node_id: str, account_id: str) -> str:
+ return f"{node_id}:{account_id}"
+
+ def _clear_lease_recovery(self, key: str) -> None:
+ self.state.setdefault("lease_recovery", {}).pop(key, None)
+
+ def _defer_lease_recovery(self, key: str, now: float) -> None:
+ recovery = self.state.setdefault("lease_recovery", {})
+ current = recovery.get(key) or {}
+ failures = min(16, int(current.get("failures") or 0) + 1)
+ delay = min(LEASE_RECOVERY_BACKOFF_MAX_SECONDS, LEASE_RECOVERY_BACKOFF_BASE_SECONDS * (2 ** (failures - 1)))
+ recovery[key] = {"failures": failures, "next_attempt_at": now + delay}
+
+ def _quarantine_lease(self, node: dict[str, Any], audit_value: dict[str, Any], reason: str, now: float) -> None:
+ node_id = str(node.get("id") or "")
+ account_id = str(audit_value.get("accountId") or "")
+ state = self._state_for(node_id)
+ request_id = str(audit_value.get("requestId") or "")
+ if not account_id:
+ state.update({"observe_only": True, "observe_only_reason": "missing_account_identity", "last_reason": reason})
+ self._bump_statistic("actions", "suppressed")
+ append_state_event(self.state, "lease_quarantine_suppressed", node_id=node_id, node_name=node.get("name"), reason=reason, request_id=request_id)
+ log_event("lease_quarantine_suppressed", node_id=node_id, node_name=node.get("name"), reason=reason, cause="missing_account_identity")
+ return
+ try:
+ lease = self.api.quarantine_lease(node_id, account_id, reason)
+ except Exception as exc:
+ state.update({"observe_only": True, "observe_only_reason": "lease_api_unavailable", "last_reason": reason})
+ self._bump_statistic("actions", "suppressed")
+ append_state_event(self.state, "lease_quarantine_failed", node_id=node_id, node_name=node.get("name"), reason=reason, account_id=account_id, request_id=request_id)
+ log_event("lease_quarantine_failed", node_id=node_id, node_name=node.get("name"), reason=reason, error_type=type(exc).__name__)
+ return
+ key = self._lease_key(node_id, account_id)
+ leases = self.state.setdefault("leases", {})
+ already_quarantined = key in leases
+ leases[key] = lease
+ self._clear_lease_recovery(key)
+ state.update({"observe_only": False, "observe_only_reason": "", "last_reason": reason})
+ event = "lease_quarantine_extended" if already_quarantined else "lease_quarantined"
+ if not already_quarantined:
+ state["quarantined_lease_count"] = int(state.get("quarantined_lease_count", 0)) + 1
+ self._bump_statistic("actions", "quarantined")
+ append_state_event(self.state, event, node_id=node_id, node_name=node.get("name"), reason=reason, account_id=account_id, request_id=request_id, cooldown_until=lease.get("cooldownUntil"))
+ self._save()
+ log_event(event, node_id=node_id, node_name=node.get("name"), reason=reason, account_id=account_id, cooldown_until=lease.get("cooldownUntil"))
+
+ def _extend_lease(self, node: dict[str, Any], lease: dict[str, Any], reason: str) -> bool:
+ node_id = str(lease.get("nodeId") or node.get("id") or "")
+ account_id = str(lease.get("accountId") or "")
+ try:
+ replacement = self.api.quarantine_lease(node_id, account_id, reason)
+ except Exception as exc:
+ log_event("lease_quarantine_extension_failed", node_id=node_id, node_name=node.get("name"), reason=reason, error_type=type(exc).__name__)
+ return False
+ key = self._lease_key(node_id, account_id)
+ self.state.setdefault("leases", {})[key] = replacement
+ self._clear_lease_recovery(key)
+ append_state_event(self.state, "lease_quarantine_extended", node_id=node_id, node_name=node.get("name"), reason=reason, account_id=account_id, cooldown_until=replacement.get("cooldownUntil"))
+ log_event("lease_quarantine_extended", node_id=node_id, node_name=node.get("name"), reason=reason, account_id=account_id)
+ return True
+
+ def _recover_lease(self, node: dict[str, Any], lease: dict[str, Any], now: float) -> None:
+ node_id = str(lease.get("nodeId") or "")
+ account_id = str(lease.get("accountId") or "")
+ version = str(lease.get("version") or "")
+ if not node_id or not account_id or not version:
+ return
+ key = self._lease_key(node_id, account_id)
+ profile_id, profile = resolve_probe_profile(self.config.profiles_file, QUALITY_MARKER_PROFILE_ID)
+ self._bump_statistic("active", "total")
+ try:
+ result = self.api.quality_test(node_id, profile_id, account_id)
+ classification, reason = classify_result(result, self.config, profile)
+ except Exception as exc:
+ self._bump_statistic("active", "errors")
+ if not self._extend_lease(node, lease, "recovery_probe_error"):
+ self._defer_lease_recovery(key, now)
+ log_event("lease_recovery_probe_failed", node_id=node_id, node_name=node.get("name"), account_id=account_id, error_type=type(exc).__name__)
+ return
+ self._record_probe(node, result, classification, reason, now)
+ if classification != "healthy":
+ if not self._extend_lease(node, lease, reason):
+ self._defer_lease_recovery(key, now)
+ return
+ try:
+ restored = self.api.restore_lease(node_id, account_id, version)
+ except ApiError as exc:
+ if exc.code == "qualityLeaseConflict":
+ self._clear_lease_recovery(key)
+ log_event("lease_restore_stale", node_id=node_id, node_name=node.get("name"), account_id=account_id)
+ return
+ self._defer_lease_recovery(key, now)
+ log_event("lease_restore_failed", node_id=node_id, node_name=node.get("name"), account_id=account_id, error_type=type(exc).__name__)
+ return
+ except Exception as exc:
+ self._defer_lease_recovery(key, now)
+ log_event("lease_restore_failed", node_id=node_id, node_name=node.get("name"), account_id=account_id, error_type=type(exc).__name__)
+ return
+ if not restored:
+ self._defer_lease_recovery(key, now)
+ return
+ self.state.setdefault("leases", {}).pop(key, None)
+ self._clear_lease_recovery(key)
+ node_state = self._state_for(node_id)
+ node_state["quarantined_lease_count"] = max(0, int(node_state.get("quarantined_lease_count", 0)) - 1)
+ self._bump_statistic("actions", "restored")
+ append_state_event(self.state, "lease_restored", node_id=node_id, node_name=node.get("name"), reason="quality_probe_healthy", account_id=account_id)
+ log_event("lease_restored", node_id=node_id, node_name=node.get("name"), account_id=account_id, reason="quality_probe_healthy")
+
+ def _reconcile_leases(self, nodes: list[dict[str, Any]], now: float) -> bool:
+ node_by_id = {str(node.get("id") or ""): node for node in nodes}
+ try:
+ values = self.api.list_leases()
+ except Exception as exc:
+ log_event("lease_reconciliation_failed", error_type=type(exc).__name__)
+ return False
+ state_leases = self.state.setdefault("leases", {})
+ recovery_state = self.state.setdefault("lease_recovery", {})
+ backend_keys: set[str] = set()
+ due: list[tuple[dict[str, Any], dict[str, Any]]] = []
+ for node in nodes:
+ if self._is_lease_scoped(node):
+ self._state_for(str(node["id"]))["quarantined_lease_count"] = 0
+ for lease in values:
+ node_id = str(lease.get("nodeId") or "")
+ account_id = str(lease.get("accountId") or "")
+ if not node_id or not account_id:
+ continue
+ key = self._lease_key(node_id, account_id)
+ backend_keys.add(key)
+ state_leases[key] = lease
+ node = node_by_id.get(node_id)
+ if node is None or not self._is_lease_scoped(node):
+ continue
+ state = self._state_for(node_id)
+ state["observe_only"] = False
+ state["observe_only_reason"] = ""
+ state["quarantined_lease_count"] = int(state.get("quarantined_lease_count", 0)) + 1
+ if now >= float(lease.get("cooldownUntil") or 0):
+ retry = recovery_state.get(key) or {}
+ if now >= float(retry.get("next_attempt_at") or 0):
+ due.append((node, lease))
+ for key in list(state_leases):
+ if key not in backend_keys:
+ state_leases.pop(key, None)
+ recovery_state.pop(key, None)
+ for key in list(recovery_state):
+ if key not in backend_keys:
+ recovery_state.pop(key, None)
+ for node, lease in due[:LEASE_RECOVERY_MAX_PER_CYCLE]:
+ self._recover_lease(node, lease, now)
+ deferred = max(0, len(due) - LEASE_RECOVERY_MAX_PER_CYCLE)
+ if deferred:
+ log_event("lease_recovery_budget_exhausted", due=len(due), deferred=deferred, limit=LEASE_RECOVERY_MAX_PER_CYCLE)
+ return True
+
+ def _release_protected_leases(self, protected_node_ids: set[str], nodes: list[dict[str, Any]]) -> None:
+ if not protected_node_ids:
+ return
+ node_by_id = {str(node.get("id") or ""): node for node in nodes}
+ state_leases = self.state.setdefault("leases", {})
+ for key, lease in list(state_leases.items()):
+ node_id = str(lease.get("nodeId") or "")
+ if node_id not in protected_node_ids:
+ continue
+ account_id = str(lease.get("accountId") or "")
+ version = str(lease.get("version") or "")
+ try:
+ restored = self.api.restore_lease(node_id, account_id, version)
+ except Exception as exc:
+ log_event("protected_lease_release_failed", node_id=node_id, error_type=type(exc).__name__)
+ continue
+ if not restored:
+ continue
+ state_leases.pop(key, None)
+ node = node_by_id.get(node_id) or {}
+ state = self._state_for(node_id)
+ state["quarantined_lease_count"] = max(0, int(state.get("quarantined_lease_count", 0)) - 1)
+ self._bump_statistic("actions", "restored")
+ append_state_event(self.state, "lease_restored", node_id=node_id, node_name=node.get("name"), reason="fixed_fallback_node", account_id=account_id)
+ log_event("protected_lease_released", node_id=node_id, node_name=node.get("name"), account_id=account_id)
+
def _quarantine(self, nodes: list[dict[str, Any]], node: dict[str, Any], reason: str, now: float, recover_now: bool = True) -> None:
node_id = str(node["id"])
state = self._state_for(node_id)
+ if self._is_lease_scoped(node):
+ state.update({
+ "observe_only": True,
+ "observe_only_reason": "account_bound_proxy",
+ "last_reason": reason,
+ })
+ self._bump_statistic("actions", "suppressed")
+ append_state_event(
+ self.state,
+ "lease_scoped_quarantine_suppressed",
+ node_id=node_id,
+ node_name=node.get("name"),
+ reason=reason,
+ )
+ log_event(
+ "lease_scoped_quarantine_suppressed",
+ node_id=node_id,
+ node_name=node.get("name"),
+ reason=reason,
+ )
+ return
if not self._can_quarantine(nodes, node_id):
self._bump_statistic("actions", "suppressed")
log_event("quarantine_suppressed", node_id=node_id, node_name=node.get("name"), reason=reason, minimum_healthy=self.config.min_healthy_nodes)
@@ -993,13 +1250,80 @@ def _probe_quarantined(self, node: dict[str, Any], now: float) -> None:
def _prepare_nodes(self, now: float) -> tuple[list[dict[str, Any]], list[dict[str, Any]], set[str]]:
all_nodes = self.api.list_nodes()
+ lease_api_ready = self._reconcile_leases(all_nodes, now)
protected_node_ids = self.api.fixed_fallback_node_ids()
+ if lease_api_ready:
+ self._release_protected_leases(protected_node_ids, all_nodes)
previous_protected = set(str(value) for value in self.state.get("protected_node_ids", []))
if protected_node_ids != previous_protected:
self.state["protected_node_ids"] = sorted(protected_node_ids)
for node_id in sorted(protected_node_ids - previous_protected):
log_event("fixed_fallback_node_skipped", node_id=node_id)
state_nodes = self.state.setdefault("nodes", {})
+ release_failed_ids: set[str] = set()
+ # An account-bound proxy renders a different sticky lease for each
+ # account. Release any whole-node quarantine left by an older guard;
+ # current versions isolate the audited account lease instead.
+ for node in all_nodes:
+ node_id = str(node.get("id") or "")
+ if not node_id or not node.get("proxyConfigured"):
+ continue
+ existing = state_nodes.get(node_id)
+ if not self._is_lease_scoped(node):
+ if existing:
+ existing["observe_only"] = False
+ existing["observe_only_reason"] = ""
+ continue
+ state = self._state_for(node_id)
+ if not lease_api_ready:
+ state["observe_only"] = True
+ state["observe_only_reason"] = "lease_api_unavailable"
+ elif state.get("observe_only_reason") in {"account_bound_proxy", "lease_api_unavailable"}:
+ state["observe_only"] = False
+ state["observe_only_reason"] = ""
+ if not state.get("disabled_by_guard"):
+ continue
+ if not node.get("enabled"):
+ try:
+ updated = self.api.set_enabled(node_id, True)
+ except Exception as exc:
+ release_failed_ids.add(node_id)
+ log_event(
+ "lease_scoped_guard_release_failed",
+ node_id=node_id,
+ node_name=node.get("name"),
+ error_type=type(exc).__name__,
+ )
+ continue
+ if updated != 1:
+ release_failed_ids.add(node_id)
+ log_event(
+ "lease_scoped_guard_release_not_applied",
+ node_id=node_id,
+ node_name=node.get("name"),
+ updated=updated,
+ )
+ continue
+ node["enabled"] = True
+ self._bump_statistic("actions", "restored")
+ state.update({
+ "active_soft_strikes": 0,
+ "passive_soft_strikes": 0,
+ "error_strikes": 0,
+ "quarantined_until": 0.0,
+ "disabled_by_guard": False,
+ "last_reason": "",
+ "quarantine_source": "",
+ })
+ append_state_event(
+ self.state,
+ "lease_scoped_guard_released",
+ node_id=node_id,
+ node_name=node.get("name"),
+ reason="lease_scoped_node",
+ )
+ self._save()
+ log_event("lease_scoped_guard_released", node_id=node_id, node_name=node.get("name"))
# Making an enabled node a fixed fallback is an explicit operator
# override. Relinquish stale guard ownership before eligibility checks
# so strict mode cannot repeatedly attempt an invalid disable. A
@@ -1030,7 +1354,7 @@ def _prepare_nodes(self, now: float) -> tuple[list[dict[str, Any]], list[dict[st
tracked = bool((state_nodes.get(stale_id) or {}).get("disabled_by_guard"))
if stale_id not in present_ids or (stale_id not in managed_ids and not tracked):
del state_nodes[stale_id]
- skip_ids: set[str] = set()
+ skip_ids: set[str] = set(release_failed_ids)
if not nodes:
log_event("no_eligible_nodes")
return all_nodes, [], skip_ids
@@ -1067,6 +1391,8 @@ def _prepare_nodes(self, now: float) -> tuple[list[dict[str, Any]], list[dict[st
continue
if state.get("disabled_by_guard"):
skip_ids.add(node_id)
+ if self._is_lease_scoped(node):
+ continue
self._probe_quarantined(node, now)
return all_nodes, nodes, skip_ids
@@ -1076,6 +1402,9 @@ def run_active_cycle(self) -> None:
for node in nodes:
node_id = str(node["id"])
state = self._state_for(node_id)
+ if self._is_lease_scoped(node):
+ self._save()
+ continue
if node_id not in skip_ids and node.get("enabled") and not state.get("disabled_by_guard"):
self._probe_active(all_nodes, node, now)
self._save()
@@ -1175,15 +1504,18 @@ def _record_passive_audit(self, all_nodes: list[dict[str, Any]], node: dict[str,
duration_ms=int(audit_value.get("durationMs") or 0),
strikes=int(state.get("passive_soft_strikes", 0)),
)
- log_event(
- "passive_immediate_quarantine",
- node_id=node_id,
- node_name=node.get("name"),
- classification=classification,
- reason=reason,
- output_tps=round(speed, 3),
- )
- self._quarantine(all_nodes, node, reason, now, recover_now=False)
+ if self._is_lease_scoped(node):
+ self._quarantine_lease(node, audit_value, reason, now)
+ else:
+ log_event(
+ "passive_immediate_quarantine",
+ node_id=node_id,
+ node_name=node.get("name"),
+ classification=classification,
+ reason=reason,
+ output_tps=round(speed, 3),
+ )
+ self._quarantine(all_nodes, node, reason, now, recover_now=False)
def run_passive_cycle(self) -> None:
now = time.time()
diff --git a/tools/egress-quality-guard/quality_guard_test.py b/tools/egress-quality-guard/quality_guard_test.py
index d6b574b76..e6ef7d9c0 100644
--- a/tools/egress-quality-guard/quality_guard_test.py
+++ b/tools/egress-quality-guard/quality_guard_test.py
@@ -342,6 +342,28 @@ def test_fixed_fallback_nodes_are_discovered_from_operations_policy(self):
}
self.assertEqual(client.fixed_fallback_node_ids(), {"9", "11"})
+ def test_list_leases_uses_stable_cursor_until_complete(self):
+ client = quality_guard.ApiClient(config())
+ requested_cursors = []
+
+ def request(_method, path, _body=None):
+ query = quality_guard.urllib.parse.parse_qs(quality_guard.urllib.parse.urlparse(path).query)
+ cursor = (query.get("cursor") or [""])[0]
+ requested_cursors.append(cursor)
+ if not cursor:
+ return {"items": [{"accountId": "1"}], "hasMore": True, "nextCursor": "next-page"}
+ return {"items": [{"accountId": "2"}], "hasMore": False, "nextCursor": ""}
+
+ client._request = request
+ self.assertEqual([value["accountId"] for value in client.list_leases()], ["1", "2"])
+ self.assertEqual(requested_cursors, ["", "next-page"])
+
+ def test_list_leases_rejects_non_advancing_cursor(self):
+ client = quality_guard.ApiClient(config())
+ client._request = lambda *_args, **_kwargs: {"items": [], "hasMore": True, "nextCursor": "same"}
+ with self.assertRaises(RuntimeError):
+ client.list_leases()
+
class FakeApi:
def __init__(self, nodes, results, audit_pages=None, fixed_fallback_ids=None):
@@ -352,7 +374,9 @@ def __init__(self, nodes, results, audit_pages=None, fixed_fallback_ids=None):
self.enabled_calls = []
self.quality_calls = []
self.quality_profile_calls = []
+ self.quality_account_calls = []
self.rotation_calls = []
+ self.leases = {}
def list_nodes(self):
return self.nodes
@@ -360,9 +384,10 @@ def list_nodes(self):
def fixed_fallback_node_ids(self):
return set(self.fixed_fallback_ids)
- def quality_test(self, node_id, profile_id=""):
+ def quality_test(self, node_id, profile_id="", account_id=""):
self.quality_calls.append(node_id)
self.quality_profile_calls.append(profile_id)
+ self.quality_account_calls.append(account_id)
value = self.results.pop(0)
if isinstance(value, Exception):
raise value
@@ -388,6 +413,26 @@ def list_audits(self, _cursor=""):
return self.audit_pages.pop(0)
return {"items": [], "hasMore": False, "nextCursor": ""}
+ def list_leases(self):
+ return list(self.leases.values())
+
+ def quarantine_lease(self, node_id, account_id, reason):
+ key = f"{node_id}:{account_id}"
+ value = {
+ "nodeId": node_id, "accountId": account_id, "reason": reason,
+ "version": f"version-{len(self.leases) + 1:09d}", "cooldownUntil": time.time() + 300,
+ }
+ self.leases[key] = value
+ return value
+
+ def restore_lease(self, node_id, account_id, version):
+ key = f"{node_id}:{account_id}"
+ current = self.leases.get(key)
+ if current is None or current.get("version") != version:
+ raise quality_guard.ApiError(409, "qualityLeaseConflict", "stale")
+ del self.leases[key]
+ return True
+
class GuardTests(unittest.TestCase):
@staticmethod
@@ -444,6 +489,182 @@ def test_fixed_fallback_node_is_excluded_without_aborting_other_nodes(self):
self.assertEqual(api.quality_calls, ["2"])
self.assertEqual(guard.state["protected_node_ids"], ["1"])
+ def test_fixed_fallback_releases_existing_account_lease_without_probe(self):
+ with tempfile.TemporaryDirectory() as directory:
+ cfg = config(state_file=Path(directory) / "state.json", lock_file=Path(directory) / "lock", node_ids=("1",))
+ nodes = self.nodes(2)
+ nodes[0]["accountBoundProxy"] = True
+ api = FakeApi(nodes, [], fixed_fallback_ids={"1"})
+ api.leases["1:101"] = {
+ "nodeId": "1", "accountId": "101", "reason": "hard_tps",
+ "version": "lease-version-0001", "cooldownUntil": time.time() + 300,
+ }
+ guard = quality_guard.Guard(cfg, api)
+ guard.run_active_cycle()
+ self.assertEqual(api.leases, {})
+ self.assertEqual(api.quality_calls, [])
+ self.assertEqual(guard.state["statistics"]["actions"]["restored"], 1)
+ self.assertEqual(guard.state["recent_events"][-1]["event"], "lease_restored")
+
+ def test_account_bound_proxy_skips_scheduled_probe_and_stays_observable(self):
+ with tempfile.TemporaryDirectory() as directory:
+ cfg = config(
+ state_file=Path(directory) / "state.json",
+ lock_file=Path(directory) / "lock",
+ node_ids=("1",),
+ )
+ nodes = self.nodes(3)
+ nodes[0]["accountBoundProxy"] = True
+ api = FakeApi(nodes, [])
+ guard = quality_guard.Guard(cfg, api)
+ guard.run_active_cycle()
+
+ self.assertEqual(api.quality_calls, [])
+ self.assertEqual(api.enabled_calls, [])
+ self.assertTrue(nodes[0]["enabled"])
+ self.assertFalse(guard.state["nodes"]["1"]["observe_only"])
+ self.assertEqual(guard.state["nodes"]["1"]["observe_only_reason"], "")
+
+ def test_account_bound_proxy_records_passive_anomaly_without_node_quarantine(self):
+ with tempfile.TemporaryDirectory() as directory:
+ cfg = config(
+ state_file=Path(directory) / "state.json",
+ lock_file=Path(directory) / "lock",
+ mode="passive",
+ node_ids=("1",),
+ )
+ nodes = self.nodes(3)
+ nodes[0]["accountBoundProxy"] = True
+ api = FakeApi(nodes, [{"expectedMatched": True, "outputTokens": 100, "reasoningTokens": 40, "outputTokensPerSecond": 100}], [
+ {"items": [], "hasMore": False, "nextCursor": ""},
+ {"items": [self.audit("lease-hit", "1", 1200)], "hasMore": False, "nextCursor": ""},
+ ])
+ guard = quality_guard.Guard(cfg, api)
+ guard.run_passive_cycle()
+ guard.run_passive_cycle()
+
+ state = guard.state["nodes"]["1"]
+ self.assertEqual(api.enabled_calls, [])
+ self.assertTrue(nodes[0]["enabled"])
+ self.assertFalse(state["disabled_by_guard"])
+ self.assertEqual(state["last_classification"], "hard")
+ self.assertEqual(state["last_reason"], "hard_tps")
+ self.assertEqual(guard.state["statistics"]["actions"]["quarantined"], 1)
+ self.assertEqual(len(api.leases), 1)
+ self.assertEqual(
+ [event["event"] for event in guard.state["recent_events"]],
+ ["passive_audit_anomaly", "lease_quarantined"],
+ )
+ next(iter(api.leases.values()))["cooldownUntil"] = 0
+ guard.run_active_cycle()
+ self.assertEqual(api.quality_account_calls, ["101"])
+ self.assertEqual(api.leases, {})
+ self.assertEqual(guard.state["recent_events"][-1]["event"], "lease_restored")
+
+ def test_repeated_account_anomaly_extends_one_lease_without_double_counting(self):
+ with tempfile.TemporaryDirectory() as directory:
+ cfg = config(state_file=Path(directory) / "state.json", lock_file=Path(directory) / "lock", node_ids=("1",))
+ nodes = self.nodes(2)
+ nodes[0]["accountBoundProxy"] = True
+ api = FakeApi(nodes, [])
+ guard = quality_guard.Guard(cfg, api)
+ audit = {"accountId": "101"}
+
+ guard._quarantine_lease(nodes[0], audit, "hard_tps", time.time())
+ guard._quarantine_lease(nodes[0], audit, "hard_tps", time.time())
+
+ self.assertEqual(len(api.leases), 1)
+ self.assertEqual(guard.state["nodes"]["1"]["quarantined_lease_count"], 1)
+ self.assertEqual(guard.state["statistics"]["actions"]["quarantined"], 1)
+ self.assertEqual(guard.state["recent_events"][-1]["event"], "lease_quarantine_extended")
+
+ def test_due_lease_recovery_has_a_per_cycle_budget(self):
+ with tempfile.TemporaryDirectory() as directory:
+ cfg = config(state_file=Path(directory) / "state.json", lock_file=Path(directory) / "lock", node_ids=("1",))
+ nodes = self.nodes(2)
+ nodes[0]["accountBoundProxy"] = True
+ healthy = {"expectedMatched": True, "outputTokens": 100, "reasoningTokens": 40, "outputTokensPerSecond": 100}
+ api = FakeApi(nodes, [healthy] * 20)
+ for index in range(20):
+ account_id = str(1000 + index)
+ api.leases[f"1:{account_id}"] = {
+ "nodeId": "1", "accountId": account_id, "reason": "hard_tps",
+ "version": f"lease-version-{index:04d}", "cooldownUntil": 0,
+ }
+ guard = quality_guard.Guard(cfg, api)
+ guard.run_active_cycle()
+ self.assertEqual(len(api.quality_account_calls), quality_guard.LEASE_RECOVERY_MAX_PER_CYCLE)
+ self.assertEqual(len(api.leases), 20 - quality_guard.LEASE_RECOVERY_MAX_PER_CYCLE)
+ guard.run_active_cycle()
+ self.assertEqual(len(api.quality_account_calls), quality_guard.LEASE_RECOVERY_MAX_PER_CYCLE * 2)
+
+ def test_failed_lease_recovery_is_backed_off(self):
+ with tempfile.TemporaryDirectory() as directory:
+ cfg = config(state_file=Path(directory) / "state.json", lock_file=Path(directory) / "lock", node_ids=("1",))
+ nodes = self.nodes(2)
+ nodes[0]["accountBoundProxy"] = True
+ api = FakeApi(nodes, [RuntimeError("probe failed"), RuntimeError("must not run immediately")])
+ api.leases["1:101"] = {
+ "nodeId": "1", "accountId": "101", "reason": "hard_tps",
+ "version": "lease-version-0001", "cooldownUntil": 0,
+ }
+ api.quarantine_lease = mock.Mock(side_effect=RuntimeError("backend unavailable"))
+ guard = quality_guard.Guard(cfg, api)
+ guard.run_active_cycle()
+ guard.run_active_cycle()
+ self.assertEqual(api.quality_account_calls, ["101"])
+ retry = guard.state["lease_recovery"]["1:101"]
+ self.assertGreater(retry["next_attempt_at"], time.time())
+
+ def test_account_bound_proxy_releases_only_guard_owned_legacy_quarantine(self):
+ with tempfile.TemporaryDirectory() as directory:
+ cfg = config(
+ state_file=Path(directory) / "state.json",
+ lock_file=Path(directory) / "lock",
+ node_ids=("1",),
+ )
+ nodes = self.nodes(3)
+ nodes[0].update({"accountBoundProxy": True, "enabled": False})
+ api = FakeApi(nodes, [])
+ guard = quality_guard.Guard(cfg, api)
+ state = guard._state_for("1")
+ state.update({"disabled_by_guard": True, "last_reason": "hard_tps", "quarantined_until": time.time() + 300})
+ guard.run_active_cycle()
+
+ self.assertEqual(api.enabled_calls, [("1", True)])
+ self.assertTrue(nodes[0]["enabled"])
+ self.assertFalse(state["disabled_by_guard"])
+ self.assertFalse(state["observe_only"])
+ self.assertEqual(guard.state["statistics"]["actions"]["restored"], 1)
+ self.assertEqual(guard.state["recent_events"][-1]["event"], "lease_scoped_guard_released")
+
+ nodes[0]["enabled"] = False
+ guard.run_active_cycle()
+ self.assertEqual(api.enabled_calls, [("1", True)])
+ self.assertFalse(nodes[0]["enabled"])
+
+ def test_account_bound_proxy_keeps_ownership_when_legacy_release_fails(self):
+ with tempfile.TemporaryDirectory() as directory:
+ cfg = config(
+ state_file=Path(directory) / "state.json",
+ lock_file=Path(directory) / "lock",
+ node_ids=("1",),
+ )
+ nodes = self.nodes(3)
+ nodes[0].update({"accountBoundProxy": True, "enabled": False})
+ api = FakeApi(nodes, [])
+ api.set_enabled = mock.Mock(side_effect=RuntimeError("temporary backend failure"))
+ guard = quality_guard.Guard(cfg, api)
+ state = guard._state_for("1")
+ state.update({"disabled_by_guard": True, "last_reason": "hard_tps", "quarantined_until": 0})
+ guard.run_active_cycle()
+
+ api.set_enabled.assert_called_once_with("1", True)
+ self.assertEqual(api.quality_calls, [])
+ self.assertFalse(nodes[0]["enabled"])
+ self.assertTrue(state["disabled_by_guard"])
+ self.assertFalse(state["observe_only"])
+
def test_enabled_node_promoted_to_fixed_fallback_releases_guard_ownership(self):
with tempfile.TemporaryDirectory() as directory:
cfg = config(
@@ -990,7 +1211,7 @@ def audit(audit_id, node_id, output_tps, quality_probe=False):
"provider": "grok_build", "streaming": True,
"statusCode": 200, "firstTokenMs": 200, "durationMs": 200 + generation_ms,
"outputTokens": output_tokens, "reasoningTokens": min(100, max(0, output_tokens - 1)),
- "egressNodeId": node_id, "errorCode": None,
+ "accountId": "101", "egressNodeId": node_id, "errorCode": None,
}