jev-lite 0.0.1__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.
- jev_lite/__init__.py +70 -0
- jev_lite/_client.py +364 -0
- jev_lite/_constants.py +19 -0
- jev_lite/_exceptions.py +189 -0
- jev_lite/_resource.py +18 -0
- jev_lite/_telemetry.py +105 -0
- jev_lite/_version.py +3 -0
- jev_lite/engine/__init__.py +30 -0
- jev_lite/engine/backbone.py +348 -0
- jev_lite/engine/evaluate.py +189 -0
- jev_lite/engine/frozen.py +105 -0
- jev_lite/engine/hashing.py +46 -0
- jev_lite/engine/head.py +57 -0
- jev_lite/engine/hf.py +131 -0
- jev_lite/engine/interaction.py +84 -0
- jev_lite/engine/letters.py +21 -0
- jev_lite/engine/lexical.py +45 -0
- jev_lite/engine/pairs.py +106 -0
- jev_lite/engine/prompt.py +48 -0
- jev_lite/engine/protocol.py +31 -0
- jev_lite/engine/render.py +32 -0
- jev_lite/engine/slots.py +77 -0
- jev_lite/engine/system_one.py +92 -0
- jev_lite/eval/__init__.py +12 -0
- jev_lite/eval/__main__.py +6 -0
- jev_lite/eval/ban.py +26 -0
- jev_lite/eval/collect.py +64 -0
- jev_lite/eval/collect_public.py +105 -0
- jev_lite/eval/decode.py +20 -0
- jev_lite/eval/distill.py +199 -0
- jev_lite/eval/env.py +20 -0
- jev_lite/eval/jevbench.py +199 -0
- jev_lite/eval/json_cache.py +45 -0
- jev_lite/eval/langs.py +103 -0
- jev_lite/eval/metrics.py +67 -0
- jev_lite/eval/ollama.py +113 -0
- jev_lite/eval/public_states.py +188 -0
- jev_lite/eval/questions_gen.py +107 -0
- jev_lite/eval/schema.py +39 -0
- jev_lite/eval/targets.py +22 -0
- jev_lite/eval/teacher_cache.py +116 -0
- jev_lite/eval/tokenizer_gate.py +60 -0
- jev_lite/eval/train_head.py +195 -0
- jev_lite/eval/translate_public.py +180 -0
- jev_lite/py.typed +0 -0
- jev_lite/resources/__init__.py +17 -0
- jev_lite/resources/models.py +99 -0
- jev_lite/resources/system_one.py +189 -0
- jev_lite/types.py +187 -0
- jev_lite-0.0.1.dist-info/METADATA +128 -0
- jev_lite-0.0.1.dist-info/RECORD +53 -0
- jev_lite-0.0.1.dist-info/WHEEL +4 -0
- jev_lite-0.0.1.dist-info/licenses/LICENSE +201 -0
jev_lite/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""jev-lite — OpenAI-style Pydantic client for TypeSafe Jev."""
|
|
2
|
+
|
|
3
|
+
from jev_lite._client import AsyncClient, AsyncTypeSafe, Client, TypeSafe
|
|
4
|
+
from jev_lite._exceptions import (
|
|
5
|
+
APIConnectionError,
|
|
6
|
+
APIError,
|
|
7
|
+
APIResponseValidationError,
|
|
8
|
+
APIStatusError,
|
|
9
|
+
APITimeoutError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
BadRequestError,
|
|
12
|
+
InternalServerError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
OverloadedError,
|
|
15
|
+
PermissionDeniedError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
TypeSafeError,
|
|
18
|
+
UnprocessableEntityError,
|
|
19
|
+
)
|
|
20
|
+
from jev_lite._telemetry import instrument
|
|
21
|
+
from jev_lite._version import __version__
|
|
22
|
+
from jev_lite.types import (
|
|
23
|
+
Choice,
|
|
24
|
+
ChoiceAnswer,
|
|
25
|
+
Model,
|
|
26
|
+
ModelList,
|
|
27
|
+
Noul,
|
|
28
|
+
NoulAnswer,
|
|
29
|
+
NoulCriteria,
|
|
30
|
+
Score,
|
|
31
|
+
ScoreAnswer,
|
|
32
|
+
SystemOneRequest,
|
|
33
|
+
SystemOneResponse,
|
|
34
|
+
Usage,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"APIConnectionError",
|
|
39
|
+
"APIError",
|
|
40
|
+
"APIResponseValidationError",
|
|
41
|
+
"APIStatusError",
|
|
42
|
+
"APITimeoutError",
|
|
43
|
+
"AsyncClient",
|
|
44
|
+
"AsyncTypeSafe",
|
|
45
|
+
"AuthenticationError",
|
|
46
|
+
"BadRequestError",
|
|
47
|
+
"Choice",
|
|
48
|
+
"ChoiceAnswer",
|
|
49
|
+
"Client",
|
|
50
|
+
"InternalServerError",
|
|
51
|
+
"Model",
|
|
52
|
+
"ModelList",
|
|
53
|
+
"NotFoundError",
|
|
54
|
+
"Noul",
|
|
55
|
+
"NoulAnswer",
|
|
56
|
+
"NoulCriteria",
|
|
57
|
+
"OverloadedError",
|
|
58
|
+
"PermissionDeniedError",
|
|
59
|
+
"RateLimitError",
|
|
60
|
+
"Score",
|
|
61
|
+
"ScoreAnswer",
|
|
62
|
+
"SystemOneRequest",
|
|
63
|
+
"SystemOneResponse",
|
|
64
|
+
"TypeSafe",
|
|
65
|
+
"TypeSafeError",
|
|
66
|
+
"UnprocessableEntityError",
|
|
67
|
+
"Usage",
|
|
68
|
+
"__version__",
|
|
69
|
+
"instrument",
|
|
70
|
+
]
|
jev_lite/_client.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""Synchronous and asynchronous TypeSafe clients, in the OpenAI SDK style."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import random
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from typing import Any, Self
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from opentelemetry.trace import SpanKind
|
|
14
|
+
|
|
15
|
+
from jev_lite._constants import (
|
|
16
|
+
DEFAULT_BASE_URL,
|
|
17
|
+
DEFAULT_MAX_RETRIES,
|
|
18
|
+
DEFAULT_MODEL,
|
|
19
|
+
DEFAULT_TIMEOUT_SECONDS,
|
|
20
|
+
ENV_API_KEY,
|
|
21
|
+
ENV_BASE_URL,
|
|
22
|
+
ENV_DEFAULT_MODEL,
|
|
23
|
+
REQUEST_ID_HEADER,
|
|
24
|
+
RETRYABLE_STATUS_CODES,
|
|
25
|
+
)
|
|
26
|
+
from jev_lite._exceptions import (
|
|
27
|
+
APIConnectionError,
|
|
28
|
+
APITimeoutError,
|
|
29
|
+
TypeSafeError,
|
|
30
|
+
decode_json,
|
|
31
|
+
raise_for_status,
|
|
32
|
+
)
|
|
33
|
+
from jev_lite._telemetry import get_tracer, json_attribute, record_exception
|
|
34
|
+
from jev_lite._version import __version__
|
|
35
|
+
from jev_lite.resources.models import AsyncModels, Models
|
|
36
|
+
from jev_lite.resources.system_one import AsyncSystemOne, SystemOne
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _read_env(name: str) -> str | None:
|
|
40
|
+
value = os.environ.get(name)
|
|
41
|
+
if value is None:
|
|
42
|
+
return None
|
|
43
|
+
stripped = value.strip()
|
|
44
|
+
return stripped or None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TypeSafe:
|
|
48
|
+
"""Synchronous TypeSafe API client.
|
|
49
|
+
|
|
50
|
+
Authentication and the default model are inferred from the environment when
|
|
51
|
+
omitted, matching `openai.OpenAI`:
|
|
52
|
+
|
|
53
|
+
- `api_key` from `TYPESAFE_API_KEY`
|
|
54
|
+
- `base_url` from `TYPESAFE_BASE_URL`
|
|
55
|
+
- `model` from `TYPESAFE_DEFAULT_MODEL`
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
api_key: str
|
|
59
|
+
base_url: str
|
|
60
|
+
model: str
|
|
61
|
+
max_retries: int
|
|
62
|
+
timeout: float | httpx.Timeout
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
*,
|
|
67
|
+
api_key: str | None = None,
|
|
68
|
+
base_url: str | None = None,
|
|
69
|
+
model: str | None = None,
|
|
70
|
+
timeout: float | httpx.Timeout | None = None,
|
|
71
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
72
|
+
default_headers: Mapping[str, str] | None = None,
|
|
73
|
+
http_client: httpx.Client | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
resolved_key = api_key if api_key is not None else _read_env(ENV_API_KEY)
|
|
76
|
+
if not resolved_key:
|
|
77
|
+
raise TypeSafeError(
|
|
78
|
+
"The api_key client option must be set either by passing api_key "
|
|
79
|
+
f"to the client or by setting the {ENV_API_KEY} environment variable"
|
|
80
|
+
)
|
|
81
|
+
self.api_key = resolved_key
|
|
82
|
+
self.base_url = (
|
|
83
|
+
base_url or _read_env(ENV_BASE_URL) or DEFAULT_BASE_URL
|
|
84
|
+
).rstrip("/")
|
|
85
|
+
self.model = model or _read_env(ENV_DEFAULT_MODEL) or DEFAULT_MODEL
|
|
86
|
+
self.timeout = DEFAULT_TIMEOUT_SECONDS if timeout is None else timeout
|
|
87
|
+
self.max_retries = max_retries
|
|
88
|
+
self._default_headers = dict(default_headers or {})
|
|
89
|
+
self._owns_http_client = http_client is None
|
|
90
|
+
self._http_client = http_client or httpx.Client(
|
|
91
|
+
base_url=self.base_url, timeout=self.timeout
|
|
92
|
+
)
|
|
93
|
+
self.system_one = SystemOne(self)
|
|
94
|
+
self.models = Models(self)
|
|
95
|
+
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
"""Close the owned HTTP client. A caller-supplied `http_client` is left open."""
|
|
98
|
+
if self._owns_http_client:
|
|
99
|
+
self._http_client.close()
|
|
100
|
+
|
|
101
|
+
def __enter__(self) -> Self:
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def __exit__(self, *args: object) -> None:
|
|
105
|
+
self.close()
|
|
106
|
+
|
|
107
|
+
def _headers(self, extra: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
108
|
+
headers = {
|
|
109
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
"Accept": "application/json",
|
|
112
|
+
"User-Agent": f"jev_lite/{__version__}",
|
|
113
|
+
}
|
|
114
|
+
headers.update(self._default_headers)
|
|
115
|
+
if extra:
|
|
116
|
+
headers.update(
|
|
117
|
+
{key: value for key, value in extra.items() if value is not None}
|
|
118
|
+
)
|
|
119
|
+
return headers
|
|
120
|
+
|
|
121
|
+
def _request(
|
|
122
|
+
self,
|
|
123
|
+
method: str,
|
|
124
|
+
path: str,
|
|
125
|
+
*,
|
|
126
|
+
json: dict[str, Any] | None,
|
|
127
|
+
extra: Mapping[str, Any] | None = None,
|
|
128
|
+
) -> httpx.Response:
|
|
129
|
+
extra = extra or {}
|
|
130
|
+
extra_headers = extra.get("headers")
|
|
131
|
+
timeout = extra.get("timeout")
|
|
132
|
+
tracer = get_tracer()
|
|
133
|
+
with tracer.start_as_current_span(
|
|
134
|
+
f"http.{method.lower()}",
|
|
135
|
+
kind=SpanKind.CLIENT,
|
|
136
|
+
) as span:
|
|
137
|
+
_set_http_span(
|
|
138
|
+
span, method=method, base_url=self.base_url, path=path, json=json
|
|
139
|
+
)
|
|
140
|
+
last_error: Exception | None = None
|
|
141
|
+
for attempt in range(self.max_retries + 1):
|
|
142
|
+
try:
|
|
143
|
+
response = self._http_client.request(
|
|
144
|
+
method,
|
|
145
|
+
path,
|
|
146
|
+
headers=self._headers(extra_headers),
|
|
147
|
+
json=json,
|
|
148
|
+
timeout=timeout if timeout is not None else self.timeout,
|
|
149
|
+
)
|
|
150
|
+
except httpx.TimeoutException as exc:
|
|
151
|
+
last_error = APITimeoutError(request=exc.request)
|
|
152
|
+
if attempt >= self.max_retries:
|
|
153
|
+
record_exception(span, last_error)
|
|
154
|
+
raise last_error from exc
|
|
155
|
+
_sleep_backoff(attempt)
|
|
156
|
+
continue
|
|
157
|
+
except httpx.RequestError as exc:
|
|
158
|
+
last_error = APIConnectionError(request=exc.request)
|
|
159
|
+
if attempt >= self.max_retries:
|
|
160
|
+
record_exception(span, last_error)
|
|
161
|
+
raise last_error from exc
|
|
162
|
+
_sleep_backoff(attempt)
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
span.set_attribute("http.response.status_code", response.status_code)
|
|
166
|
+
request_id = response.headers.get(REQUEST_ID_HEADER)
|
|
167
|
+
if request_id:
|
|
168
|
+
span.set_attribute("typesafe.request_id", request_id)
|
|
169
|
+
if (
|
|
170
|
+
response.status_code in RETRYABLE_STATUS_CODES
|
|
171
|
+
and attempt < self.max_retries
|
|
172
|
+
):
|
|
173
|
+
_sleep_backoff(
|
|
174
|
+
attempt, retry_after=response.headers.get("retry-after")
|
|
175
|
+
)
|
|
176
|
+
continue
|
|
177
|
+
try:
|
|
178
|
+
raise_for_status(response)
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
record_exception(span, exc)
|
|
181
|
+
raise
|
|
182
|
+
return response
|
|
183
|
+
assert last_error is not None
|
|
184
|
+
record_exception(span, last_error)
|
|
185
|
+
raise last_error
|
|
186
|
+
|
|
187
|
+
def _decode(self, response: httpx.Response) -> Any:
|
|
188
|
+
return decode_json(response)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class AsyncTypeSafe:
|
|
192
|
+
"""Asynchronous TypeSafe API client."""
|
|
193
|
+
|
|
194
|
+
api_key: str
|
|
195
|
+
base_url: str
|
|
196
|
+
model: str
|
|
197
|
+
max_retries: int
|
|
198
|
+
timeout: float | httpx.Timeout
|
|
199
|
+
|
|
200
|
+
def __init__(
|
|
201
|
+
self,
|
|
202
|
+
*,
|
|
203
|
+
api_key: str | None = None,
|
|
204
|
+
base_url: str | None = None,
|
|
205
|
+
model: str | None = None,
|
|
206
|
+
timeout: float | httpx.Timeout | None = None,
|
|
207
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
208
|
+
default_headers: Mapping[str, str] | None = None,
|
|
209
|
+
http_client: httpx.AsyncClient | None = None,
|
|
210
|
+
) -> None:
|
|
211
|
+
resolved_key = api_key if api_key is not None else _read_env(ENV_API_KEY)
|
|
212
|
+
if not resolved_key:
|
|
213
|
+
raise TypeSafeError(
|
|
214
|
+
"The api_key client option must be set either by passing api_key "
|
|
215
|
+
f"to the client or by setting the {ENV_API_KEY} environment variable"
|
|
216
|
+
)
|
|
217
|
+
self.api_key = resolved_key
|
|
218
|
+
self.base_url = (
|
|
219
|
+
base_url or _read_env(ENV_BASE_URL) or DEFAULT_BASE_URL
|
|
220
|
+
).rstrip("/")
|
|
221
|
+
self.model = model or _read_env(ENV_DEFAULT_MODEL) or DEFAULT_MODEL
|
|
222
|
+
self.timeout = DEFAULT_TIMEOUT_SECONDS if timeout is None else timeout
|
|
223
|
+
self.max_retries = max_retries
|
|
224
|
+
self._default_headers = dict(default_headers or {})
|
|
225
|
+
self._owns_http_client = http_client is None
|
|
226
|
+
self._http_client = http_client or httpx.AsyncClient(
|
|
227
|
+
base_url=self.base_url, timeout=self.timeout
|
|
228
|
+
)
|
|
229
|
+
self.system_one = AsyncSystemOne(self)
|
|
230
|
+
self.models = AsyncModels(self)
|
|
231
|
+
|
|
232
|
+
async def close(self) -> None:
|
|
233
|
+
"""Close the owned HTTP client. A caller-supplied `http_client` is left open."""
|
|
234
|
+
if self._owns_http_client:
|
|
235
|
+
await self._http_client.aclose()
|
|
236
|
+
|
|
237
|
+
async def __aenter__(self) -> Self:
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
async def __aexit__(self, *args: object) -> None:
|
|
241
|
+
await self.close()
|
|
242
|
+
|
|
243
|
+
def _headers(self, extra: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
244
|
+
headers = {
|
|
245
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
246
|
+
"Content-Type": "application/json",
|
|
247
|
+
"Accept": "application/json",
|
|
248
|
+
"User-Agent": f"jev_lite/{__version__}",
|
|
249
|
+
}
|
|
250
|
+
headers.update(self._default_headers)
|
|
251
|
+
if extra:
|
|
252
|
+
headers.update(
|
|
253
|
+
{key: value for key, value in extra.items() if value is not None}
|
|
254
|
+
)
|
|
255
|
+
return headers
|
|
256
|
+
|
|
257
|
+
async def _request(
|
|
258
|
+
self,
|
|
259
|
+
method: str,
|
|
260
|
+
path: str,
|
|
261
|
+
*,
|
|
262
|
+
json: dict[str, Any] | None,
|
|
263
|
+
extra: Mapping[str, Any] | None = None,
|
|
264
|
+
) -> httpx.Response:
|
|
265
|
+
extra = extra or {}
|
|
266
|
+
extra_headers = extra.get("headers")
|
|
267
|
+
timeout = extra.get("timeout")
|
|
268
|
+
tracer = get_tracer()
|
|
269
|
+
with tracer.start_as_current_span(
|
|
270
|
+
f"http.{method.lower()}",
|
|
271
|
+
kind=SpanKind.CLIENT,
|
|
272
|
+
) as span:
|
|
273
|
+
_set_http_span(
|
|
274
|
+
span, method=method, base_url=self.base_url, path=path, json=json
|
|
275
|
+
)
|
|
276
|
+
last_error: Exception | None = None
|
|
277
|
+
for attempt in range(self.max_retries + 1):
|
|
278
|
+
try:
|
|
279
|
+
response = await self._http_client.request(
|
|
280
|
+
method,
|
|
281
|
+
path,
|
|
282
|
+
headers=self._headers(extra_headers),
|
|
283
|
+
json=json,
|
|
284
|
+
timeout=timeout if timeout is not None else self.timeout,
|
|
285
|
+
)
|
|
286
|
+
except httpx.TimeoutException as exc:
|
|
287
|
+
last_error = APITimeoutError(request=exc.request)
|
|
288
|
+
if attempt >= self.max_retries:
|
|
289
|
+
record_exception(span, last_error)
|
|
290
|
+
raise last_error from exc
|
|
291
|
+
await _asleep_backoff(attempt)
|
|
292
|
+
continue
|
|
293
|
+
except httpx.RequestError as exc:
|
|
294
|
+
last_error = APIConnectionError(request=exc.request)
|
|
295
|
+
if attempt >= self.max_retries:
|
|
296
|
+
record_exception(span, last_error)
|
|
297
|
+
raise last_error from exc
|
|
298
|
+
await _asleep_backoff(attempt)
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
span.set_attribute("http.response.status_code", response.status_code)
|
|
302
|
+
request_id = response.headers.get(REQUEST_ID_HEADER)
|
|
303
|
+
if request_id:
|
|
304
|
+
span.set_attribute("typesafe.request_id", request_id)
|
|
305
|
+
if (
|
|
306
|
+
response.status_code in RETRYABLE_STATUS_CODES
|
|
307
|
+
and attempt < self.max_retries
|
|
308
|
+
):
|
|
309
|
+
await _asleep_backoff(
|
|
310
|
+
attempt, retry_after=response.headers.get("retry-after")
|
|
311
|
+
)
|
|
312
|
+
continue
|
|
313
|
+
try:
|
|
314
|
+
raise_for_status(response)
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
record_exception(span, exc)
|
|
317
|
+
raise
|
|
318
|
+
return response
|
|
319
|
+
assert last_error is not None
|
|
320
|
+
record_exception(span, last_error)
|
|
321
|
+
raise last_error
|
|
322
|
+
|
|
323
|
+
def _decode(self, response: httpx.Response) -> Any:
|
|
324
|
+
return decode_json(response)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
Client = TypeSafe
|
|
328
|
+
AsyncClient = AsyncTypeSafe
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _set_http_span(
|
|
332
|
+
span: Any,
|
|
333
|
+
*,
|
|
334
|
+
method: str,
|
|
335
|
+
base_url: str,
|
|
336
|
+
path: str,
|
|
337
|
+
json: dict[str, Any] | None,
|
|
338
|
+
) -> None:
|
|
339
|
+
span.set_attribute("http.request.method", method)
|
|
340
|
+
span.set_attribute("url.full", f"{base_url}{path}")
|
|
341
|
+
host = urlparse(base_url).hostname
|
|
342
|
+
if host:
|
|
343
|
+
span.set_attribute("server.address", host)
|
|
344
|
+
if json is not None:
|
|
345
|
+
span.set_attribute("http.request.body.json", json_attribute(json))
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _backoff_seconds(attempt: int, retry_after: str | None = None) -> float:
|
|
349
|
+
if retry_after:
|
|
350
|
+
try:
|
|
351
|
+
return max(0.0, float(retry_after))
|
|
352
|
+
except ValueError:
|
|
353
|
+
pass
|
|
354
|
+
return min(8.0, 0.25 * (2**attempt)) + random.random() * 0.05
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _sleep_backoff(attempt: int, retry_after: str | None = None) -> None:
|
|
358
|
+
time.sleep(_backoff_seconds(attempt, retry_after))
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
async def _asleep_backoff(attempt: int, retry_after: str | None = None) -> None:
|
|
362
|
+
import asyncio
|
|
363
|
+
|
|
364
|
+
await asyncio.sleep(_backoff_seconds(attempt, retry_after))
|
jev_lite/_constants.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Client defaults."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
DEFAULT_BASE_URL = "https://api.typesafe.ai"
|
|
6
|
+
DEFAULT_MODEL = "jev-latest"
|
|
7
|
+
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
8
|
+
DEFAULT_MAX_RETRIES = 2
|
|
9
|
+
DEFAULT_OTLP_ENDPOINT = "http://127.0.0.1:4318"
|
|
10
|
+
|
|
11
|
+
ENV_API_KEY = "TYPESAFE_API_KEY"
|
|
12
|
+
ENV_BASE_URL = "TYPESAFE_BASE_URL"
|
|
13
|
+
ENV_DEFAULT_MODEL = "TYPESAFE_DEFAULT_MODEL"
|
|
14
|
+
ENV_OTEL_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT"
|
|
15
|
+
ENV_OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME"
|
|
16
|
+
|
|
17
|
+
REQUEST_ID_HEADER = "x-typesafe-request-id"
|
|
18
|
+
RETRYABLE_STATUS_CODES = frozenset({408, 429, 500, 502, 503, 504, 529})
|
|
19
|
+
MAX_SPAN_VALUE_CHARS = 8192
|
jev_lite/_exceptions.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""HTTP and client errors, named after the OpenAI Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from jev_lite._constants import REQUEST_ID_HEADER
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TypeSafeError(Exception):
|
|
13
|
+
"""Base error for the TypeSafe client."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class APIError(TypeSafeError):
|
|
17
|
+
"""An error associated with an HTTP request."""
|
|
18
|
+
|
|
19
|
+
message: str
|
|
20
|
+
request: httpx.Request
|
|
21
|
+
body: object | None
|
|
22
|
+
code: str | None
|
|
23
|
+
type: str | None
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self, message: str, request: httpx.Request, *, body: object | None
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.message = message
|
|
30
|
+
self.request = request
|
|
31
|
+
self.body = body
|
|
32
|
+
self.code = None
|
|
33
|
+
self.type = None
|
|
34
|
+
if isinstance(body, dict):
|
|
35
|
+
detail = body.get("detail", body)
|
|
36
|
+
if isinstance(detail, dict):
|
|
37
|
+
self.code = _as_str(detail.get("error_type") or detail.get("code"))
|
|
38
|
+
self.type = self.code
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class APIResponseValidationError(APIError):
|
|
42
|
+
"""The response JSON did not match the expected Pydantic schema."""
|
|
43
|
+
|
|
44
|
+
response: httpx.Response
|
|
45
|
+
status_code: int
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
response: httpx.Response,
|
|
50
|
+
body: object | None,
|
|
51
|
+
*,
|
|
52
|
+
message: str | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
super().__init__(
|
|
55
|
+
message or "Data returned by the API was invalid for the expected schema.",
|
|
56
|
+
response.request,
|
|
57
|
+
body=body,
|
|
58
|
+
)
|
|
59
|
+
self.response = response
|
|
60
|
+
self.status_code = response.status_code
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class APIStatusError(APIError):
|
|
64
|
+
"""Raised when the API responds with a 4xx or 5xx status code."""
|
|
65
|
+
|
|
66
|
+
response: httpx.Response
|
|
67
|
+
status_code: int
|
|
68
|
+
request_id: str | None
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self, message: str, *, response: httpx.Response, body: object | None
|
|
72
|
+
) -> None:
|
|
73
|
+
super().__init__(message, response.request, body=body)
|
|
74
|
+
self.response = response
|
|
75
|
+
self.status_code = response.status_code
|
|
76
|
+
self.request_id = response.headers.get(REQUEST_ID_HEADER)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class APIConnectionError(APIError):
|
|
80
|
+
def __init__(
|
|
81
|
+
self, *, message: str = "Connection error.", request: httpx.Request
|
|
82
|
+
) -> None:
|
|
83
|
+
super().__init__(message, request, body=None)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class APITimeoutError(APIConnectionError):
|
|
87
|
+
def __init__(self, request: httpx.Request) -> None:
|
|
88
|
+
super().__init__(message="Request timed out.", request=request)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class BadRequestError(APIStatusError):
|
|
92
|
+
status_code: Literal[400] = 400
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AuthenticationError(APIStatusError):
|
|
96
|
+
status_code: Literal[401] = 401
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class PermissionDeniedError(APIStatusError):
|
|
100
|
+
status_code: Literal[403] = 403
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class NotFoundError(APIStatusError):
|
|
104
|
+
status_code: Literal[404] = 404
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class UnprocessableEntityError(APIStatusError):
|
|
108
|
+
status_code: Literal[422] = 422
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class RateLimitError(APIStatusError):
|
|
112
|
+
status_code: Literal[429] = 429
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class InternalServerError(APIStatusError):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class OverloadedError(InternalServerError):
|
|
120
|
+
"""TypeSafe returned 529 (temporarily overloaded)."""
|
|
121
|
+
|
|
122
|
+
status_code: Literal[529] = 529
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _as_str(value: object) -> str | None:
|
|
126
|
+
return value if isinstance(value, str) else None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _message_from_body(body: object | None, fallback: str) -> str:
|
|
130
|
+
if isinstance(body, dict):
|
|
131
|
+
detail = body.get("detail", body)
|
|
132
|
+
if isinstance(detail, dict):
|
|
133
|
+
message = detail.get("message")
|
|
134
|
+
if isinstance(message, str) and message:
|
|
135
|
+
return message
|
|
136
|
+
if isinstance(detail, str) and detail:
|
|
137
|
+
return detail
|
|
138
|
+
if isinstance(detail, list) and detail:
|
|
139
|
+
first = detail[0]
|
|
140
|
+
if isinstance(first, dict):
|
|
141
|
+
loc = first.get("loc")
|
|
142
|
+
msg = first.get("msg")
|
|
143
|
+
if msg:
|
|
144
|
+
return f"{msg} ({loc})" if loc is not None else str(msg)
|
|
145
|
+
return fallback
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def raise_for_status(response: httpx.Response) -> None:
|
|
149
|
+
"""Map HTTP errors onto OpenAI-style exception classes."""
|
|
150
|
+
if response.is_success:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
body: object | None
|
|
154
|
+
try:
|
|
155
|
+
body = response.json()
|
|
156
|
+
except ValueError:
|
|
157
|
+
body = response.text or None
|
|
158
|
+
|
|
159
|
+
message = _message_from_body(body, fallback=f"HTTP {response.status_code}")
|
|
160
|
+
status = response.status_code
|
|
161
|
+
error_cls: type[APIStatusError]
|
|
162
|
+
if status == 400:
|
|
163
|
+
error_cls = BadRequestError
|
|
164
|
+
elif status == 401:
|
|
165
|
+
error_cls = AuthenticationError
|
|
166
|
+
elif status == 403:
|
|
167
|
+
error_cls = PermissionDeniedError
|
|
168
|
+
elif status == 404:
|
|
169
|
+
error_cls = NotFoundError
|
|
170
|
+
elif status == 422:
|
|
171
|
+
error_cls = UnprocessableEntityError
|
|
172
|
+
elif status == 429:
|
|
173
|
+
error_cls = RateLimitError
|
|
174
|
+
elif status == 529:
|
|
175
|
+
error_cls = OverloadedError
|
|
176
|
+
elif status >= 500:
|
|
177
|
+
error_cls = InternalServerError
|
|
178
|
+
else:
|
|
179
|
+
error_cls = APIStatusError
|
|
180
|
+
raise error_cls(message, response=response, body=body)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def decode_json(response: httpx.Response) -> Any:
|
|
184
|
+
try:
|
|
185
|
+
return response.json()
|
|
186
|
+
except ValueError as exc:
|
|
187
|
+
raise APIResponseValidationError(
|
|
188
|
+
response, body=response.text, message=str(exc)
|
|
189
|
+
) from exc
|
jev_lite/_resource.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Resource objects bound to a client, matching the OpenAI SDK pattern."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from jev_lite._client import AsyncTypeSafe, TypeSafe
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SyncAPIResource:
|
|
12
|
+
def __init__(self, client: TypeSafe) -> None:
|
|
13
|
+
self._client = client
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncAPIResource:
|
|
17
|
+
def __init__(self, client: AsyncTypeSafe) -> None:
|
|
18
|
+
self._client = client
|