ratecap 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.
ratecap/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from ratecap.client import AllowResult, Client, Ticket
2
+ from ratecap.cost import estimate_llm_cost
3
+
4
+ __all__ = ["AllowResult", "Client", "Ticket", "estimate_llm_cost"]
ratecap/client.py ADDED
@@ -0,0 +1,153 @@
1
+ import ssl
2
+ import time
3
+ import urllib.parse
4
+ import urllib.request
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass
9
+ class AllowResult:
10
+ allowed: bool
11
+ retry_after_ms: int = 0
12
+
13
+
14
+ @dataclass
15
+ class _Reservation:
16
+ key: str
17
+ token: str
18
+
19
+
20
+ class Ticket:
21
+ def __init__(self, client, key, allowed, retry_after_ms=0, reservations=None):
22
+ self.allowed = allowed
23
+ self.retry_after_ms = retry_after_ms
24
+ self._client = client
25
+ self._key = key
26
+ self._reservations = reservations or []
27
+
28
+ def release(self):
29
+ errors = []
30
+ for reservation in self._reservations:
31
+ try:
32
+ self._client._release_one(reservation)
33
+ except Exception as exc:
34
+ errors.append(f"{reservation.key}: {exc}")
35
+ if errors:
36
+ raise RuntimeError("failed to release reservation(s): " + "; ".join(errors))
37
+
38
+ def refund(self, refund_amount):
39
+ url = f"{self._client._sidecar_addr}/release"
40
+ req = urllib.request.Request(
41
+ url,
42
+ method="POST",
43
+ headers={
44
+ "X-RateCap-Refund-Key": self._key,
45
+ "X-RateCap-Refund-Amount": str(refund_amount),
46
+ },
47
+ )
48
+ try:
49
+ with self._client._urlopen(req) as resp:
50
+ if resp.status != 200:
51
+ raise RuntimeError(f"refund failed with status {resp.status}")
52
+ except urllib.error.HTTPError as err:
53
+ raise RuntimeError(f"refund failed with status {err.code}") from err
54
+
55
+ def __enter__(self):
56
+ return self
57
+
58
+ def __exit__(self, exc_type, exc_val, exc_tb):
59
+ self.release()
60
+ return False
61
+
62
+
63
+ class Client:
64
+ def __init__(
65
+ self, sidecar_addr, timeout=5.0, max_retries=0, backoff_base=0.1, ca_file=None
66
+ ):
67
+ self._sidecar_addr = sidecar_addr.rstrip("/")
68
+ self._timeout = timeout
69
+ self._max_retries = max_retries
70
+ self._backoff_base = backoff_base
71
+ self._ssl_context = None
72
+ if ca_file is not None:
73
+ self._ssl_context = ssl.create_default_context(cafile=ca_file)
74
+
75
+ def _urlopen(self, req):
76
+ attempt = 0
77
+ while True:
78
+ try:
79
+ kwargs = {"timeout": self._timeout}
80
+ if self._ssl_context is not None:
81
+ kwargs["context"] = self._ssl_context
82
+ return urllib.request.urlopen(req, **kwargs)
83
+ except urllib.error.HTTPError:
84
+ raise
85
+ except Exception:
86
+ if attempt >= self._max_retries:
87
+ raise
88
+ time.sleep(self._backoff_base * (2**attempt))
89
+ attempt += 1
90
+
91
+ def allow(self, key, cost=None, priority=None):
92
+ params = {"key": key, "skip_reservations": "true"}
93
+ if cost is not None:
94
+ params["cost"] = str(cost)
95
+ query = urllib.parse.urlencode(params)
96
+ url = f"{self._sidecar_addr}/check?{query}"
97
+ headers = {"x-ratecap-priority": priority} if priority else {}
98
+ req = urllib.request.Request(url, method="GET", headers=headers)
99
+ try:
100
+ with self._urlopen(req) as resp:
101
+ return AllowResult(allowed=True)
102
+ except urllib.error.HTTPError as err:
103
+ retry_after_ms = int(err.headers.get("Retry-After-Ms", 0) or 0)
104
+ return AllowResult(allowed=False, retry_after_ms=retry_after_ms)
105
+
106
+ def acquire(self, key, cost=None, priority=None):
107
+ params = {"key": key}
108
+ if cost is not None:
109
+ params["cost"] = str(cost)
110
+ query = urllib.parse.urlencode(params)
111
+ url = f"{self._sidecar_addr}/check?{query}"
112
+ headers = {"x-ratecap-priority": priority} if priority else {}
113
+ req = urllib.request.Request(url, method="GET", headers=headers)
114
+ try:
115
+ with self._urlopen(req) as resp:
116
+ reservations = self._parse_reservations(resp.headers)
117
+ return Ticket(self, key, allowed=True, reservations=reservations)
118
+ except urllib.error.HTTPError as err:
119
+ reservations = self._parse_reservations(err.headers)
120
+ retry_after_ms = int(err.headers.get("Retry-After-Ms", 0) or 0)
121
+ return Ticket(
122
+ self,
123
+ key,
124
+ allowed=False,
125
+ retry_after_ms=retry_after_ms,
126
+ reservations=reservations,
127
+ )
128
+
129
+ def _parse_reservations(self, headers):
130
+ reservations = []
131
+ i = 0
132
+ while True:
133
+ token = headers.get(f"Concurrency-Token-{i}")
134
+ if not token:
135
+ break
136
+ key = headers.get(f"Concurrency-Key-{i}", "")
137
+ reservations.append(_Reservation(key=key, token=token))
138
+ i += 1
139
+ return reservations
140
+
141
+ def _release_one(self, reservation):
142
+ url = f"{self._sidecar_addr}/release"
143
+ req = urllib.request.Request(
144
+ url,
145
+ method="POST",
146
+ headers={
147
+ "X-RateCap-Concurrency-Key": reservation.key,
148
+ "X-RateCap-Concurrency-Token": reservation.token,
149
+ },
150
+ )
151
+ with self._urlopen(req) as resp:
152
+ if resp.status != 200:
153
+ raise RuntimeError(f"release failed with status {resp.status}")
ratecap/cost.py ADDED
@@ -0,0 +1,3 @@
1
+ def estimate_llm_cost(input_tokens, max_tokens):
2
+ cost = input_tokens + max_tokens
3
+ return max(cost, 0)
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: ratecap
3
+ Version: 0.1.0
4
+ Summary: Thin Python client for the RateCap sidecar
5
+ Project-URL: Homepage, https://github.com/sairam0424/RateCap
6
+ Project-URL: Repository, https://github.com/sairam0424/RateCap
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Requires-Python: >=3.10
@@ -0,0 +1,7 @@
1
+ ratecap/__init__.py,sha256=aw7Xl4bx1EzbDR7APDaNkVWqD0j2Hp3cTvwOeOueqj8,166
2
+ ratecap/client.py,sha256=LV-SzJphgzoqDxuKireuPMGs-gLcbe7CevCOuezG5DI,5298
3
+ ratecap/cost.py,sha256=7s7sEkrnk0nRB0og_UwuwwA6-GlFqlCE7xLiN4-FeHg,110
4
+ ratecap-0.1.0.dist-info/METADATA,sha256=YC02E7QB9OiZ3miKoNQLA4fQdCjOB1kiAiIZX0KNSu0,350
5
+ ratecap-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ ratecap-0.1.0.dist-info/top_level.txt,sha256=Iev0w-CZJjvnt3tcpvMjD2H86vVZCbd4aRuZPwb_Sjk,8
7
+ ratecap-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ ratecap