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.
Files changed (53) hide show
  1. jev_lite/__init__.py +70 -0
  2. jev_lite/_client.py +364 -0
  3. jev_lite/_constants.py +19 -0
  4. jev_lite/_exceptions.py +189 -0
  5. jev_lite/_resource.py +18 -0
  6. jev_lite/_telemetry.py +105 -0
  7. jev_lite/_version.py +3 -0
  8. jev_lite/engine/__init__.py +30 -0
  9. jev_lite/engine/backbone.py +348 -0
  10. jev_lite/engine/evaluate.py +189 -0
  11. jev_lite/engine/frozen.py +105 -0
  12. jev_lite/engine/hashing.py +46 -0
  13. jev_lite/engine/head.py +57 -0
  14. jev_lite/engine/hf.py +131 -0
  15. jev_lite/engine/interaction.py +84 -0
  16. jev_lite/engine/letters.py +21 -0
  17. jev_lite/engine/lexical.py +45 -0
  18. jev_lite/engine/pairs.py +106 -0
  19. jev_lite/engine/prompt.py +48 -0
  20. jev_lite/engine/protocol.py +31 -0
  21. jev_lite/engine/render.py +32 -0
  22. jev_lite/engine/slots.py +77 -0
  23. jev_lite/engine/system_one.py +92 -0
  24. jev_lite/eval/__init__.py +12 -0
  25. jev_lite/eval/__main__.py +6 -0
  26. jev_lite/eval/ban.py +26 -0
  27. jev_lite/eval/collect.py +64 -0
  28. jev_lite/eval/collect_public.py +105 -0
  29. jev_lite/eval/decode.py +20 -0
  30. jev_lite/eval/distill.py +199 -0
  31. jev_lite/eval/env.py +20 -0
  32. jev_lite/eval/jevbench.py +199 -0
  33. jev_lite/eval/json_cache.py +45 -0
  34. jev_lite/eval/langs.py +103 -0
  35. jev_lite/eval/metrics.py +67 -0
  36. jev_lite/eval/ollama.py +113 -0
  37. jev_lite/eval/public_states.py +188 -0
  38. jev_lite/eval/questions_gen.py +107 -0
  39. jev_lite/eval/schema.py +39 -0
  40. jev_lite/eval/targets.py +22 -0
  41. jev_lite/eval/teacher_cache.py +116 -0
  42. jev_lite/eval/tokenizer_gate.py +60 -0
  43. jev_lite/eval/train_head.py +195 -0
  44. jev_lite/eval/translate_public.py +180 -0
  45. jev_lite/py.typed +0 -0
  46. jev_lite/resources/__init__.py +17 -0
  47. jev_lite/resources/models.py +99 -0
  48. jev_lite/resources/system_one.py +189 -0
  49. jev_lite/types.py +187 -0
  50. jev_lite-0.0.1.dist-info/METADATA +128 -0
  51. jev_lite-0.0.1.dist-info/RECORD +53 -0
  52. jev_lite-0.0.1.dist-info/WHEEL +4 -0
  53. 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
@@ -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