|
|
@@ -27,13 +27,17 @@ MAX_TOTAL_TOKENS_ENV = "AGENT_MAX_TOTAL_TOKENS"
|
|
|
MAX_TOTAL_COST_USD_ENV = "AGENT_MAX_TOTAL_COST_USD"
|
|
|
MAX_DURATION_SECONDS_ENV = "AGENT_MAX_DURATION_SECONDS"
|
|
|
RESERVED_FINAL_CALLS_ENV = "AGENT_RESERVED_FINAL_CALLS"
|
|
|
+MAX_VALIDATION_TOOL_CALLS_ENV = "AGENT_MAX_VALIDATION_TOOL_CALLS"
|
|
|
+MAX_VALIDATION_MATERIAL_CHARS_ENV = "AGENT_MAX_VALIDATION_MATERIAL_CHARS"
|
|
|
|
|
|
DEFAULT_MAX_TOTAL_AGENTS = 50
|
|
|
DEFAULT_MAX_LLM_CALLS = 150
|
|
|
DEFAULT_MAX_TOTAL_TOKENS = 1_500_000
|
|
|
DEFAULT_MAX_TOTAL_COST_USD = 15.0
|
|
|
DEFAULT_MAX_DURATION_SECONDS = 3_600
|
|
|
-DEFAULT_RESERVED_FINAL_CALLS = 1
|
|
|
+DEFAULT_RESERVED_FINAL_CALLS = 8
|
|
|
+DEFAULT_MAX_VALIDATION_TOOL_CALLS = 300
|
|
|
+DEFAULT_MAX_VALIDATION_MATERIAL_CHARS = 1_000_000
|
|
|
|
|
|
BudgetDimension = Literal[
|
|
|
"agents",
|
|
|
@@ -41,6 +45,8 @@ BudgetDimension = Literal[
|
|
|
"tokens",
|
|
|
"cost_usd",
|
|
|
"duration_seconds",
|
|
|
+ "validation_tool_calls",
|
|
|
+ "validation_material_chars",
|
|
|
]
|
|
|
LLMCallPurpose = Literal["ordinary", "root_validator"]
|
|
|
|
|
|
@@ -105,6 +111,8 @@ class ResourceBudget:
|
|
|
max_total_cost_usd: float = DEFAULT_MAX_TOTAL_COST_USD
|
|
|
max_duration_seconds: int = DEFAULT_MAX_DURATION_SECONDS
|
|
|
reserved_final_calls: int = DEFAULT_RESERVED_FINAL_CALLS
|
|
|
+ max_validation_tool_calls: int = DEFAULT_MAX_VALIDATION_TOOL_CALLS
|
|
|
+ max_validation_material_chars: int = DEFAULT_MAX_VALIDATION_MATERIAL_CHARS
|
|
|
|
|
|
def __post_init__(self) -> None:
|
|
|
if not isinstance(self.enabled, bool):
|
|
|
@@ -115,6 +123,8 @@ class ResourceBudget:
|
|
|
"max_total_tokens": self.max_total_tokens,
|
|
|
"max_duration_seconds": self.max_duration_seconds,
|
|
|
"reserved_final_calls": self.reserved_final_calls,
|
|
|
+ "max_validation_tool_calls": self.max_validation_tool_calls,
|
|
|
+ "max_validation_material_chars": self.max_validation_material_chars,
|
|
|
}
|
|
|
for name, value in integer_limits.items():
|
|
|
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
|
@@ -159,6 +169,16 @@ class ResourceBudget:
|
|
|
reserved_final_calls=_positive_int(
|
|
|
values, RESERVED_FINAL_CALLS_ENV, DEFAULT_RESERVED_FINAL_CALLS
|
|
|
),
|
|
|
+ max_validation_tool_calls=_positive_int(
|
|
|
+ values,
|
|
|
+ MAX_VALIDATION_TOOL_CALLS_ENV,
|
|
|
+ DEFAULT_MAX_VALIDATION_TOOL_CALLS,
|
|
|
+ ),
|
|
|
+ max_validation_material_chars=_positive_int(
|
|
|
+ values,
|
|
|
+ MAX_VALIDATION_MATERIAL_CHARS_ENV,
|
|
|
+ DEFAULT_MAX_VALIDATION_MATERIAL_CHARS,
|
|
|
+ ),
|
|
|
)
|
|
|
|
|
|
@classmethod
|
|
|
@@ -197,6 +217,8 @@ class ResourceUsage:
|
|
|
completion_tokens: int
|
|
|
total_tokens: int
|
|
|
total_cost_usd: float
|
|
|
+ validation_tool_calls: int
|
|
|
+ validation_material_chars: int
|
|
|
started_at: str
|
|
|
last_denial: dict[str, Any] | None = None
|
|
|
exhausted_reason: str | None = None
|
|
|
@@ -212,6 +234,8 @@ class ResourceUsage:
|
|
|
completion_tokens=0,
|
|
|
total_tokens=0,
|
|
|
total_cost_usd=0.0,
|
|
|
+ validation_tool_calls=0,
|
|
|
+ validation_material_chars=0,
|
|
|
started_at=(started_at or _utc_now()).isoformat(),
|
|
|
)
|
|
|
|
|
|
@@ -241,6 +265,8 @@ class ResourceUsage:
|
|
|
"prompt_tokens": self.prompt_tokens,
|
|
|
"completion_tokens": self.completion_tokens,
|
|
|
"total_tokens": self.total_tokens,
|
|
|
+ "validation_tool_calls": self.validation_tool_calls,
|
|
|
+ "validation_material_chars": self.validation_material_chars,
|
|
|
}
|
|
|
for name, value in counters.items():
|
|
|
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
|
@@ -266,6 +292,8 @@ class ResourceUsage:
|
|
|
"tokens",
|
|
|
"cost_usd",
|
|
|
"duration_seconds",
|
|
|
+ "validation_tool_calls",
|
|
|
+ "validation_material_chars",
|
|
|
}
|
|
|
if self.last_denial is not None:
|
|
|
if set(self.last_denial) != {"dimension", "detail", "denied_at"}:
|
|
|
@@ -531,6 +559,82 @@ class ResourceBudgetController:
|
|
|
),
|
|
|
)
|
|
|
|
|
|
+ async def record_validation_usage(
|
|
|
+ self,
|
|
|
+ root_trace_id: str,
|
|
|
+ budget: ResourceBudget,
|
|
|
+ *,
|
|
|
+ tool_calls: int = 0,
|
|
|
+ material_chars: int = 0,
|
|
|
+ provider_cost_usd: float = 0.0,
|
|
|
+ ) -> ResourceUsage:
|
|
|
+ """原子预留 Validator 工具调用或登记可信材料字符。
|
|
|
+
|
|
|
+ 搜索/打开在真实外部调用前用 ``tool_calls=1`` 预留;网页正文、
|
|
|
+ Artifact字符和 Provider 报告的成本事后登记,超限先落实际消耗。
|
|
|
+ """
|
|
|
+ for name, value in {
|
|
|
+ "tool_calls": tool_calls,
|
|
|
+ "material_chars": material_chars,
|
|
|
+ }.items():
|
|
|
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
|
+ raise ValueError(f"{name} must be a non-negative integer")
|
|
|
+ if (
|
|
|
+ isinstance(provider_cost_usd, bool)
|
|
|
+ or not isinstance(provider_cost_usd, (int, float))
|
|
|
+ or not math.isfinite(float(provider_cost_usd))
|
|
|
+ or provider_cost_usd < 0
|
|
|
+ ):
|
|
|
+ raise ValueError("provider_cost_usd must be a non-negative number")
|
|
|
+ if not tool_calls and not material_chars and not provider_cost_usd:
|
|
|
+ return await self.get_usage(root_trace_id)
|
|
|
+ async with self._lock(root_trace_id):
|
|
|
+ usage = await self.get_usage(root_trace_id)
|
|
|
+ self._raise_if_terminal(usage, budget)
|
|
|
+ await self._check_time_locked(root_trace_id, usage, budget)
|
|
|
+ proposed_calls = usage.validation_tool_calls + tool_calls
|
|
|
+ if budget.enabled and proposed_calls > budget.max_validation_tool_calls:
|
|
|
+ usage = await self._deny_locked(
|
|
|
+ root_trace_id,
|
|
|
+ usage,
|
|
|
+ "validation_tool_calls",
|
|
|
+ f"Validator tool calls would reach {proposed_calls}",
|
|
|
+ )
|
|
|
+ raise self._exceeded("validation_tool_calls", usage, budget)
|
|
|
+ usage.validation_tool_calls = proposed_calls
|
|
|
+ usage.validation_material_chars += material_chars
|
|
|
+ usage.total_cost_usd += float(provider_cost_usd)
|
|
|
+ exceeded: BudgetDimension | None = None
|
|
|
+ if (
|
|
|
+ budget.enabled
|
|
|
+ and usage.validation_material_chars
|
|
|
+ > budget.max_validation_material_chars
|
|
|
+ ):
|
|
|
+ exceeded = "validation_material_chars"
|
|
|
+ elif budget.enabled and usage.total_cost_usd > budget.max_total_cost_usd:
|
|
|
+ exceeded = "cost_usd"
|
|
|
+ if exceeded:
|
|
|
+ usage = await self._deny_locked(
|
|
|
+ root_trace_id,
|
|
|
+ usage,
|
|
|
+ exceeded,
|
|
|
+ (
|
|
|
+ "Validator material characters reached "
|
|
|
+ f"{usage.validation_material_chars}"
|
|
|
+ if exceeded == "validation_material_chars"
|
|
|
+ else "Validator provider cost reached "
|
|
|
+ f"{usage.total_cost_usd}"
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ await self.trace_store.replace_resource_usage(
|
|
|
+ root_trace_id,
|
|
|
+ usage.to_dict(),
|
|
|
+ )
|
|
|
+ if exceeded:
|
|
|
+ raise self._exceeded(exceeded, usage, budget)
|
|
|
+ return usage
|
|
|
+
|
|
|
@staticmethod
|
|
|
def _validate_reported_usage(
|
|
|
prompt_tokens: int,
|
|
|
@@ -665,6 +769,8 @@ class ResourceBudgetController:
|
|
|
"tokens": "tokens",
|
|
|
"cost_usd": "cost_usd",
|
|
|
"duration_seconds": "duration_seconds",
|
|
|
+ "validation_tool_calls": "validation_tool_calls",
|
|
|
+ "validation_material_chars": "validation_material_chars",
|
|
|
}
|
|
|
if dimension in terminal_dimensions:
|
|
|
typed_dimension = terminal_dimensions[dimension]
|