responsible-request 0.4.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.
@@ -0,0 +1,266 @@
1
+ """User-facing configuration objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from dataclasses import dataclass, field, replace
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any, Literal
9
+
10
+ from .logfiles import COMPRESSIONS, compression_of
11
+
12
+ if TYPE_CHECKING:
13
+ from .estimator import LoadEstimator
14
+ from .records import RequestRecord
15
+
16
+ Metric = Literal["latency_per_token", "latency", "ttfb"]
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ThrottleConfig:
21
+ """How requests are paced and how the pace adapts to the observed load.
22
+
23
+ Requests are spaced evenly at the current RPM (no bursts). The RPM starts at ``start_rpm``,
24
+ ramps up towards ``max_rpm`` while latency stays close to the baseline, and drops to
25
+ ``min_rpm`` as soon as latency reaches ``high_ratio`` x baseline (or the server returns
26
+ 429/5xx/timeouts).
27
+ """
28
+
29
+ max_rpm: float = 15
30
+ """Upper bound on requests per minute when the endpoint is idle."""
31
+ min_rpm: float = 1
32
+ """Requests per minute while other users are active. Also serves as probing rate."""
33
+ start_rpm: float = 2
34
+ """Initial rate while the latency baseline is being established (clamped to [min, max])."""
35
+ max_concurrency: int = 32
36
+ """Maximum number of in-flight requests per model, regardless of RPM."""
37
+
38
+ high_ratio: float = 3.0
39
+ """Throttle to ``min_rpm`` when current latency >= ``high_ratio`` x baseline."""
40
+ recover_ratio: float = 1.5
41
+ """Ramp up only while current latency < ``recover_ratio`` x baseline (dead band in between)."""
42
+ cooldown_s: float = 120
43
+ """Minimum time spent at ``min_rpm`` after the last high-load signal."""
44
+ ramp_factor: float = 1.5
45
+ """Multiplicative RPM increase per ramp step."""
46
+ ramp_interval_s: float = 30
47
+ """Minimum time between two ramp steps."""
48
+
49
+ metric: Metric | Callable[[RequestRecord], float | None] = "latency_per_token"
50
+ """Latency signal: ``latency_per_token`` (latency / max(completion tokens, ``min_tokens``);
51
+ uses time-to-first-byte for streamed requests), ``latency`` (raw), ``ttfb``, or a callable."""
52
+ min_tokens: int = 16
53
+ """Floor for the token count used by the ``latency_per_token`` metric."""
54
+ window: int = 10
55
+ """Number of recent observations whose median forms the current latency signal."""
56
+ warmup_requests: int = 20
57
+ """Observations required before the baseline is trusted and the RPM may ramp up."""
58
+ baseline: float | None = None
59
+ """Pin the baseline (in units of ``metric``) instead of estimating it."""
60
+ baseline_percentile: float = 10
61
+ """Baseline = this percentile of the observations inside ``baseline_window_s``."""
62
+ baseline_window_s: float = 1800
63
+ """Time span of observations used for the baseline (samples taken while throttled are
64
+ excluded, so the baseline freezes during high-load periods)."""
65
+
66
+ observe_paths: tuple[str, ...] = ("/chat/completions", "/embeddings")
67
+ """Endpoints whose latency feeds the load estimate. Other endpoints are paced and logged,
68
+ and react to 429/5xx, but do not adapt to latency."""
69
+ ignore_server_tools: bool = True
70
+ """Exclude requests using server-side tools (e.g. MCP, web search) from the load estimate,
71
+ since their latency includes external calls."""
72
+ adaptive: bool = True
73
+ """If False, requests are paced at a constant ``max_rpm``; a 429 then pauses the lane (see
74
+ ``backoff_initial_s``) instead of lowering the rate."""
75
+ backoff_initial_s: float = 1.0
76
+ """Fixed mode: pause after a 429 without a usable ``Retry-After`` header; doubles with every
77
+ further 429 until a request succeeds."""
78
+ backoff_max_s: float = 60.0
79
+ """Fixed mode: upper bound of that backoff (an explicit ``Retry-After`` is obeyed as is)."""
80
+ backoff_jitter_s: float = 1.0
81
+ """Fixed mode: random extra time in ``[0, backoff_jitter_s]`` added to every pause."""
82
+ estimator_factory: Callable[[ThrottleConfig], LoadEstimator] | None = None
83
+ """Custom load estimator (see :class:`responsible_request.estimator.LoadEstimator`)."""
84
+
85
+ def __post_init__(self) -> None:
86
+ if not 0 < self.min_rpm <= self.max_rpm:
87
+ raise ValueError("require 0 < min_rpm <= max_rpm")
88
+ if not 1 < self.recover_ratio < self.high_ratio:
89
+ raise ValueError("require 1 < recover_ratio < high_ratio")
90
+ if self.ramp_factor <= 1:
91
+ raise ValueError("ramp_factor must be > 1")
92
+ if self.max_concurrency < 1 or self.window < 1:
93
+ raise ValueError("max_concurrency and window must be >= 1")
94
+ if not 0 < self.baseline_percentile < 100:
95
+ raise ValueError("baseline_percentile must be in (0, 100)")
96
+ if not 0 <= self.backoff_initial_s <= self.backoff_max_s or self.backoff_jitter_s < 0:
97
+ raise ValueError("require 0 <= backoff_initial_s <= backoff_max_s and jitter >= 0")
98
+
99
+ @property
100
+ def initial_rpm(self) -> float:
101
+ if not self.adaptive:
102
+ return self.max_rpm
103
+ return min(self.max_rpm, max(self.min_rpm, self.start_rpm))
104
+
105
+ @classmethod
106
+ def fixed(cls, rpm: float, max_concurrency: int = 32, **kwargs: Any) -> ThrottleConfig:
107
+ """A plain rate limiter at a constant ``rpm`` (no load adaptation).
108
+
109
+ A 429 pauses the affected model for ``Retry-After`` (or an exponential backoff) plus
110
+ jitter. Further keyword arguments (e.g. ``backoff_max_s``) are passed to the constructor.
111
+ """
112
+ return cls(
113
+ max_rpm=rpm,
114
+ min_rpm=rpm,
115
+ start_rpm=rpm,
116
+ max_concurrency=max_concurrency,
117
+ adaptive=False,
118
+ **kwargs,
119
+ )
120
+
121
+
122
+ FIELD_GROUPS = (
123
+ "timing",
124
+ "usage",
125
+ "throttle",
126
+ "response_meta",
127
+ "params",
128
+ "request_body",
129
+ "response_body",
130
+ )
131
+ """Record field groups that can be toggled in :class:`LogConfig`. Core fields (id, timestamp,
132
+ path, model, status, error, tags) are always logged."""
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class LogConfig:
137
+ """Where and what to log.
138
+
139
+ The console only receives throttle changes, a periodic summary, and warnings. Every request
140
+ is written as one structured record to the configured JSONL file and/or SQLite database.
141
+ """
142
+
143
+ jsonl: str | Path | None = None
144
+ """Append one JSON object per request to this file."""
145
+ sqlite: str | Path | None = None
146
+ """Insert one row per request into the ``requests`` table of this SQLite database."""
147
+ fields: Mapping[str, bool] = field(default_factory=dict)
148
+ """Toggle record field groups (see ``FIELD_GROUPS``); groups not mentioned are logged."""
149
+ summary_interval_s: float | None = 60
150
+ """Emit an INFO summary line per model at most this often (None disables)."""
151
+ console_level: str | None = None
152
+ """If set, replace loguru's default stderr handler with one at this level. If None, the
153
+ console handlers are left untouched."""
154
+ rotation: str | int | None = "500 MB"
155
+ """Passed to loguru for the JSONL file."""
156
+ compression: str | None = None
157
+ """Compress JSONL segments once they are rotated away: ``"gz"``, ``"bz2"``, ``"xz"`` or
158
+ ``"zst"`` (zstd: Python 3.14+ or ``pip install responsible-request[zstd]``). Without
159
+ rotation, the file is compressed when its sink is removed. The cache, cost budget and
160
+ :func:`~responsible_request.load_records` read compressed segments too."""
161
+
162
+ def __post_init__(self) -> None:
163
+ unknown = set(self.fields) - set(FIELD_GROUPS)
164
+ if unknown:
165
+ raise ValueError(f"unknown field groups {sorted(unknown)}; valid: {FIELD_GROUPS}")
166
+ if self.compression is not None and self.compression not in COMPRESSIONS:
167
+ raise ValueError(f"unknown compression {self.compression!r}; valid: {COMPRESSIONS}")
168
+ if self.jsonl is not None and compression_of(Path(self.jsonl)):
169
+ raise ValueError("log to a plain .jsonl file and set compression= for its segments")
170
+
171
+ def enabled(self, group: str) -> bool:
172
+ return self.fields.get(group, True)
173
+
174
+ @classmethod
175
+ def minimal(cls, **kwargs: Any) -> LogConfig:
176
+ """Log timing, usage and throttle state, but no parameters or message contents."""
177
+ off = {"params": False, "request_body": False, "response_body": False}
178
+ return cls(fields=off, **kwargs)
179
+
180
+ @classmethod
181
+ def full(cls, **kwargs: Any) -> LogConfig:
182
+ """Log everything (the default)."""
183
+ return cls(**kwargs)
184
+
185
+ def with_fields(self, **toggles: bool) -> LogConfig:
186
+ return replace(self, fields={**self.fields, **toggles})
187
+
188
+
189
+ @dataclass(frozen=True)
190
+ class CacheConfig:
191
+ """Answer requests from previously logged records instead of sending them again.
192
+
193
+ A request is served from the cache if a successful (HTTP 200) record with the same cache key
194
+ exists. The key is a hash of the URL and the complete JSON request body (model, messages and
195
+ all parameters, after ``default_params`` are applied), plus the values of ``key_tags``.
196
+ Streamed requests are never served from the cache. Cache hits are logged with
197
+ ``cache_hit=True`` and neither wait for nor affect the throttle.
198
+ """
199
+
200
+ sqlite: str | Path | None = None
201
+ """Read cached responses from this SQLite database (defaults to ``LogConfig.sqlite``)."""
202
+ jsonl: str | Path | None = None
203
+ """Read cached responses from this JSONL file (defaults to ``LogConfig.jsonl``)."""
204
+ key_tags: tuple[str, ...] = ()
205
+ """Tags (see :func:`responsible_request.tags`) that are part of the cache key, e.g.
206
+ ``("sample",)`` to draw several independent samples for the same request."""
207
+
208
+
209
+ @dataclass(frozen=True)
210
+ class Price:
211
+ """Token prices in USD per million tokens (used when the response reports no cost)."""
212
+
213
+ input: float
214
+ output: float
215
+ cached_input: float | None = None
216
+ """Price of cached prompt tokens; defaults to ``input``."""
217
+
218
+ def cost(
219
+ self, prompt_tokens: int | None, completion_tokens: int | None, cached_tokens: int | None
220
+ ) -> float:
221
+ prompt, cached = prompt_tokens or 0, min(cached_tokens or 0, prompt_tokens or 0)
222
+ cached_price = self.input if self.cached_input is None else self.cached_input
223
+ total = (prompt - cached) * self.input + cached * cached_price
224
+ return (total + (completion_tokens or 0) * self.output) / 1_000_000
225
+
226
+
227
+ @dataclass(frozen=True)
228
+ class CostConfig:
229
+ """Track the cost of every request and optionally stop sending requests above a budget.
230
+
231
+ The cost of a request is the one reported by the provider (OpenRouter's ``usage.cost``, the
232
+ LiteLLM ``x-litellm-response-cost`` header, or a custom extractor), else it is computed from
233
+ ``prices`` and the token usage. Cache hits cost nothing.
234
+ """
235
+
236
+ budget_usd: float | None = None
237
+ """Refuse new requests (raising :class:`responsible_request.BudgetExceeded`) once this much
238
+ has been spent. Requests already in flight still complete, so the budget can be overshot by
239
+ their cost. Cache hits are always served."""
240
+ prices: Mapping[str, Price] = field(default_factory=dict)
241
+ """Fallback prices per model name, for providers that don't report a cost."""
242
+ include_logged: bool = True
243
+ """Count the cost already recorded in the request log (``LogConfig.sqlite``/``jsonl``), so
244
+ that restarting a script does not reset the budget."""
245
+
246
+ def __post_init__(self) -> None:
247
+ if self.budget_usd is not None and self.budget_usd < 0:
248
+ raise ValueError("budget_usd must be >= 0")
249
+
250
+
251
+ @dataclass(frozen=True)
252
+ class RunConfig:
253
+ """Describes a run: one client (or ``http_client``) from creation to close.
254
+
255
+ Every request record carries the ``run_id`` of its run. On the first request a run manifest
256
+ (versions, git commit, command line, all configurations, ``default_params``, ``metadata``) is
257
+ written to the ``runs`` table of the SQLite log, or to ``<name>.runs.jsonl`` next to the
258
+ JSONL log.
259
+ """
260
+
261
+ name: str | None = None
262
+ """Human-readable name of the run, e.g. the experiment."""
263
+ metadata: Mapping[str, Any] = field(default_factory=dict)
264
+ """Anything else worth recording (dataset version, hyperparameters, notes, ...)."""
265
+ capture_git: bool = True
266
+ """Record the git commit and dirty flag of the working directory."""
@@ -0,0 +1,79 @@
1
+ """Decide the target request rate from the load signal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+
10
+ from .config import ThrottleConfig
11
+
12
+
13
+ class State(str, Enum):
14
+ WARMUP = "warmup" # establishing the baseline at start_rpm
15
+ NORMAL = "normal" # endpoint looks idle: ramp up to max_rpm
16
+ THROTTLED = "throttled" # other users detected: stay at min_rpm for at least the cooldown
17
+ RECOVERING = "recovering" # load cleared: ramping back up
18
+ FIXED = "fixed" # adaptation disabled
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Transition:
23
+ old_state: State
24
+ new_state: State
25
+ old_rpm: float
26
+ new_rpm: float
27
+ reason: str
28
+
29
+
30
+ class ThrottleController:
31
+ """State machine with hysteresis mapping load ratios to a requests-per-minute target.
32
+
33
+ * ratio >= ``high_ratio`` or a congestion error: drop to ``min_rpm`` immediately.
34
+ * ``recover_ratio`` <= ratio < ``high_ratio``: hold the current rate (dead band).
35
+ * ratio < ``recover_ratio``: multiply the rate by ``ramp_factor`` every ``ramp_interval_s``
36
+ up to ``max_rpm`` (after the cooldown, if throttled).
37
+ """
38
+
39
+ def __init__(self, config: ThrottleConfig, clock: Callable[[], float] = time.monotonic) -> None:
40
+ self.config = config
41
+ self._clock = clock
42
+ self.state = State.WARMUP if config.adaptive else State.FIXED
43
+ self.rpm = config.initial_rpm
44
+ self._throttled_at = -float("inf")
45
+ self._last_ramp = clock()
46
+
47
+ def update(self, ratio: float | None, *, congested: bool = False) -> Transition | None:
48
+ """Process one observation. Returns the transition if the state or the rate changed."""
49
+ if self.state is State.FIXED:
50
+ return None
51
+ cfg, now = self.config, self._clock()
52
+ old_state, old_rpm = self.state, self.rpm
53
+ reason = ""
54
+
55
+ if congested or (ratio is not None and ratio >= cfg.high_ratio):
56
+ self._throttled_at = now
57
+ self.state, self.rpm = State.THROTTLED, cfg.min_rpm
58
+ reason = "server errors" if congested else f"load {ratio:.1f}x baseline"
59
+ elif self.state is State.WARMUP:
60
+ if ratio is not None:
61
+ self.state, self._last_ramp = State.NORMAL, now
62
+ reason = "baseline established"
63
+ elif self.state is State.THROTTLED:
64
+ cooled_down = now - self._throttled_at >= cfg.cooldown_s
65
+ if cooled_down and ratio is not None and ratio < cfg.recover_ratio:
66
+ self.state, self._last_ramp = State.RECOVERING, now
67
+ reason = f"load {ratio:.1f}x baseline, recovering"
68
+ elif ratio is not None and ratio < cfg.recover_ratio:
69
+ if now - self._last_ramp >= cfg.ramp_interval_s and self.rpm < cfg.max_rpm:
70
+ self.rpm = min(cfg.max_rpm, self.rpm * cfg.ramp_factor)
71
+ self._last_ramp = now
72
+ reason = f"load {ratio:.1f}x baseline, ramping up"
73
+ if self.state is State.RECOVERING and self.rpm >= cfg.max_rpm:
74
+ self.state = State.NORMAL
75
+ reason = reason or "fully recovered"
76
+
77
+ if self.state is old_state and self.rpm == old_rpm:
78
+ return None
79
+ return Transition(old_state, self.state, old_rpm, self.rpm, reason)
@@ -0,0 +1,96 @@
1
+ """Per-request cost and the budget guard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import openai
6
+ from loguru import logger
7
+
8
+ from .config import CostConfig, LogConfig
9
+ from .records import RequestRecord
10
+ from .sources import open_source
11
+
12
+
13
+ class BudgetExceeded(openai.OpenAIError):
14
+ """Raised instead of sending a request once ``CostConfig.budget_usd`` has been spent."""
15
+
16
+ def __init__(self, spent_usd: float, budget_usd: float) -> None:
17
+ super().__init__(f"budget of ${budget_usd:.4f} exhausted (${spent_usd:.4f} spent)")
18
+ self.spent_usd = spent_usd
19
+ self.budget_usd = budget_usd
20
+
21
+
22
+ class CostTracker:
23
+ """Adds up the cost of the requests of one client. Available as ``client.cost``."""
24
+
25
+ def __init__(self, config: CostConfig, log: LogConfig | None = None) -> None:
26
+ self.config = config
27
+ self.logged_usd = 0.0 # spent before this client was created, according to the log
28
+ if config.include_logged:
29
+ source = open_source(log)
30
+ if source is not None:
31
+ try:
32
+ self.logged_usd = source.total_cost()
33
+ finally:
34
+ source.close()
35
+ self.session_usd = 0.0
36
+ self.priced = 0
37
+ self.unpriced = 0
38
+ self._warned: set[str | None] = set()
39
+ self._exhausted_logged = False
40
+
41
+ @property
42
+ def spent_usd(self) -> float:
43
+ return self.logged_usd + self.session_usd
44
+
45
+ @property
46
+ def remaining_usd(self) -> float | None:
47
+ if self.config.budget_usd is None:
48
+ return None
49
+ return max(0.0, self.config.budget_usd - self.spent_usd)
50
+
51
+ def check(self) -> None:
52
+ """Raise :class:`BudgetExceeded` if the budget is exhausted."""
53
+ budget = self.config.budget_usd
54
+ if budget is None or self.spent_usd < budget:
55
+ return
56
+ if not self._exhausted_logged:
57
+ self._exhausted_logged = True
58
+ logger.warning(
59
+ "budget of ${:.4f} exhausted (${:.4f} spent): refusing new requests",
60
+ budget,
61
+ self.spent_usd,
62
+ )
63
+ raise BudgetExceeded(self.spent_usd, budget)
64
+
65
+ def add(self, record: RequestRecord) -> None:
66
+ """Fill in ``record.cost_usd`` if the provider did not report it, and count it."""
67
+ if record.cost_usd is None:
68
+ price = self.config.prices.get(record.model or "")
69
+ if price is None and record.response_model:
70
+ price = self.config.prices.get(record.response_model)
71
+ if price is not None and record.prompt_tokens is not None:
72
+ record.cost_usd = price.cost(
73
+ record.prompt_tokens, record.completion_tokens, record.cached_tokens
74
+ )
75
+ if record.cost_usd is not None:
76
+ self.priced += 1
77
+ self.session_usd += record.cost_usd
78
+ elif record.status_code == 200:
79
+ self.unpriced += 1
80
+ if self.config.budget_usd is not None and record.model not in self._warned:
81
+ self._warned.add(record.model)
82
+ logger.warning(
83
+ "{}: no cost reported and no price configured; the budget does not see "
84
+ "these requests (set CostConfig(prices=...))",
85
+ record.model or record.path,
86
+ )
87
+
88
+ def stats(self) -> dict[str, float | int | None]:
89
+ return {
90
+ "spent_usd": round(self.spent_usd, 6),
91
+ "session_usd": round(self.session_usd, 6),
92
+ "budget_usd": self.config.budget_usd,
93
+ "remaining_usd": self.remaining_usd,
94
+ "priced": self.priced,
95
+ "unpriced": self.unpriced,
96
+ }
@@ -0,0 +1,113 @@
1
+ """Load estimation: turn observed request latencies into a load ratio (current / baseline)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import statistics
7
+ import time
8
+ from collections import deque
9
+ from collections.abc import Callable
10
+ from typing import Protocol, runtime_checkable
11
+
12
+ from .config import ThrottleConfig
13
+ from .records import RequestRecord
14
+
15
+
16
+ @runtime_checkable
17
+ class LoadEstimator(Protocol):
18
+ """Anything that can judge the endpoint's load from completed requests.
19
+
20
+ Implement this to use a different signal (e.g. queue depth from a Prometheus endpoint) and
21
+ pass it via ``ThrottleConfig(estimator_factory=...)``.
22
+ """
23
+
24
+ def observe(self, record: RequestRecord, *, throttled: bool) -> None:
25
+ """Feed one successful, eligible request. ``throttled`` is True while at min RPM."""
26
+
27
+ def load_ratio(self) -> float | None:
28
+ """Current load relative to an idle endpoint (1.0 = idle); None while unknown."""
29
+
30
+ @property
31
+ def baseline(self) -> float | None: ...
32
+
33
+ def reset(self) -> None:
34
+ """Forget the learned baseline."""
35
+
36
+
37
+ def metric_value(record: RequestRecord, config: ThrottleConfig) -> float | None:
38
+ """Compute the latency signal of one request according to ``config.metric``."""
39
+ metric = config.metric
40
+ if callable(metric):
41
+ return metric(record)
42
+ if metric == "latency":
43
+ return record.latency_s
44
+ if metric == "ttfb":
45
+ return record.ttfb_s
46
+ # latency_per_token: streamed requests expose the queueing delay directly as TTFB.
47
+ if record.stream and record.ttfb_s is not None:
48
+ return record.ttfb_s
49
+ if record.latency_s is None:
50
+ return None
51
+ tokens = record.completion_tokens or 0
52
+ return record.latency_s / max(tokens, config.min_tokens)
53
+
54
+
55
+ def _percentile(values: list[float], pct: float) -> float:
56
+ ordered = sorted(values)
57
+ k = (len(ordered) - 1) * pct / 100
58
+ lo, hi = math.floor(k), math.ceil(k)
59
+ return ordered[lo] + (ordered[hi] - ordered[lo]) * (k - lo)
60
+
61
+
62
+ class LatencyEstimator:
63
+ """Compare the median of the last ``window`` observations to a low-percentile baseline.
64
+
65
+ The baseline is the ``baseline_percentile``-th percentile of observations made within
66
+ ``baseline_window_s``. Observations made while throttled are not added to the baseline, so a
67
+ long busy period cannot turn into the new "normal". The window is measured relative to the
68
+ newest baseline sample, which means the baseline freezes (rather than expires) meanwhile.
69
+ """
70
+
71
+ def __init__(self, config: ThrottleConfig, clock: Callable[[], float] = time.monotonic) -> None:
72
+ self.config = config
73
+ self._clock = clock
74
+ self._recent: deque[float] = deque(maxlen=config.window)
75
+ self._samples: deque[tuple[float, float]] = deque(maxlen=5000)
76
+ self._pinned: float | None = config.baseline
77
+
78
+ def observe(self, record: RequestRecord, *, throttled: bool) -> None:
79
+ value = metric_value(record, self.config)
80
+ if value is None or value <= 0:
81
+ return
82
+ self._recent.append(value)
83
+ if not throttled:
84
+ now = self._clock()
85
+ self._samples.append((now, value))
86
+ while self._samples and self._samples[0][0] < now - self.config.baseline_window_s:
87
+ self._samples.popleft()
88
+
89
+ @property
90
+ def baseline(self) -> float | None:
91
+ if self._pinned is not None:
92
+ return self._pinned
93
+ if len(self._samples) < self.config.warmup_requests:
94
+ return None
95
+ return _percentile([v for _, v in self._samples], self.config.baseline_percentile)
96
+
97
+ def current(self) -> float | None:
98
+ return statistics.median(self._recent) if self._recent else None
99
+
100
+ def load_ratio(self) -> float | None:
101
+ baseline, current = self.baseline, self.current()
102
+ if baseline is None or current is None or baseline <= 0:
103
+ return None
104
+ return current / baseline
105
+
106
+ def pin_baseline(self, value: float | None) -> None:
107
+ """Use ``value`` as the baseline (None returns to estimating it)."""
108
+ self._pinned = value
109
+
110
+ def reset(self) -> None:
111
+ self._pinned = self.config.baseline
112
+ self._samples.clear()
113
+ self._recent.clear()
@@ -0,0 +1,21 @@
1
+ from .batch import calibrate, run_batch, run_batch_sync
2
+ from .messages import cached_system_message
3
+ from .params import reproducible
4
+ from .structured import (
5
+ StructuredOutputError,
6
+ extract_json,
7
+ response_format_from_model,
8
+ structured,
9
+ )
10
+
11
+ __all__ = [
12
+ "StructuredOutputError",
13
+ "cached_system_message",
14
+ "calibrate",
15
+ "extract_json",
16
+ "reproducible",
17
+ "response_format_from_model",
18
+ "run_batch",
19
+ "run_batch_sync",
20
+ "structured",
21
+ ]