agentloop-profiler 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- dashboard/value_view.py +105 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Payload-free resource declarations shared by dispatch wrappers and budgets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import asdict, dataclass
|
|
9
|
+
from decimal import Decimal, localcontext
|
|
10
|
+
from fractions import Fraction
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from agentloop.loop_types import StepInfo
|
|
14
|
+
|
|
15
|
+
TOKEN_PROVENANCE = frozenset(
|
|
16
|
+
{
|
|
17
|
+
"provider",
|
|
18
|
+
"tokenizer",
|
|
19
|
+
"user_supplied",
|
|
20
|
+
"estimated_words",
|
|
21
|
+
"legacy",
|
|
22
|
+
"unspecified",
|
|
23
|
+
"unavailable",
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
COST_PROVENANCE = frozenset(
|
|
27
|
+
{"provider_reported", "user_reported", "calculated", "estimated", "unavailable"}
|
|
28
|
+
)
|
|
29
|
+
RETRY_SOURCES = frozenset({"framework", "provider", "harness"})
|
|
30
|
+
EXACT_TOKENS = frozenset({"provider", "tokenizer", "user_supplied"})
|
|
31
|
+
COUNT_KEYS = ("model_calls", "tool_calls", "iterations", "retries")
|
|
32
|
+
METRIC_KEYS = (*COUNT_KEYS, "tokens", "cost_usd")
|
|
33
|
+
_MONEY_TEXT = re.compile(r"(?:0|[1-9][0-9]*)(?:\.[0-9]+)?\Z")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BudgetValidationError(ValueError):
|
|
37
|
+
"""A resource declaration is invalid; errors never include response payloads."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def count(value: Any, name: str, *, optional: bool = True) -> int | None:
|
|
41
|
+
if value is None and optional:
|
|
42
|
+
return None
|
|
43
|
+
if type(value) is not int or value < 0:
|
|
44
|
+
raise BudgetValidationError(f"{name} must be a nonnegative integer")
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def number(value: Any, name: str, *, optional: bool = True) -> float | None:
|
|
49
|
+
if value is None and optional:
|
|
50
|
+
return None
|
|
51
|
+
try:
|
|
52
|
+
if type(value) not in {int, float} or value < 0 or not math.isfinite(float(value)):
|
|
53
|
+
raise ValueError
|
|
54
|
+
except (ValueError, OverflowError):
|
|
55
|
+
raise BudgetValidationError(f"{name} must be finite and nonnegative") from None
|
|
56
|
+
return float(value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def amount(value: int | float) -> Fraction:
|
|
60
|
+
"""Accumulate declared decimal amounts exactly, avoiding float drift at a cap."""
|
|
61
|
+
number(value, "cost_usd", optional=False)
|
|
62
|
+
return Fraction(str(value))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def money_text(value: Fraction) -> str:
|
|
66
|
+
"""Render exact decimal-input arithmetic as portable JSON decimal text."""
|
|
67
|
+
with localcontext() as context:
|
|
68
|
+
context.prec = len(str(value.numerator)) + len(str(value.denominator)) + 5
|
|
69
|
+
text = format(Decimal(value.numerator) / Decimal(value.denominator), "f")
|
|
70
|
+
return text.rstrip("0").rstrip(".") if "." in text else text
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _boolean(value: Any, name: str) -> None:
|
|
74
|
+
if type(value) is not bool:
|
|
75
|
+
raise BudgetValidationError(f"{name} must be a boolean")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class Reservation:
|
|
80
|
+
"""Caller-declared conservative bounds, not proof of a provider billing cap."""
|
|
81
|
+
|
|
82
|
+
tokens: int | None = None
|
|
83
|
+
cost_usd: float | None = None
|
|
84
|
+
provenance: str = "unavailable"
|
|
85
|
+
pricing_known: bool = False
|
|
86
|
+
evidence_refs: tuple[str, ...] = ()
|
|
87
|
+
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
|
+
count(self.tokens, "reserved tokens")
|
|
90
|
+
number(self.cost_usd, "reserved cost")
|
|
91
|
+
if not isinstance(self.provenance, str) or self.provenance not in {
|
|
92
|
+
"upper_bound",
|
|
93
|
+
"estimated",
|
|
94
|
+
"unavailable",
|
|
95
|
+
}:
|
|
96
|
+
raise BudgetValidationError("unsupported reservation provenance")
|
|
97
|
+
_boolean(self.pricing_known, "pricing_known")
|
|
98
|
+
if not isinstance(self.evidence_refs, (tuple, list)) or any(
|
|
99
|
+
not isinstance(item, str) or not item for item in self.evidence_refs
|
|
100
|
+
):
|
|
101
|
+
raise BudgetValidationError("reservation evidence_refs must contain opaque identifiers")
|
|
102
|
+
object.__setattr__(self, "evidence_refs", tuple(sorted(set(self.evidence_refs))))
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def tokens_known(self) -> bool:
|
|
106
|
+
return self.tokens is not None and self.provenance == "upper_bound"
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def cost_known(self) -> bool:
|
|
110
|
+
return self.cost_usd is not None and self.provenance == "upper_bound" and self.pricing_known
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict[str, Any]:
|
|
113
|
+
return asdict(self)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class ResourceUsage:
|
|
118
|
+
"""One exclusive usage snapshot; streams must explicitly identify final usage."""
|
|
119
|
+
|
|
120
|
+
tokens: int | None = None
|
|
121
|
+
cost_usd: float | None = None
|
|
122
|
+
token_provenance: str | None = None
|
|
123
|
+
cost_provenance: str | None = None
|
|
124
|
+
pricing_known: bool = False
|
|
125
|
+
complete: bool = False
|
|
126
|
+
exclusive: bool = True
|
|
127
|
+
usage_id: str | None = None
|
|
128
|
+
|
|
129
|
+
def __post_init__(self) -> None:
|
|
130
|
+
count(self.tokens, "usage tokens")
|
|
131
|
+
number(self.cost_usd, "usage cost")
|
|
132
|
+
if self.token_provenance is not None and (
|
|
133
|
+
not isinstance(self.token_provenance, str)
|
|
134
|
+
or self.token_provenance not in TOKEN_PROVENANCE
|
|
135
|
+
):
|
|
136
|
+
raise BudgetValidationError("unsupported token provenance")
|
|
137
|
+
if self.cost_provenance is not None and (
|
|
138
|
+
not isinstance(self.cost_provenance, str) or self.cost_provenance not in COST_PROVENANCE
|
|
139
|
+
):
|
|
140
|
+
raise BudgetValidationError("unsupported cost provenance")
|
|
141
|
+
for name in ("pricing_known", "complete", "exclusive"):
|
|
142
|
+
_boolean(getattr(self, name), name)
|
|
143
|
+
if self.usage_id is not None and (not isinstance(self.usage_id, str) or not self.usage_id):
|
|
144
|
+
raise BudgetValidationError("usage_id must be an opaque nonempty string")
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def tokens_known(self) -> bool:
|
|
148
|
+
return (
|
|
149
|
+
self.complete
|
|
150
|
+
and self.exclusive
|
|
151
|
+
and self.tokens is not None
|
|
152
|
+
and self.token_provenance in EXACT_TOKENS
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def cost_known(self) -> bool:
|
|
157
|
+
return (
|
|
158
|
+
self.complete
|
|
159
|
+
and self.exclusive
|
|
160
|
+
and self.cost_usd is not None
|
|
161
|
+
and (
|
|
162
|
+
self.cost_provenance in {"provider_reported", "user_reported"}
|
|
163
|
+
or (
|
|
164
|
+
self.cost_provenance == "calculated"
|
|
165
|
+
and self.pricing_known
|
|
166
|
+
and self.tokens_known
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def to_dict(self) -> dict[str, Any]:
|
|
172
|
+
return asdict(self)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(frozen=True)
|
|
176
|
+
class DispatchOptions:
|
|
177
|
+
"""Safe per-binding admission hints; no call arguments or output content."""
|
|
178
|
+
|
|
179
|
+
reservation: Reservation | None = None
|
|
180
|
+
retry_source: str | None = None
|
|
181
|
+
step: StepInfo | None = None
|
|
182
|
+
|
|
183
|
+
def __post_init__(self) -> None:
|
|
184
|
+
if self.step is not None and type(self.step) is not StepInfo:
|
|
185
|
+
raise BudgetValidationError("step must be StepInfo")
|
|
186
|
+
if self.reservation is not None and type(self.reservation) is not Reservation:
|
|
187
|
+
raise BudgetValidationError("reservation must be a Reservation")
|
|
188
|
+
if self.retry_source is not None and (
|
|
189
|
+
not isinstance(self.retry_source, str) or self.retry_source not in RETRY_SOURCES
|
|
190
|
+
):
|
|
191
|
+
raise BudgetValidationError("unsupported retry source")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass(frozen=True)
|
|
195
|
+
class BudgetSnapshot:
|
|
196
|
+
"""A closed numeric schema prevents arbitrary payloads entering decision evidence."""
|
|
197
|
+
|
|
198
|
+
_json: str
|
|
199
|
+
|
|
200
|
+
def to_dict(self) -> dict[str, Any]:
|
|
201
|
+
return json.loads(self._json)
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def from_dict(cls, value: dict[str, Any]) -> BudgetSnapshot:
|
|
205
|
+
fields = {
|
|
206
|
+
"schema_version",
|
|
207
|
+
"scope",
|
|
208
|
+
"hard_spend_cap",
|
|
209
|
+
"deadline_enforcement",
|
|
210
|
+
"metered_boundaries",
|
|
211
|
+
"limits",
|
|
212
|
+
"committed",
|
|
213
|
+
"in_flight",
|
|
214
|
+
"refunded",
|
|
215
|
+
"unknown_usage",
|
|
216
|
+
"total_usage",
|
|
217
|
+
"retry_sources",
|
|
218
|
+
"status_codes",
|
|
219
|
+
"soft_exceeded",
|
|
220
|
+
"overshoot",
|
|
221
|
+
"deadline_remaining_s",
|
|
222
|
+
"last_usage",
|
|
223
|
+
"usage_error",
|
|
224
|
+
"reservation",
|
|
225
|
+
"budget_scope_id",
|
|
226
|
+
"spend_enforcement",
|
|
227
|
+
"token_enforcement",
|
|
228
|
+
"possible_spend_overshoot",
|
|
229
|
+
"unknown_usage_policy",
|
|
230
|
+
"soft_fraction",
|
|
231
|
+
}
|
|
232
|
+
if not isinstance(value, dict) or set(value) != fields or value["schema_version"] != "1.0":
|
|
233
|
+
raise BudgetValidationError("invalid budget snapshot fields")
|
|
234
|
+
if (
|
|
235
|
+
value["scope"] != "wrapped_dispatch"
|
|
236
|
+
or value["hard_spend_cap"] is not False
|
|
237
|
+
or value["deadline_enforcement"] != "cooperative_admission"
|
|
238
|
+
):
|
|
239
|
+
raise BudgetValidationError("unsupported budget guarantee")
|
|
240
|
+
if not isinstance(value["budget_scope_id"], str) or not value["budget_scope_id"]:
|
|
241
|
+
raise BudgetValidationError("budget scope must have an opaque identity")
|
|
242
|
+
if not isinstance(value["metered_boundaries"], list) or any(
|
|
243
|
+
not isinstance(item, str) or item not in {"model", "tool"}
|
|
244
|
+
for item in value["metered_boundaries"]
|
|
245
|
+
):
|
|
246
|
+
raise BudgetValidationError("invalid metered boundaries")
|
|
247
|
+
groups = {
|
|
248
|
+
"limits": (set(COUNT_KEYS) | {"tokens", "cost_usd", "deadline_at"}, True),
|
|
249
|
+
"committed": (
|
|
250
|
+
set(COUNT_KEYS) | {"metered_calls", "tokens_known", "cost_known_usd"},
|
|
251
|
+
False,
|
|
252
|
+
),
|
|
253
|
+
"in_flight": (
|
|
254
|
+
set(COUNT_KEYS)
|
|
255
|
+
| {
|
|
256
|
+
"metered_calls",
|
|
257
|
+
"tokens_reserved",
|
|
258
|
+
"cost_reserved_usd",
|
|
259
|
+
"unknown_token_reservations",
|
|
260
|
+
"unknown_cost_reservations",
|
|
261
|
+
},
|
|
262
|
+
False,
|
|
263
|
+
),
|
|
264
|
+
"refunded": (set(COUNT_KEYS) | {"tokens_bound", "cost_bound_usd"}, False),
|
|
265
|
+
"unknown_usage": ({"calls", "tokens", "cost", "tokens_held", "cost_held_usd"}, False),
|
|
266
|
+
"total_usage": ({"tokens", "cost_usd"}, True),
|
|
267
|
+
"overshoot": ({"tokens", "cost_usd"}, True),
|
|
268
|
+
"retry_sources": (set(RETRY_SOURCES), False),
|
|
269
|
+
}
|
|
270
|
+
for group, (keys, optional) in groups.items():
|
|
271
|
+
items = value[group]
|
|
272
|
+
if not isinstance(items, dict) or set(items) != keys:
|
|
273
|
+
raise BudgetValidationError("invalid budget counter group")
|
|
274
|
+
for key, item in items.items():
|
|
275
|
+
if optional and item is None:
|
|
276
|
+
continue
|
|
277
|
+
if key.endswith("usd"):
|
|
278
|
+
if not isinstance(item, str) or not _MONEY_TEXT.fullmatch(item):
|
|
279
|
+
raise BudgetValidationError("budget costs must be nonnegative decimal text")
|
|
280
|
+
elif key == "deadline_at":
|
|
281
|
+
number(item, key, optional=optional)
|
|
282
|
+
else:
|
|
283
|
+
count(item, key, optional=optional)
|
|
284
|
+
for key, limit in (("spend_enforcement", "cost_usd"), ("token_enforcement", "tokens")):
|
|
285
|
+
if value[key] not in ("not_configured", "shadow", "best_effort"):
|
|
286
|
+
raise BudgetValidationError("unsupported resource enforcement claim")
|
|
287
|
+
if (value[key] == "not_configured") != (value["limits"][limit] is None):
|
|
288
|
+
raise BudgetValidationError("resource enforcement must match configured limits")
|
|
289
|
+
expected_overshoot = None if value["limits"]["cost_usd"] is None else True
|
|
290
|
+
if value["possible_spend_overshoot"] is not expected_overshoot:
|
|
291
|
+
raise BudgetValidationError("the Python adapter cannot exclude spend overshoot")
|
|
292
|
+
if not isinstance(value["unknown_usage_policy"], str) or value[
|
|
293
|
+
"unknown_usage_policy"
|
|
294
|
+
] not in {"deny", "escalate", "monitor_only"}:
|
|
295
|
+
raise BudgetValidationError("invalid unknown-usage policy")
|
|
296
|
+
soft = number(value["soft_fraction"], "soft_fraction")
|
|
297
|
+
if soft is not None and soft > 1:
|
|
298
|
+
raise BudgetValidationError("soft_fraction must not exceed one")
|
|
299
|
+
number(value["deadline_remaining_s"], "deadline remaining")
|
|
300
|
+
codes = {
|
|
301
|
+
"ok",
|
|
302
|
+
"not_admitted",
|
|
303
|
+
"unknown_tokens",
|
|
304
|
+
"unknown_cost",
|
|
305
|
+
"bound_exceeded",
|
|
306
|
+
"deadline_expired",
|
|
307
|
+
"duplicate_usage",
|
|
308
|
+
"usage_conflict",
|
|
309
|
+
"usage_collector_error",
|
|
310
|
+
"clock_invalid",
|
|
311
|
+
}
|
|
312
|
+
codes |= {f"limit_{key}" for key in METRIC_KEYS}
|
|
313
|
+
if not isinstance(value["status_codes"], list) or any(
|
|
314
|
+
not isinstance(code, str) or code not in codes for code in value["status_codes"]
|
|
315
|
+
):
|
|
316
|
+
raise BudgetValidationError("invalid budget status")
|
|
317
|
+
if not isinstance(value["soft_exceeded"], list) or any(
|
|
318
|
+
key not in METRIC_KEYS for key in value["soft_exceeded"]
|
|
319
|
+
):
|
|
320
|
+
raise BudgetValidationError("invalid soft threshold metric")
|
|
321
|
+
if value["usage_error"] is not None and (
|
|
322
|
+
not isinstance(value["usage_error"], str)
|
|
323
|
+
or value["usage_error"]
|
|
324
|
+
not in {"collector_error", "collector_cancelled", "invalid_usage"}
|
|
325
|
+
):
|
|
326
|
+
raise BudgetValidationError("invalid usage diagnostic")
|
|
327
|
+
try:
|
|
328
|
+
if value["last_usage"] is not None:
|
|
329
|
+
ResourceUsage(**value["last_usage"])
|
|
330
|
+
if value["reservation"] is not None:
|
|
331
|
+
Reservation(**value["reservation"])
|
|
332
|
+
serialized = json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
333
|
+
except (TypeError, ValueError, OverflowError, RecursionError):
|
|
334
|
+
raise BudgetValidationError("invalid budget usage or reservation") from None
|
|
335
|
+
return cls(serialized)
|