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.
- relaygpu/__init__.py +21 -0
- relaygpu/_async/__init__.py +1 -0
- relaygpu/_async/_http.py +146 -0
- relaygpu/_async/account.py +83 -0
- relaygpu/_async/audio.py +86 -0
- relaygpu/_async/client.py +218 -0
- relaygpu/_async/estimate.py +18 -0
- relaygpu/_async/files.py +293 -0
- relaygpu/_async/image.py +157 -0
- relaygpu/_async/keys.py +103 -0
- relaygpu/_async/models.py +75 -0
- relaygpu/_async/run.py +99 -0
- relaygpu/_async/tasks.py +66 -0
- relaygpu/_async/video.py +114 -0
- relaygpu/_async/webhooks.py +67 -0
- relaygpu/_async/workflows.py +149 -0
- relaygpu/_clock.py +31 -0
- relaygpu/_core.py +208 -0
- relaygpu/_errors.py +119 -0
- relaygpu/_estimate.py +224 -0
- relaygpu/_exceptions.py +166 -0
- relaygpu/_generated/__init__.py +1 -0
- relaygpu/_generated/error_codes.py +400 -0
- relaygpu/_generated/models_map.py +229 -0
- relaygpu/_generated/types.py +2676 -0
- relaygpu/_images.py +70 -0
- relaygpu/_run_common.py +124 -0
- relaygpu/_shapes.py +85 -0
- relaygpu/_sync/__init__.py +2 -0
- relaygpu/_sync/_http.py +147 -0
- relaygpu/_sync/account.py +84 -0
- relaygpu/_sync/audio.py +87 -0
- relaygpu/_sync/client.py +219 -0
- relaygpu/_sync/estimate.py +19 -0
- relaygpu/_sync/files.py +294 -0
- relaygpu/_sync/image.py +158 -0
- relaygpu/_sync/keys.py +104 -0
- relaygpu/_sync/models.py +76 -0
- relaygpu/_sync/run.py +100 -0
- relaygpu/_sync/tasks.py +67 -0
- relaygpu/_sync/video.py +115 -0
- relaygpu/_sync/webhooks.py +68 -0
- relaygpu/_sync/workflows.py +150 -0
- relaygpu/_util.py +73 -0
- relaygpu/_version.py +1 -0
- relaygpu/async_client.py +5 -0
- relaygpu/client.py +5 -0
- relaygpu/errors.py +10 -0
- relaygpu/inputs.py +306 -0
- relaygpu/py.typed +0 -0
- relaygpu/types.py +166 -0
- relaygpu/webhooks.py +212 -0
- relaygpu_client-0.1.0.dist-info/METADATA +567 -0
- relaygpu_client-0.1.0.dist-info/RECORD +56 -0
- relaygpu_client-0.1.0.dist-info/WHEEL +4 -0
- 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."""
|
relaygpu/_async/_http.py
ADDED
|
@@ -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)
|
relaygpu/_async/audio.py
ADDED
|
@@ -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)
|