relaygpu-client 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.
Files changed (56) hide show
  1. relaygpu/__init__.py +21 -0
  2. relaygpu/_async/__init__.py +1 -0
  3. relaygpu/_async/_http.py +146 -0
  4. relaygpu/_async/account.py +83 -0
  5. relaygpu/_async/audio.py +86 -0
  6. relaygpu/_async/client.py +218 -0
  7. relaygpu/_async/estimate.py +18 -0
  8. relaygpu/_async/files.py +293 -0
  9. relaygpu/_async/image.py +157 -0
  10. relaygpu/_async/keys.py +103 -0
  11. relaygpu/_async/models.py +75 -0
  12. relaygpu/_async/run.py +99 -0
  13. relaygpu/_async/tasks.py +66 -0
  14. relaygpu/_async/video.py +114 -0
  15. relaygpu/_async/webhooks.py +67 -0
  16. relaygpu/_async/workflows.py +149 -0
  17. relaygpu/_clock.py +31 -0
  18. relaygpu/_core.py +208 -0
  19. relaygpu/_errors.py +119 -0
  20. relaygpu/_estimate.py +224 -0
  21. relaygpu/_exceptions.py +166 -0
  22. relaygpu/_generated/__init__.py +1 -0
  23. relaygpu/_generated/error_codes.py +400 -0
  24. relaygpu/_generated/models_map.py +229 -0
  25. relaygpu/_generated/types.py +2676 -0
  26. relaygpu/_images.py +70 -0
  27. relaygpu/_run_common.py +124 -0
  28. relaygpu/_shapes.py +85 -0
  29. relaygpu/_sync/__init__.py +2 -0
  30. relaygpu/_sync/_http.py +147 -0
  31. relaygpu/_sync/account.py +84 -0
  32. relaygpu/_sync/audio.py +87 -0
  33. relaygpu/_sync/client.py +219 -0
  34. relaygpu/_sync/estimate.py +19 -0
  35. relaygpu/_sync/files.py +294 -0
  36. relaygpu/_sync/image.py +158 -0
  37. relaygpu/_sync/keys.py +104 -0
  38. relaygpu/_sync/models.py +76 -0
  39. relaygpu/_sync/run.py +100 -0
  40. relaygpu/_sync/tasks.py +67 -0
  41. relaygpu/_sync/video.py +115 -0
  42. relaygpu/_sync/webhooks.py +68 -0
  43. relaygpu/_sync/workflows.py +150 -0
  44. relaygpu/_util.py +73 -0
  45. relaygpu/_version.py +1 -0
  46. relaygpu/async_client.py +5 -0
  47. relaygpu/client.py +5 -0
  48. relaygpu/errors.py +10 -0
  49. relaygpu/inputs.py +306 -0
  50. relaygpu/py.typed +0 -0
  51. relaygpu/types.py +166 -0
  52. relaygpu/webhooks.py +212 -0
  53. relaygpu_client-0.1.0.dist-info/METADATA +567 -0
  54. relaygpu_client-0.1.0.dist-info/RECORD +56 -0
  55. relaygpu_client-0.1.0.dist-info/WHEEL +4 -0
  56. relaygpu_client-0.1.0.dist-info/licenses/LICENSE +21 -0
relaygpu/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """Relay's Python SDK: image, video and audio generation, async tasks, file uploads, webhook verification,
2
+ workflows, account and keys, with typed errors. A port of ``@relaygpu/client``."""
3
+
4
+ from ._async.image import AsyncImageResult as AsyncImageResult
5
+ from ._async.image import AsyncRelayImage as AsyncRelayImage
6
+ from ._core import DEFAULT_BASE_URL as DEFAULT_BASE_URL
7
+ from ._core import DEFAULT_TIMEOUT as DEFAULT_TIMEOUT
8
+ from ._core import APIResponse as APIResponse
9
+ from ._core import RetryOptions as RetryOptions
10
+ from ._run_common import is_accepted as is_accepted
11
+ from ._sync.image import ImageResult as ImageResult
12
+ from ._sync.image import RelayImage as RelayImage
13
+ from ._sync.image import to_relay_image as to_relay_image
14
+ from ._version import VERSION as VERSION
15
+ from .async_client import AsyncRelay as AsyncRelay
16
+ from .client import Relay as Relay
17
+ from .errors import * # noqa: F403
18
+ from .inputs import INLINE_IMAGE_MAX_BYTES as INLINE_IMAGE_MAX_BYTES
19
+ from .webhooks import verify_webhook as verify_webhook
20
+
21
+ __version__ = VERSION
@@ -0,0 +1 @@
1
+ """Hand-written async sources; relaygpu/_sync/ is generated from them by scripts/unasync.py."""
@@ -0,0 +1,146 @@
1
+ """The transport (port of src/http.ts `HttpClient`). Hand-written async; ``_sync/_http.py`` is generated from it by
2
+ scripts/unasync.py. Everything that is not I/O lives in ``relaygpu/_core.py``."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Mapping
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from .. import _clock
12
+ from .._core import (
13
+ _UNSET,
14
+ DEFAULT_BASE_URL,
15
+ DEFAULT_TIMEOUT,
16
+ USER_AGENT,
17
+ APIResponse,
18
+ RawBody,
19
+ RetryOptions,
20
+ RetryPolicy,
21
+ RetryState,
22
+ auth_headers,
23
+ build_headers,
24
+ encode_json,
25
+ is_replayable,
26
+ parse_ok,
27
+ query_params,
28
+ read_error_body,
29
+ transport_errors,
30
+ )
31
+ from .._errors import error_from_response
32
+ from .._exceptions import APIConnectionError, APITimeoutError
33
+ from .._images import download_error
34
+
35
+
36
+ class AsyncHttpClient:
37
+ """Headers, timeouts, the retry policy, ``X-Request-ID`` and ``Idempotency-Replayed``. Never exposes the credential."""
38
+
39
+ def __init__(
40
+ self,
41
+ *,
42
+ api_key: str | None = None,
43
+ jwt: str | None = None,
44
+ base_url: str | None = None,
45
+ timeout: float | None = None,
46
+ retry: bool | RetryOptions | None = None,
47
+ default_headers: Mapping[str, str] | None = None,
48
+ http_client: httpx.AsyncClient | None = None,
49
+ ) -> None:
50
+ self.__auth = auth_headers(api_key, jwt)
51
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
52
+ self.timeout = float(timeout) if timeout is not None else DEFAULT_TIMEOUT
53
+ self.retry = RetryPolicy.from_option(retry)
54
+ self._default_headers = dict(default_headers or {})
55
+ self._owns_client = http_client is None
56
+ self._client = http_client if http_client is not None else httpx.AsyncClient(follow_redirects=True)
57
+
58
+ def __repr__(self) -> str:
59
+ return f"{type(self).__name__}(base_url={self.base_url!r})"
60
+
61
+ async def aclose(self) -> None:
62
+ if self._owns_client:
63
+ await self._client.aclose()
64
+
65
+ def url(self, path: str) -> str:
66
+ return self.base_url + path
67
+
68
+ async def request(
69
+ self,
70
+ method: str,
71
+ path: str,
72
+ *,
73
+ query: Mapping[str, Any] | None = None, # a Query, or a params TypedDict (only Mapping[str, Any] takes those)
74
+ json: Any = _UNSET,
75
+ content: RawBody | None = None,
76
+ content_type: str | None = None,
77
+ headers: Mapping[str, str] | None = None,
78
+ timeout: float | None = None,
79
+ idempotency_key: str | None = None,
80
+ no_auth: bool = False,
81
+ ) -> APIResponse[Any]:
82
+ """One API call with the retry policy. ``json`` is serialised; ``content`` is sent raw (bytes, or an iterator
83
+ of bytes, which is sent once and never retried). ``idempotency_key`` makes a POST retry-safe."""
84
+ method = method.upper()
85
+ is_get = method in ("GET", "HEAD")
86
+ hdrs = build_headers(self.__auth, self._default_headers, headers, no_auth=no_auth, idempotency_key=idempotency_key)
87
+ body: RawBody | None = None
88
+ if content is not None:
89
+ body = content
90
+ if content_type:
91
+ hdrs["Content-Type"] = content_type
92
+ elif json is not _UNSET:
93
+ body = encode_json(json)
94
+ hdrs["Content-Type"] = "application/json"
95
+ state = RetryState(self.retry, is_get, not is_get and idempotency_key is not None, is_replayable(body))
96
+ url = self.url(path)
97
+ params: list[tuple[str, str | int | float | bool | None]] = list(query_params(query)) # list is invariant: widen for httpx
98
+ t = timeout if timeout is not None else self.timeout
99
+ while True:
100
+ try:
101
+ res = await self._send(method, url, params, hdrs, body, t, path)
102
+ except (APIConnectionError, APITimeoutError):
103
+ wait = state.after_transport_error()
104
+ if wait is None:
105
+ raise
106
+ await _clock.async_sleep(wait)
107
+ continue
108
+ if res.is_success:
109
+ return parse_ok(res)
110
+ err = error_from_response(res.status_code, read_error_body(res), res.headers)
111
+ wait = state.after_error(res.status_code, err.code, err.retry_after)
112
+ if wait is None:
113
+ raise err
114
+ await _clock.async_sleep(wait)
115
+
116
+ async def _send(
117
+ self,
118
+ method: str,
119
+ url: str,
120
+ params: list[tuple[str, str | int | float | bool | None]],
121
+ headers: dict[str, str],
122
+ body: RawBody | None,
123
+ timeout: float,
124
+ path: str,
125
+ ) -> httpx.Response:
126
+ with transport_errors(f"Request timed out after {timeout:g} s: {method} {path}", f"Connection error: {method} {path}"):
127
+ res = await self._client.request(
128
+ method,
129
+ url,
130
+ params=params or None,
131
+ headers=headers,
132
+ content=body,
133
+ timeout=timeout,
134
+ )
135
+ await res.aread()
136
+ return res
137
+
138
+ async def download(self, url: str) -> bytes:
139
+ """GETs an absolute result link (an output image) over the same transport. Never with the credential or the
140
+ client's default headers (only ``User-Agent``); redirects followed; never retried."""
141
+ with transport_errors(f"Image download timed out: {url}", "Image download failed"):
142
+ res = await self._client.get(url, headers={"User-Agent": USER_AGENT}, timeout=self.timeout, follow_redirects=True)
143
+ await res.aread()
144
+ if not res.is_success:
145
+ raise download_error(res.status_code)
146
+ return res.content
@@ -0,0 +1,83 @@
1
+ """The ``account`` namespace (port of src/account.ts)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import TYPE_CHECKING, Any, cast
7
+
8
+ from .._generated.types import (
9
+ CreditHistoryResponse,
10
+ CreditsResponse,
11
+ CustomerPricingResponse,
12
+ CustomerUsageResponse,
13
+ KeyWithAnalytics,
14
+ MetricsPage,
15
+ ModelAllowlistResponse,
16
+ ModelAllowlistView,
17
+ ProfileResponse,
18
+ UsageTimeseriesPage,
19
+ )
20
+ from .._shapes import CreditHistoryParams, KeyUsageParams, MetricsParams, UsageParams, UsageTimeseriesParams
21
+ from .._util import path_id
22
+
23
+ if TYPE_CHECKING:
24
+ from typing_extensions import Unpack
25
+
26
+ from .client import AsyncRelay
27
+
28
+
29
+ class AsyncAccount:
30
+ """Account reads (Customer + Metrics ops). Auth: a dashboard JWT, or the superkey of a partner (custom) tier. A plain
31
+ inference key gets the server's 403 as ``PermissionDeniedError``; the SDK never guesses the key class client-side.
32
+
33
+ Query parameters are keyword arguments with the spec's names; ``from`` is spelled ``from_``."""
34
+
35
+ def __init__(self, relay: AsyncRelay) -> None:
36
+ self._relay = relay
37
+
38
+ async def _get(self, path: str, params: Mapping[str, Any] | None = None) -> Any:
39
+ res = await self._relay._http.request("GET", path, query=params)
40
+ return res.data
41
+
42
+ async def credits(self) -> CreditsResponse:
43
+ """``GET /v2/customer/credits``: balance, promos, consumption."""
44
+ return cast(CreditsResponse, await self._get("/v2/customer/credits"))
45
+
46
+ async def credits_history(self, **params: Unpack[CreditHistoryParams]) -> CreditHistoryResponse:
47
+ """``GET /v2/customer/credits/history``. **JWT only** (a superkey is refused by the server)."""
48
+ return cast(CreditHistoryResponse, await self._get("/v2/customer/credits/history", params))
49
+
50
+ async def usage(self, **params: Unpack[UsageParams]) -> CustomerUsageResponse:
51
+ """``GET /v2/customer/usage``: per-key analytics; page with ``starting_after`` = ``next_cursor``."""
52
+ return cast(CustomerUsageResponse, await self._get("/v2/customer/usage", params))
53
+
54
+ async def usage_by_key(self, key_id: str, **params: Unpack[KeyUsageParams]) -> KeyWithAnalytics:
55
+ """``GET /v2/customer/usage/{key_id}``. Address the key by ``key_id``, never its secret."""
56
+ return cast(KeyWithAnalytics, await self._get(f"/v2/customer/usage/{path_id(key_id)}", params))
57
+
58
+ async def usage_timeseries(self, **params: Unpack[UsageTimeseriesParams]) -> UsageTimeseriesPage:
59
+ """``GET /v2/customer/usage/timeseries``: bucketed spend/tokens (``start_time`` = Unix seconds, required)."""
60
+ return cast(UsageTimeseriesPage, await self._get("/v2/customer/usage/timeseries", params))
61
+
62
+ async def metrics(self, **params: Unpack[MetricsParams]) -> MetricsPage:
63
+ """``GET /v2/customer/metrics``: latency percentiles and error splits (``start_time`` = Unix seconds, required)."""
64
+ return cast(MetricsPage, await self._get("/v2/customer/metrics", params))
65
+
66
+ async def pricing(self) -> CustomerPricingResponse:
67
+ """``GET /v2/customer/pricing``: the account's effective rows (custom-tier overrides included)."""
68
+ return cast(CustomerPricingResponse, await self._get("/v2/customer/pricing"))
69
+
70
+ async def profile(self) -> ProfileResponse:
71
+ """``GET /v2/customer/profile``: profile, tier (``tier_details.is_custom``), balance, allowlist."""
72
+ return cast(ProfileResponse, await self._get("/v2/customer/profile"))
73
+
74
+ async def model_allowlist(self) -> ModelAllowlistView:
75
+ """``GET /v2/customer/model-allowlist``: the customer-wide scope list for non-superkey keys (``None`` = unset)."""
76
+ return cast(ModelAllowlistView, await self._get("/v2/customer/model-allowlist"))
77
+
78
+ async def set_model_allowlist(self, model_allowlist: Sequence[str] | None) -> ModelAllowlistResponse:
79
+ """``PATCH /v2/customer/model-allowlist``: replaces the list (``None`` clears it). Scopes are
80
+ ``{mode}.{source}.{model}``. Partner tiers only; the superkey stays exempt."""
81
+ body = {"model_allowlist": None if model_allowlist is None else [*model_allowlist]}
82
+ res = await self._relay._http.request("PATCH", "/v2/customer/model-allowlist", json=body)
83
+ return cast(ModelAllowlistResponse, res.data)
@@ -0,0 +1,86 @@
1
+ """Text-to-speech and speech-to-text (port of src/audio.ts)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from typing import TYPE_CHECKING, Any, cast
7
+
8
+ from ..types import KnownSpeechModel, KnownTranscribeModel, Mode, TaskProgress, UploadOptions
9
+ from .run import run
10
+
11
+ if TYPE_CHECKING:
12
+ from .client import AsyncRelay
13
+
14
+
15
+ class AsyncAudio:
16
+ """Text-to-speech and speech-to-text. Same options as the image helpers: an async answer (``async_=True``) is
17
+ waited for."""
18
+
19
+ def __init__(self, relay: AsyncRelay) -> None:
20
+ self._relay = relay
21
+
22
+ async def _run(self, model: str, input: Mapping[str, Any], **options: Any) -> dict[str, Any]:
23
+ """The one body of ``speech`` and ``transcribe``: ``run(..., wait=True)``, so never the 202 envelope."""
24
+ return cast("dict[str, Any]", await run(self._relay, model, input, wait=True, **options))
25
+
26
+ async def speech(
27
+ self,
28
+ model: KnownSpeechModel | str,
29
+ input: Mapping[str, Any],
30
+ *,
31
+ on_progress: Callable[[TaskProgress], None] | None = None,
32
+ timeout: float | None = None,
33
+ mode: Mode | None = None,
34
+ store_output: str | None = None,
35
+ webhook_url: str | None = None,
36
+ idempotency_key: str | None = None,
37
+ async_: bool | None = None,
38
+ upload: UploadOptions | None = None,
39
+ inline_images: bool = False,
40
+ ) -> dict[str, Any]:
41
+ """Text-to-speech. Returns the response body: ``audio_url`` (a link that expires) or ``audio_base64`` +
42
+ ``content_type``, per model."""
43
+ return await self._run(
44
+ model,
45
+ input,
46
+ on_progress=on_progress,
47
+ timeout=timeout,
48
+ mode=mode,
49
+ store_output=store_output,
50
+ webhook_url=webhook_url,
51
+ idempotency_key=idempotency_key,
52
+ async_=async_,
53
+ upload=upload,
54
+ inline_images=inline_images,
55
+ )
56
+
57
+ async def transcribe(
58
+ self,
59
+ model: KnownTranscribeModel | str,
60
+ input: Mapping[str, Any],
61
+ *,
62
+ on_progress: Callable[[TaskProgress], None] | None = None,
63
+ timeout: float | None = None,
64
+ mode: Mode | None = None,
65
+ store_output: str | None = None,
66
+ webhook_url: str | None = None,
67
+ idempotency_key: str | None = None,
68
+ async_: bool | None = None,
69
+ upload: UploadOptions | None = None,
70
+ inline_images: bool = False,
71
+ ) -> dict[str, Any]:
72
+ """Speech-to-text. ``audio_url`` may be a link or bytes / a path / a file-like object (uploaded first). Returns
73
+ ``{text, language?, duration?}``."""
74
+ return await self._run(
75
+ model,
76
+ input,
77
+ on_progress=on_progress,
78
+ timeout=timeout,
79
+ mode=mode,
80
+ store_output=store_output,
81
+ webhook_url=webhook_url,
82
+ idempotency_key=idempotency_key,
83
+ async_=async_,
84
+ upload=upload,
85
+ inline_images=inline_images,
86
+ )
@@ -0,0 +1,218 @@
1
+ """The async client (port of src/client.ts). ``_sync/client.py`` (``Relay``) is generated from this file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from types import TracebackType
7
+ from typing import Any, Literal, overload
8
+
9
+ import httpx
10
+
11
+ from .._core import _UNSET, Query, RawBody, RetryOptions
12
+ from ..types import AsyncAccepted, CostEstimate, HealthResponse, Mode, TaskProgress, UploadOptions, UsageInput
13
+ from ._http import AsyncHttpClient
14
+ from .account import AsyncAccount
15
+ from .audio import AsyncAudio
16
+ from .estimate import estimate_cost
17
+ from .files import AsyncFiles
18
+ from .image import AsyncImages
19
+ from .keys import AsyncKeys
20
+ from .models import AsyncModels, AsyncPricing, AsyncTiers, health
21
+ from .run import run
22
+ from .tasks import AsyncTasks
23
+ from .video import AsyncVideos
24
+ from .webhooks import AsyncWebhooks
25
+ from .workflows import AsyncWorkflows
26
+
27
+
28
+ class AsyncRelay:
29
+ """The Relay client.
30
+
31
+ ``api_key`` (``relay_sk_…``) is sent as ``X-API-Key``; ``jwt`` (a dashboard login token) as ``Authorization:
32
+ Bearer``; never both. Catalog reads (``models``, ``pricing``, ``tiers``, ``health``) and task polls need no
33
+ credential. ``timeout`` is seconds per HTTP attempt (default 600). ``retry=False`` disables every retry.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ api_key: str | None = None,
39
+ *,
40
+ jwt: str | None = None,
41
+ base_url: str | None = None,
42
+ timeout: float | None = None,
43
+ retry: bool | RetryOptions | None = None,
44
+ default_headers: Mapping[str, str] | None = None,
45
+ http_client: httpx.AsyncClient | None = None,
46
+ ) -> None:
47
+ self._http = AsyncHttpClient(
48
+ api_key=api_key,
49
+ jwt=jwt,
50
+ base_url=base_url,
51
+ timeout=timeout,
52
+ retry=retry,
53
+ default_headers=default_headers,
54
+ http_client=http_client,
55
+ )
56
+ self.models = AsyncModels(self)
57
+ self.pricing = AsyncPricing(self)
58
+ self.tiers = AsyncTiers(self)
59
+ self.tasks = AsyncTasks(self)
60
+ self.image = AsyncImages(self)
61
+ self.video = AsyncVideos(self)
62
+ self.audio = AsyncAudio(self)
63
+ self.files = AsyncFiles(self)
64
+ self.workflows = AsyncWorkflows(self)
65
+ self.account = AsyncAccount(self)
66
+ self.keys = AsyncKeys(self)
67
+ self.webhooks = AsyncWebhooks(self)
68
+
69
+ @property
70
+ def base_url(self) -> str:
71
+ return self._http.base_url
72
+
73
+ def __repr__(self) -> str:
74
+ # Never reveal the credential when the client is logged or printed.
75
+ return f"AsyncRelay(base_url={self._http.base_url!r})"
76
+
77
+ async def health(self) -> HealthResponse:
78
+ """``GET /v2/health``."""
79
+ return await health(self)
80
+
81
+ @overload
82
+ async def run(
83
+ self,
84
+ model: str,
85
+ input: Mapping[str, Any],
86
+ *,
87
+ wait: Literal[True] = True,
88
+ on_progress: Callable[[TaskProgress], None] | None = None,
89
+ timeout: float | None = None,
90
+ mode: Mode | None = None,
91
+ store_output: str | None = None,
92
+ webhook_url: str | None = None,
93
+ idempotency_key: str | None = None,
94
+ async_: bool | None = None,
95
+ upload: UploadOptions | None = None,
96
+ inline_images: bool = False,
97
+ ) -> dict[str, Any]: ...
98
+
99
+ @overload
100
+ async def run(
101
+ self,
102
+ model: str,
103
+ input: Mapping[str, Any],
104
+ *,
105
+ wait: Literal[False],
106
+ on_progress: Callable[[TaskProgress], None] | None = None,
107
+ timeout: float | None = None,
108
+ mode: Mode | None = None,
109
+ store_output: str | None = None,
110
+ webhook_url: str | None = None,
111
+ idempotency_key: str | None = None,
112
+ async_: bool | None = None,
113
+ upload: UploadOptions | None = None,
114
+ inline_images: bool = False,
115
+ ) -> dict[str, Any] | AsyncAccepted: ...
116
+
117
+ @overload
118
+ async def run(
119
+ self,
120
+ model: str,
121
+ input: Mapping[str, Any],
122
+ *,
123
+ wait: bool,
124
+ on_progress: Callable[[TaskProgress], None] | None = None,
125
+ timeout: float | None = None,
126
+ mode: Mode | None = None,
127
+ store_output: str | None = None,
128
+ webhook_url: str | None = None,
129
+ idempotency_key: str | None = None,
130
+ async_: bool | None = None,
131
+ upload: UploadOptions | None = None,
132
+ inline_images: bool = False,
133
+ ) -> dict[str, Any] | AsyncAccepted: ...
134
+
135
+ async def run(
136
+ self,
137
+ model: str,
138
+ input: Mapping[str, Any],
139
+ *,
140
+ wait: bool = True,
141
+ on_progress: Callable[[TaskProgress], None] | None = None,
142
+ timeout: float | None = None,
143
+ mode: Mode | None = None,
144
+ store_output: str | None = None,
145
+ webhook_url: str | None = None,
146
+ idempotency_key: str | None = None,
147
+ async_: bool | None = None,
148
+ upload: UploadOptions | None = None,
149
+ inline_images: bool = False,
150
+ ) -> dict[str, Any] | AsyncAccepted:
151
+ """Runs any model: resolves its route through ``models.get``, submits, and (by default) waits for the result.
152
+
153
+ A sync route returns its body; an async route (or ``async_=True``) waits and returns the task's ``result``;
154
+ ``wait=False`` returns the ``202`` envelope instead (narrow with ``is_accepted``).
155
+ """
156
+ return await run(
157
+ self,
158
+ model,
159
+ input,
160
+ wait=wait,
161
+ on_progress=on_progress,
162
+ timeout=timeout,
163
+ mode=mode,
164
+ store_output=store_output,
165
+ webhook_url=webhook_url,
166
+ idempotency_key=idempotency_key,
167
+ async_=async_,
168
+ upload=upload,
169
+ inline_images=inline_images,
170
+ )
171
+
172
+ async def estimate_cost(self, model: str, usage: UsageInput) -> CostEstimate:
173
+ """Client-side estimate from ``/v2/pricing`` rows. An estimate, never an invoice."""
174
+ return await estimate_cost(self, model, usage)
175
+
176
+ async def request(
177
+ self,
178
+ method: str,
179
+ path: str,
180
+ *,
181
+ json: Any = _UNSET,
182
+ query: Query | None = None,
183
+ headers: Mapping[str, str] | None = None,
184
+ content: RawBody | None = None,
185
+ content_type: str | None = None,
186
+ timeout: float | None = None,
187
+ idempotency_key: str | None = None,
188
+ no_auth: bool = False,
189
+ ) -> Any:
190
+ """Escape hatch: any route, typed errors and the retry policy included. Returns the parsed body."""
191
+ res = await self._http.request(
192
+ method,
193
+ path,
194
+ json=json,
195
+ query=query,
196
+ headers=headers,
197
+ content=content,
198
+ content_type=content_type,
199
+ timeout=timeout,
200
+ idempotency_key=idempotency_key,
201
+ no_auth=no_auth,
202
+ )
203
+ return res.data
204
+
205
+ async def aclose(self) -> None:
206
+ """Closes the HTTP connection pool (not a ``http_client`` you passed in)."""
207
+ await self._http.aclose()
208
+
209
+ async def __aenter__(self) -> AsyncRelay:
210
+ return self
211
+
212
+ async def __aexit__(
213
+ self,
214
+ exc_type: type[BaseException] | None,
215
+ exc: BaseException | None,
216
+ tb: TracebackType | None,
217
+ ) -> None:
218
+ await self.aclose()
@@ -0,0 +1,18 @@
1
+ """``estimate_cost`` (port of src/estimate.ts): the one network call; the math lives in ``relaygpu/_estimate.py``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from .._estimate import estimate
8
+ from ..types import CostEstimate, UsageInput
9
+
10
+ if TYPE_CHECKING:
11
+ from .client import AsyncRelay
12
+
13
+
14
+ async def estimate_cost(relay: AsyncRelay, model: str, usage: UsageInput) -> CostEstimate:
15
+ """Estimates the USD cost of a call from ``GET /v2/pricing`` (the only network call). An estimate, never an
16
+ invoice: the bill comes from provider-reported usage and any custom-tier pricing your account carries. Raises a
17
+ ``RelayError`` for a model with no row in the mode, an unknown billing type or an unoffered storage SKU."""
18
+ return estimate(await relay.pricing.get(), model, usage)