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.
- responsible_request/__init__.py +94 -0
- responsible_request/_http.py +31 -0
- responsible_request/analysis.py +81 -0
- responsible_request/cache.py +59 -0
- responsible_request/client.py +169 -0
- responsible_request/config.py +266 -0
- responsible_request/controller.py +79 -0
- responsible_request/cost.py +96 -0
- responsible_request/estimator.py +113 -0
- responsible_request/helpers/__init__.py +21 -0
- responsible_request/helpers/batch.py +127 -0
- responsible_request/helpers/messages.py +18 -0
- responsible_request/helpers/params.py +18 -0
- responsible_request/helpers/structured.py +150 -0
- responsible_request/limiter.py +165 -0
- responsible_request/logfiles.py +120 -0
- responsible_request/logging.py +183 -0
- responsible_request/providers.py +105 -0
- responsible_request/py.typed +0 -0
- responsible_request/records.py +265 -0
- responsible_request/runs.py +116 -0
- responsible_request/sources.py +207 -0
- responsible_request/throttle.py +229 -0
- responsible_request/transport.py +376 -0
- responsible_request-0.4.0.dist-info/METADATA +319 -0
- responsible_request-0.4.0.dist-info/RECORD +28 -0
- responsible_request-0.4.0.dist-info/WHEEL +4 -0
- responsible_request-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Responsible LLM requests for research: polite (load-aware throttling), reproducible (request
|
|
2
|
+
logs, run manifests, decoding presets) and cost-safe (response cache, cost tracking, budgets).
|
|
3
|
+
Works with any OpenAI-compatible endpoint (LiteLLM, OpenRouter, vLLM, Ollama, OpenAI, ...).
|
|
4
|
+
|
|
5
|
+
Typical use::
|
|
6
|
+
|
|
7
|
+
import responsible_request as rr
|
|
8
|
+
|
|
9
|
+
client = rr.AsyncOpenAI(base_url=..., api_key=..., log=rr.LogConfig(sqlite="requests.db"))
|
|
10
|
+
response = await client.chat.completions.create(model=..., messages=..., **rr.reproducible())
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
14
|
+
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
from . import providers
|
|
18
|
+
from .analysis import load_records, load_runs
|
|
19
|
+
from .cache import ResponseCache
|
|
20
|
+
from .client import AsyncOpenAI, get_throttle, http_client
|
|
21
|
+
from .config import (
|
|
22
|
+
FIELD_GROUPS,
|
|
23
|
+
CacheConfig,
|
|
24
|
+
CostConfig,
|
|
25
|
+
LogConfig,
|
|
26
|
+
Price,
|
|
27
|
+
RunConfig,
|
|
28
|
+
ThrottleConfig,
|
|
29
|
+
)
|
|
30
|
+
from .controller import State, ThrottleController
|
|
31
|
+
from .cost import BudgetExceeded, CostTracker
|
|
32
|
+
from .estimator import LatencyEstimator, LoadEstimator
|
|
33
|
+
from .helpers import (
|
|
34
|
+
StructuredOutputError,
|
|
35
|
+
cached_system_message,
|
|
36
|
+
calibrate,
|
|
37
|
+
extract_json,
|
|
38
|
+
reproducible,
|
|
39
|
+
response_format_from_model,
|
|
40
|
+
run_batch,
|
|
41
|
+
run_batch_sync,
|
|
42
|
+
structured,
|
|
43
|
+
)
|
|
44
|
+
from .logging import remove_sinks, setup_logging
|
|
45
|
+
from .records import RequestRecord, tags
|
|
46
|
+
from .throttle import Throttle
|
|
47
|
+
from .transport import ThrottledTransport
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
__version__ = version("responsible-request")
|
|
51
|
+
except PackageNotFoundError: # pragma: no cover
|
|
52
|
+
__version__ = "0.0.0"
|
|
53
|
+
|
|
54
|
+
# Libraries should stay silent unless the application opts in (loguru convention).
|
|
55
|
+
logger.disable(__name__)
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"FIELD_GROUPS",
|
|
59
|
+
"AsyncOpenAI",
|
|
60
|
+
"BudgetExceeded",
|
|
61
|
+
"CacheConfig",
|
|
62
|
+
"CostConfig",
|
|
63
|
+
"CostTracker",
|
|
64
|
+
"LatencyEstimator",
|
|
65
|
+
"LoadEstimator",
|
|
66
|
+
"LogConfig",
|
|
67
|
+
"Price",
|
|
68
|
+
"RequestRecord",
|
|
69
|
+
"ResponseCache",
|
|
70
|
+
"RunConfig",
|
|
71
|
+
"State",
|
|
72
|
+
"StructuredOutputError",
|
|
73
|
+
"Throttle",
|
|
74
|
+
"ThrottleConfig",
|
|
75
|
+
"ThrottleController",
|
|
76
|
+
"ThrottledTransport",
|
|
77
|
+
"__version__",
|
|
78
|
+
"cached_system_message",
|
|
79
|
+
"calibrate",
|
|
80
|
+
"extract_json",
|
|
81
|
+
"get_throttle",
|
|
82
|
+
"http_client",
|
|
83
|
+
"load_records",
|
|
84
|
+
"load_runs",
|
|
85
|
+
"providers",
|
|
86
|
+
"remove_sinks",
|
|
87
|
+
"reproducible",
|
|
88
|
+
"response_format_from_model",
|
|
89
|
+
"run_batch",
|
|
90
|
+
"run_batch_sync",
|
|
91
|
+
"setup_logging",
|
|
92
|
+
"structured",
|
|
93
|
+
"tags",
|
|
94
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Resolve the HTTP library used by the installed ``openai`` SDK.
|
|
2
|
+
|
|
3
|
+
``openai<3`` is built on ``httpx``; ``openai>=3`` is built on its fork ``httpx2``. Both expose the
|
|
4
|
+
same transport API, so we pick whichever one ``openai.DefaultAsyncHttpxClient`` derives from.
|
|
5
|
+
|
|
6
|
+
The library's per-request ``INFO`` line ("HTTP Request: POST ...") is silenced, since every
|
|
7
|
+
request is already recorded by our transport. Lower the level again to get it back.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib
|
|
13
|
+
import logging
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
import openai
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve() -> ModuleType:
|
|
20
|
+
for base in openai.DefaultAsyncHttpxClient.__mro__:
|
|
21
|
+
root = base.__module__.split(".")[0]
|
|
22
|
+
if root in ("httpx", "httpx2"):
|
|
23
|
+
return importlib.import_module(root)
|
|
24
|
+
return importlib.import_module("httpx") # pragma: no cover
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
httpx = _resolve()
|
|
28
|
+
|
|
29
|
+
logging.getLogger(httpx.__name__).setLevel(logging.WARNING)
|
|
30
|
+
|
|
31
|
+
__all__ = ["httpx"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Read logged request records back for analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .logfiles import log_segments, open_binary, strip_compression
|
|
11
|
+
from .logging import runs_path
|
|
12
|
+
from .records import JSON_FIELDS
|
|
13
|
+
from .runs import RUN_JSON_FIELDS
|
|
14
|
+
|
|
15
|
+
_JSONL_SUFFIXES = (".jsonl", ".json", ".log")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _is_jsonl(path: Path) -> bool:
|
|
19
|
+
return strip_compression(path).suffix in _JSONL_SUFFIXES
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_records(path: str | Path, *, as_dataframe: bool | None = None) -> Any:
|
|
23
|
+
"""Load request records from a JSONL file or SQLite database written by this package.
|
|
24
|
+
|
|
25
|
+
For a JSONL log, the segments rotated away from it are read too (oldest first, compressed or
|
|
26
|
+
not). A compressed file (e.g. ``requests.jsonl.zst``) is read on its own.
|
|
27
|
+
|
|
28
|
+
Returns a pandas DataFrame if pandas is installed (or ``as_dataframe=True``), otherwise a
|
|
29
|
+
list of dicts.
|
|
30
|
+
"""
|
|
31
|
+
path = Path(path)
|
|
32
|
+
if _is_jsonl(path):
|
|
33
|
+
rows = [row for segment in log_segments(path) for row in _read_jsonl(segment)]
|
|
34
|
+
else:
|
|
35
|
+
rows = _read_table(path, "requests", JSON_FIELDS)
|
|
36
|
+
return _output(rows, as_dataframe)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_runs(path: str | Path, *, as_dataframe: bool | None = None) -> Any:
|
|
40
|
+
"""Load run manifests (see :class:`responsible_request.RunConfig`) from the SQLite database,
|
|
41
|
+
or for a JSONL log (``requests.jsonl``) from ``requests.runs.jsonl`` next to it."""
|
|
42
|
+
path = Path(path)
|
|
43
|
+
if _is_jsonl(path):
|
|
44
|
+
runs = path if strip_compression(path).stem.endswith(".runs") else runs_path(path)
|
|
45
|
+
rows = _read_jsonl(runs) if runs.exists() else []
|
|
46
|
+
else:
|
|
47
|
+
rows = _read_table(path, "runs", RUN_JSON_FIELDS)
|
|
48
|
+
return _output(rows, as_dataframe)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
|
52
|
+
with open_binary(path) as fh:
|
|
53
|
+
return [json.loads(line) for line in fh if line.strip()]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_table(path: Path, table: str, json_fields: tuple[str, ...]) -> list[dict[str, Any]]:
|
|
57
|
+
conn = sqlite3.connect(path)
|
|
58
|
+
conn.row_factory = sqlite3.Row
|
|
59
|
+
try:
|
|
60
|
+
rows = [dict(r) for r in conn.execute(f"SELECT * FROM {table}")]
|
|
61
|
+
except sqlite3.OperationalError: # table does not exist (e.g. no runs in an old database)
|
|
62
|
+
rows = []
|
|
63
|
+
finally:
|
|
64
|
+
conn.close()
|
|
65
|
+
for row in rows:
|
|
66
|
+
for key in json_fields:
|
|
67
|
+
if isinstance(row.get(key), str):
|
|
68
|
+
row[key] = json.loads(row[key])
|
|
69
|
+
return rows
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _output(rows: list[dict[str, Any]], as_dataframe: bool | None) -> Any:
|
|
73
|
+
if as_dataframe is False:
|
|
74
|
+
return rows
|
|
75
|
+
try:
|
|
76
|
+
import pandas as pd
|
|
77
|
+
except ImportError:
|
|
78
|
+
if as_dataframe:
|
|
79
|
+
raise ImportError("install pandas: pip install responsible-request[pandas]") from None
|
|
80
|
+
return rows
|
|
81
|
+
return pd.DataFrame(rows)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Serve requests from previously logged request records."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .config import CacheConfig, LogConfig
|
|
10
|
+
from .sources import RecordSource, open_source
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cache_key(url: str, body: Any, salt: dict[str, Any] | None = None) -> str:
|
|
14
|
+
"""Hash of the request URL, its canonical JSON body and the salt (selected tag values)."""
|
|
15
|
+
payload = {"url": url, "body": body}
|
|
16
|
+
if salt:
|
|
17
|
+
payload["salt"] = salt
|
|
18
|
+
canonical = json.dumps(
|
|
19
|
+
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str
|
|
20
|
+
)
|
|
21
|
+
return hashlib.sha256(canonical.encode()).hexdigest()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ResponseCache:
|
|
25
|
+
"""Response lookup by cache key, backed by the records of a SQLite database or JSONL file."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: CacheConfig, log: LogConfig | None = None) -> None:
|
|
28
|
+
self.config = config
|
|
29
|
+
target: CacheConfig | LogConfig | None = config
|
|
30
|
+
if config.sqlite is None and config.jsonl is None: # read the records we write
|
|
31
|
+
target = log
|
|
32
|
+
if log is not None and not log.enabled("response_body"):
|
|
33
|
+
raise ValueError("the cache reads responses from the log: enable response_body")
|
|
34
|
+
source = open_source(target)
|
|
35
|
+
if source is None:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"the cache needs records to read from: set LogConfig(sqlite=...) or "
|
|
38
|
+
"LogConfig(jsonl=...), or CacheConfig(sqlite=...) or CacheConfig(jsonl=...)"
|
|
39
|
+
)
|
|
40
|
+
self._source: RecordSource = source
|
|
41
|
+
self.hits = 0
|
|
42
|
+
self.misses = 0
|
|
43
|
+
|
|
44
|
+
def salt(self, tags: dict[str, Any]) -> dict[str, Any]:
|
|
45
|
+
return {name: tags[name] for name in self.config.key_tags if name in tags}
|
|
46
|
+
|
|
47
|
+
def get(self, key: str) -> dict[str, Any] | None:
|
|
48
|
+
body = self._source.get(key)
|
|
49
|
+
if body is None:
|
|
50
|
+
self.misses += 1
|
|
51
|
+
else:
|
|
52
|
+
self.hits += 1
|
|
53
|
+
return body
|
|
54
|
+
|
|
55
|
+
def stats(self) -> dict[str, int]:
|
|
56
|
+
return {"hits": self.hits, "misses": self.misses}
|
|
57
|
+
|
|
58
|
+
def close(self) -> None:
|
|
59
|
+
self._source.close()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Drop-in OpenAI client and an ``http_client`` factory for existing code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import openai
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from .cache import ResponseCache
|
|
12
|
+
from .config import CacheConfig, CostConfig, LogConfig, RunConfig, ThrottleConfig
|
|
13
|
+
from .cost import BudgetExceeded, CostTracker
|
|
14
|
+
from .logging import setup_logging
|
|
15
|
+
from .providers import DEFAULT_EXTRACTORS, Extractor
|
|
16
|
+
from .runs import Run
|
|
17
|
+
from .throttle import Throttle
|
|
18
|
+
from .transport import ThrottledTransport
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def http_client(
|
|
22
|
+
throttle: ThrottleConfig | Throttle | None = None,
|
|
23
|
+
log: LogConfig | bool | None = True,
|
|
24
|
+
*,
|
|
25
|
+
default_params: Mapping[str, Any] | None = None,
|
|
26
|
+
inject_stream_usage: bool = True,
|
|
27
|
+
cache: CacheConfig | bool | None = None,
|
|
28
|
+
cost: CostConfig | bool | None = None,
|
|
29
|
+
run: RunConfig | str | None = None,
|
|
30
|
+
extractors: Iterable[Extractor] = (),
|
|
31
|
+
transport: Any = None,
|
|
32
|
+
**client_kwargs: Any,
|
|
33
|
+
) -> Any:
|
|
34
|
+
"""Build an async HTTP client for the OpenAI SDK that paces, measures and logs requests.
|
|
35
|
+
|
|
36
|
+
Use it to add throttling to existing code::
|
|
37
|
+
|
|
38
|
+
client = openai.AsyncOpenAI(http_client=rr.http_client(rr.ThrottleConfig(max_rpm=120)))
|
|
39
|
+
|
|
40
|
+
:param throttle: Throttle configuration (or an existing :class:`Throttle` to share state).
|
|
41
|
+
:param log: ``True`` for console output only, a :class:`LogConfig` to also write request
|
|
42
|
+
records to JSONL/SQLite, ``False``/``None`` to leave logging disabled.
|
|
43
|
+
:param default_params: Chat completion parameters added to every request that does not set
|
|
44
|
+
them itself, e.g. ``rr.reproducible()``.
|
|
45
|
+
:param inject_stream_usage: Request token usage for streamed chat completions.
|
|
46
|
+
:param cache: ``True`` or a :class:`CacheConfig` to answer requests that were already
|
|
47
|
+
answered successfully from the logged records instead of sending them again.
|
|
48
|
+
:param cost: ``True`` or a :class:`CostConfig` to track the cost of every request and
|
|
49
|
+
optionally enforce a budget.
|
|
50
|
+
:param run: A :class:`RunConfig` (or just a run name) for the run manifest written on the
|
|
51
|
+
first request. Every record carries the ``run_id``.
|
|
52
|
+
:param extractors: Additional response metadata extractors (see
|
|
53
|
+
:mod:`responsible_request.providers`), run after the built-in ones.
|
|
54
|
+
:param transport: Underlying transport (defaults to a plain async HTTP transport).
|
|
55
|
+
:param client_kwargs: Passed to ``openai.DefaultAsyncHttpxClient`` (e.g. ``timeout``).
|
|
56
|
+
"""
|
|
57
|
+
state = throttle if isinstance(throttle, Throttle) else Throttle(throttle)
|
|
58
|
+
log_config = LogConfig() if log is True else (log or None)
|
|
59
|
+
cache_config = CacheConfig() if cache is True else (cache or None)
|
|
60
|
+
response_cache = ResponseCache(cache_config, log_config) if cache_config is not None else None
|
|
61
|
+
cost_config = CostConfig() if cost is True else (cost or None)
|
|
62
|
+
tracker = CostTracker(cost_config, log_config) if cost_config is not None else None
|
|
63
|
+
run_config = RunConfig(name=run) if isinstance(run, str) else run
|
|
64
|
+
rr_run = Run(
|
|
65
|
+
run_config,
|
|
66
|
+
throttle=state.config,
|
|
67
|
+
log=log_config,
|
|
68
|
+
cache=cache_config,
|
|
69
|
+
cost=cost_config,
|
|
70
|
+
default_params=default_params,
|
|
71
|
+
)
|
|
72
|
+
if log_config is not None:
|
|
73
|
+
setup_logging(log_config)
|
|
74
|
+
rr_transport = ThrottledTransport(
|
|
75
|
+
state,
|
|
76
|
+
log=log_config,
|
|
77
|
+
default_params=default_params,
|
|
78
|
+
inject_stream_usage=inject_stream_usage,
|
|
79
|
+
cache=response_cache,
|
|
80
|
+
cost=tracker,
|
|
81
|
+
run=rr_run,
|
|
82
|
+
extractors=(*DEFAULT_EXTRACTORS, *extractors),
|
|
83
|
+
transport=transport,
|
|
84
|
+
)
|
|
85
|
+
client = openai.DefaultAsyncHttpxClient(transport=rr_transport, **client_kwargs)
|
|
86
|
+
client.rr_throttle = state # type: ignore[attr-defined]
|
|
87
|
+
client.rr_cache = response_cache # type: ignore[attr-defined]
|
|
88
|
+
client.rr_cost = tracker # type: ignore[attr-defined]
|
|
89
|
+
client.rr_run_id = rr_run.run_id # type: ignore[attr-defined]
|
|
90
|
+
return client
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_throttle(client: Any) -> Throttle:
|
|
94
|
+
"""Return the :class:`Throttle` behind an OpenAI client created with :func:`http_client`."""
|
|
95
|
+
throttle = getattr(client, "throttle", None)
|
|
96
|
+
if isinstance(throttle, Throttle):
|
|
97
|
+
return throttle
|
|
98
|
+
inner = getattr(client, "_client", client) # the SDK keeps its http client in ``_client``
|
|
99
|
+
throttle = getattr(inner, "rr_throttle", None)
|
|
100
|
+
if not isinstance(throttle, Throttle):
|
|
101
|
+
raise TypeError("client was not created with responsible_request")
|
|
102
|
+
return throttle
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class AsyncOpenAI(openai.AsyncOpenAI):
|
|
106
|
+
"""``openai.AsyncOpenAI`` with load-aware throttling and request logging.
|
|
107
|
+
|
|
108
|
+
Accepts every argument of ``openai.AsyncOpenAI`` plus ``throttle``, ``log``,
|
|
109
|
+
``default_params``, ``cache``, ``cost``, ``run`` and ``extractors`` (see :func:`http_client`).
|
|
110
|
+
The throttle state is available as ``client.throttle``; ``client.throttle.stats()`` shows the
|
|
111
|
+
current rate and load per model. ``client.cache.stats()`` counts cache hits and misses and
|
|
112
|
+
``client.cost.stats()`` shows the spent cost (both are None unless enabled).
|
|
113
|
+
``client.run_id`` identifies this client's records and run manifest.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
throttle: Throttle
|
|
117
|
+
cache: ResponseCache | None
|
|
118
|
+
cost: CostTracker | None
|
|
119
|
+
run_id: str
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
throttle: ThrottleConfig | Throttle | None = None,
|
|
125
|
+
log: LogConfig | bool | None = True,
|
|
126
|
+
default_params: Mapping[str, Any] | None = None,
|
|
127
|
+
cache: CacheConfig | bool | None = None,
|
|
128
|
+
cost: CostConfig | bool | None = None,
|
|
129
|
+
run: RunConfig | str | None = None,
|
|
130
|
+
extractors: Iterable[Extractor] = (),
|
|
131
|
+
http_client: Any = None,
|
|
132
|
+
**kwargs: Any,
|
|
133
|
+
) -> None:
|
|
134
|
+
if http_client is None:
|
|
135
|
+
http_client = _http_client(
|
|
136
|
+
throttle,
|
|
137
|
+
log,
|
|
138
|
+
default_params=default_params,
|
|
139
|
+
cache=cache,
|
|
140
|
+
cost=cost,
|
|
141
|
+
run=run,
|
|
142
|
+
extractors=extractors,
|
|
143
|
+
)
|
|
144
|
+
elif getattr(http_client, "rr_throttle", None) is None:
|
|
145
|
+
raise TypeError(
|
|
146
|
+
"pass throttle/log options instead of http_client, or build the http client "
|
|
147
|
+
"with responsible_request.http_client()"
|
|
148
|
+
)
|
|
149
|
+
super().__init__(http_client=http_client, **kwargs)
|
|
150
|
+
self.throttle = http_client.rr_throttle
|
|
151
|
+
self.cache = http_client.rr_cache
|
|
152
|
+
self.cost = http_client.rr_cost
|
|
153
|
+
self.run_id = http_client.rr_run_id
|
|
154
|
+
|
|
155
|
+
async def request(self, *args: Any, **kwargs: Any) -> Any:
|
|
156
|
+
try:
|
|
157
|
+
return await super().request(*args, **kwargs)
|
|
158
|
+
except openai.APIConnectionError as exc:
|
|
159
|
+
# older SDKs wrap exceptions raised by the transport
|
|
160
|
+
if isinstance(exc.__cause__, BudgetExceeded):
|
|
161
|
+
raise exc.__cause__ from None
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
async def close(self) -> None:
|
|
165
|
+
await super().close()
|
|
166
|
+
await logger.complete()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
_http_client = http_client
|