usdf 0.1.2__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.
- usdf/__init__.py +102 -0
- usdf/_http.py +91 -0
- usdf/accept.py +187 -0
- usdf/client.py +842 -0
- usdf/errors.py +56 -0
- usdf/estimate.py +123 -0
- usdf/langchain.py +142 -0
- usdf/sse.py +49 -0
- usdf/types.py +407 -0
- usdf/x402.py +555 -0
- usdf-0.1.2.dist-info/METADATA +124 -0
- usdf-0.1.2.dist-info/RECORD +14 -0
- usdf-0.1.2.dist-info/WHEEL +4 -0
- usdf-0.1.2.dist-info/licenses/LICENSE +21 -0
usdf/__init__.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Client for the USDF metered inference gateway."""
|
|
2
|
+
|
|
3
|
+
from .client import DEFAULT_BASE_URL, SDK_VERSION, USDF, ChatCompletionStream, RunResult, X402
|
|
4
|
+
from .errors import USDFError
|
|
5
|
+
from .estimate import cost_units, estimate_prompt_tokens, max_cost_units, max_prompt_tokens, units
|
|
6
|
+
from .types import (
|
|
7
|
+
Account,
|
|
8
|
+
Amount,
|
|
9
|
+
ApiKey,
|
|
10
|
+
Balance,
|
|
11
|
+
ChatCompletion,
|
|
12
|
+
CreatedKey,
|
|
13
|
+
KeyLimits,
|
|
14
|
+
Pricing,
|
|
15
|
+
PricingModel,
|
|
16
|
+
Quote,
|
|
17
|
+
Receipt,
|
|
18
|
+
ReceiptLookup,
|
|
19
|
+
Session,
|
|
20
|
+
X402Receipt,
|
|
21
|
+
)
|
|
22
|
+
from .x402 import (
|
|
23
|
+
CHAIN_ID,
|
|
24
|
+
DEFAULT_MAX_AMOUNT,
|
|
25
|
+
GATEWAY_PAYEE,
|
|
26
|
+
GATEWAY_ORIGIN,
|
|
27
|
+
PERMIT2_ADDRESS,
|
|
28
|
+
PUBLIC_RPC_URL,
|
|
29
|
+
USDF_ADDRESS,
|
|
30
|
+
USDG_ADDRESS,
|
|
31
|
+
X402_EXACT_PERMIT2_PROXY,
|
|
32
|
+
PaymentLimits,
|
|
33
|
+
Signer,
|
|
34
|
+
check_accept,
|
|
35
|
+
choose_accept,
|
|
36
|
+
decode_payment_required,
|
|
37
|
+
decode_payment_response,
|
|
38
|
+
decode_uint256,
|
|
39
|
+
encode_payment_signature,
|
|
40
|
+
approve_permit2,
|
|
41
|
+
eth_account_signer,
|
|
42
|
+
permit2_allowance_call,
|
|
43
|
+
permit2_approval_call,
|
|
44
|
+
payment_limits,
|
|
45
|
+
read_permit2_allowance,
|
|
46
|
+
sign_exact_payment,
|
|
47
|
+
wallet_signer,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"USDF",
|
|
52
|
+
"USDFError",
|
|
53
|
+
"X402",
|
|
54
|
+
"DEFAULT_BASE_URL",
|
|
55
|
+
"SDK_VERSION",
|
|
56
|
+
"RunResult",
|
|
57
|
+
"ChatCompletionStream",
|
|
58
|
+
"Account",
|
|
59
|
+
"Amount",
|
|
60
|
+
"ApiKey",
|
|
61
|
+
"Balance",
|
|
62
|
+
"ChatCompletion",
|
|
63
|
+
"CreatedKey",
|
|
64
|
+
"KeyLimits",
|
|
65
|
+
"Pricing",
|
|
66
|
+
"PricingModel",
|
|
67
|
+
"Quote",
|
|
68
|
+
"Receipt",
|
|
69
|
+
"ReceiptLookup",
|
|
70
|
+
"Session",
|
|
71
|
+
"X402Receipt",
|
|
72
|
+
"cost_units",
|
|
73
|
+
"estimate_prompt_tokens",
|
|
74
|
+
"max_cost_units",
|
|
75
|
+
"max_prompt_tokens",
|
|
76
|
+
"units",
|
|
77
|
+
"PERMIT2_ADDRESS",
|
|
78
|
+
"X402_EXACT_PERMIT2_PROXY",
|
|
79
|
+
"CHAIN_ID",
|
|
80
|
+
"USDF_ADDRESS",
|
|
81
|
+
"USDG_ADDRESS",
|
|
82
|
+
"GATEWAY_PAYEE",
|
|
83
|
+
"GATEWAY_ORIGIN",
|
|
84
|
+
"PUBLIC_RPC_URL",
|
|
85
|
+
"DEFAULT_MAX_AMOUNT",
|
|
86
|
+
"PaymentLimits",
|
|
87
|
+
"payment_limits",
|
|
88
|
+
"check_accept",
|
|
89
|
+
"Signer",
|
|
90
|
+
"choose_accept",
|
|
91
|
+
"decode_payment_required",
|
|
92
|
+
"decode_payment_response",
|
|
93
|
+
"decode_uint256",
|
|
94
|
+
"encode_payment_signature",
|
|
95
|
+
"eth_account_signer",
|
|
96
|
+
"permit2_allowance_call",
|
|
97
|
+
"permit2_approval_call",
|
|
98
|
+
"sign_exact_payment",
|
|
99
|
+
"wallet_signer",
|
|
100
|
+
"read_permit2_allowance",
|
|
101
|
+
"approve_permit2",
|
|
102
|
+
]
|
usdf/_http.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""HTTP transport: httpx when installed (and not disabled), the standard library's urllib otherwise.
|
|
2
|
+
|
|
3
|
+
Both return the same shape so the client never knows which ran: a status, lower-cased headers, and
|
|
4
|
+
either the whole body or an iterator of byte chunks for streams.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.request
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, Iterator, Optional
|
|
15
|
+
|
|
16
|
+
try: # optional
|
|
17
|
+
import httpx # type: ignore
|
|
18
|
+
except Exception: # pragma: no cover - depends on the environment
|
|
19
|
+
httpx = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Response:
|
|
24
|
+
status: int
|
|
25
|
+
headers: Dict[str, str]
|
|
26
|
+
body: bytes
|
|
27
|
+
|
|
28
|
+
def json(self) -> Any:
|
|
29
|
+
if not self.body:
|
|
30
|
+
return None
|
|
31
|
+
try:
|
|
32
|
+
return json.loads(self.body.decode("utf-8"))
|
|
33
|
+
except ValueError:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class StreamResponse:
|
|
39
|
+
status: int
|
|
40
|
+
headers: Dict[str, str]
|
|
41
|
+
chunks: Iterator[bytes]
|
|
42
|
+
close: Any
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pick_transport(name: Optional[str]) -> str:
|
|
46
|
+
name = name or os.environ.get("USDF_TRANSPORT") or ("urllib" if os.environ.get("USDF_NO_HTTPX") else None)
|
|
47
|
+
if name == "httpx":
|
|
48
|
+
if httpx is None:
|
|
49
|
+
raise RuntimeError("httpx is not installed; pip install usdf[httpx]")
|
|
50
|
+
return "httpx"
|
|
51
|
+
if name == "urllib":
|
|
52
|
+
return "urllib"
|
|
53
|
+
return "httpx" if httpx is not None else "urllib"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Transport:
|
|
57
|
+
def __init__(self, name: Optional[str] = None):
|
|
58
|
+
self.name = pick_transport(name)
|
|
59
|
+
self._client = httpx.Client(timeout=None) if self.name == "httpx" else None
|
|
60
|
+
|
|
61
|
+
def request(self, method: str, url: str, headers: Dict[str, str], body: Optional[bytes], timeout: float) -> Response:
|
|
62
|
+
if self._client is not None:
|
|
63
|
+
r = self._client.request(method, url, headers=headers, content=body, timeout=timeout)
|
|
64
|
+
return Response(r.status_code, {k.lower(): v for k, v in r.headers.items()}, r.content)
|
|
65
|
+
req = urllib.request.Request(url, data=body, method=method, headers=headers)
|
|
66
|
+
try:
|
|
67
|
+
with urllib.request.urlopen(req, timeout=timeout) as res:
|
|
68
|
+
return Response(res.status, {k.lower(): v for k, v in res.headers.items()}, res.read())
|
|
69
|
+
except urllib.error.HTTPError as e:
|
|
70
|
+
return Response(e.code, {k.lower(): v for k, v in e.headers.items()}, e.read())
|
|
71
|
+
|
|
72
|
+
def stream(self, method: str, url: str, headers: Dict[str, str], body: Optional[bytes], timeout: float) -> StreamResponse:
|
|
73
|
+
if self._client is not None:
|
|
74
|
+
req = self._client.build_request(method, url, headers=headers, content=body, timeout=timeout)
|
|
75
|
+
r = self._client.send(req, stream=True)
|
|
76
|
+
return StreamResponse(r.status_code, {k.lower(): v for k, v in r.headers.items()}, r.iter_bytes(), r.close)
|
|
77
|
+
req = urllib.request.Request(url, data=body, method=method, headers=headers)
|
|
78
|
+
try:
|
|
79
|
+
res = urllib.request.urlopen(req, timeout=timeout)
|
|
80
|
+
except urllib.error.HTTPError as e:
|
|
81
|
+
data = e.read()
|
|
82
|
+
return StreamResponse(e.code, {k.lower(): v for k, v in e.headers.items()}, iter([data]), lambda: None)
|
|
83
|
+
|
|
84
|
+
def chunks() -> Iterator[bytes]:
|
|
85
|
+
while True:
|
|
86
|
+
piece = res.read1(8192) if hasattr(res, "read1") else res.read(8192)
|
|
87
|
+
if not piece:
|
|
88
|
+
return
|
|
89
|
+
yield piece
|
|
90
|
+
|
|
91
|
+
return StreamResponse(res.status, {k.lower(): v for k, v in res.headers.items()}, chunks(), res.close)
|
usdf/accept.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Accept USDF in one integration: charge your own API's callers in USDF over x402, settled by Foundry's facilitator
|
|
2
|
+
and paid on to your payout address in USDF (or in USDG, 1:1, with settle_as="usdg"). Your server needs your Foundry API
|
|
3
|
+
key (an approved partner's, in USDF_API_KEY) and a price.
|
|
4
|
+
|
|
5
|
+
from usdf.accept import AcceptUSDF
|
|
6
|
+
|
|
7
|
+
paid = AcceptUSDF(price="0.01")
|
|
8
|
+
app = paid.wsgi(app) # any WSGI app (Flask's app.wsgi_app, Django's application)
|
|
9
|
+
|
|
10
|
+
Or for one request, whatever the framework:
|
|
11
|
+
|
|
12
|
+
result = paid.charge(url, headers)
|
|
13
|
+
if not result.paid:
|
|
14
|
+
return result.status, result.headers, result.body # 402 asking for payment, or a refusal
|
|
15
|
+
# serve it; add result.headers (PAYMENT-RESPONSE) to the answer
|
|
16
|
+
|
|
17
|
+
A request without a payment is answered 402 with the PAYMENT-REQUIRED header any x402 client reads; one with a payment
|
|
18
|
+
(PAYMENT-SIGNATURE, or X-PAYMENT) is settled before the app runs, and the app runs only for a payment that settled.
|
|
19
|
+
Standard library only."""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import urllib.error
|
|
27
|
+
import urllib.request
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import Any, Callable, Dict, Iterable, Mapping, Optional, Union
|
|
30
|
+
|
|
31
|
+
DEFAULT_FACILITATOR_URL = "https://api.usdf.fi"
|
|
32
|
+
_PRICE = re.compile(r"^\d{1,5}(\.\d{1,6})?$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Accepted:
|
|
37
|
+
"""A settled payment: its transaction, payer and network, and what it came to for you."""
|
|
38
|
+
|
|
39
|
+
transaction: str
|
|
40
|
+
payer: str
|
|
41
|
+
network: str
|
|
42
|
+
amount: str
|
|
43
|
+
fee: str
|
|
44
|
+
paid_on: str
|
|
45
|
+
settle_as: str
|
|
46
|
+
settlement_id: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ChargeResult:
|
|
51
|
+
"""paid: the payment settled (payment holds it; add headers to your answer). Otherwise answer with status,
|
|
52
|
+
headers and body instead of running the request."""
|
|
53
|
+
|
|
54
|
+
paid: bool
|
|
55
|
+
status: int = 200
|
|
56
|
+
headers: Dict[str, str] = field(default_factory=dict)
|
|
57
|
+
body: bytes = b""
|
|
58
|
+
payment: Optional[Accepted] = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _refusal(status: int, code: str, message: str) -> ChargeResult:
|
|
62
|
+
body = json.dumps({"error": {"message": message, "type": "payment_error", "code": code}}).encode()
|
|
63
|
+
return ChargeResult(paid=False, status=status, headers={"content-type": "application/json"}, body=body)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _header(headers: Mapping[str, str], name: str) -> Optional[str]:
|
|
67
|
+
for k, v in headers.items():
|
|
68
|
+
if k.lower() == name:
|
|
69
|
+
return v
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AcceptUSDF:
|
|
74
|
+
"""Charges each request `price` USDF (a string like "0.01", or a function of the request's URL and headers)."""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
price: Union[str, Callable[[str, Mapping[str, str]], str]],
|
|
79
|
+
api_key: Optional[str] = None,
|
|
80
|
+
settle_as: str = "usdf",
|
|
81
|
+
description: str = "",
|
|
82
|
+
mime_type: str = "application/json",
|
|
83
|
+
base_url: Optional[str] = None,
|
|
84
|
+
timeout: float = 30.0,
|
|
85
|
+
opener: Optional[Callable[..., Any]] = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
if settle_as not in ("usdf", "usdg"):
|
|
88
|
+
raise ValueError('settle_as is "usdf" or "usdg"')
|
|
89
|
+
self.price = price
|
|
90
|
+
self.api_key = api_key
|
|
91
|
+
self.settle_as = settle_as
|
|
92
|
+
self.description = description
|
|
93
|
+
self.mime_type = mime_type
|
|
94
|
+
self.base_url = (base_url or os.environ.get("USDF_BASE_URL") or DEFAULT_FACILITATOR_URL).rstrip("/")
|
|
95
|
+
self.timeout = timeout
|
|
96
|
+
self._open = opener or urllib.request.urlopen
|
|
97
|
+
|
|
98
|
+
def charge(self, url: str, headers: Mapping[str, str]) -> ChargeResult:
|
|
99
|
+
"""One request checked: settled, or the answer to give instead."""
|
|
100
|
+
key = self.api_key or os.environ.get("USDF_API_KEY")
|
|
101
|
+
if not key:
|
|
102
|
+
return _refusal(503, "not_configured", "this API takes USDF, but its key is not set")
|
|
103
|
+
price = self.price(url, headers) if callable(self.price) else self.price
|
|
104
|
+
if not isinstance(price, str) or not _PRICE.match(price.strip()):
|
|
105
|
+
return _refusal(500, "bad_price", "this API's price is not an amount of USDF")
|
|
106
|
+
payment = _header(headers, "payment-signature") or _header(headers, "x-payment")
|
|
107
|
+
body: Dict[str, Any] = {
|
|
108
|
+
"amount": price.strip(),
|
|
109
|
+
"settle_as": self.settle_as,
|
|
110
|
+
"resource": {"url": url, "description": self.description, "mime_type": self.mime_type},
|
|
111
|
+
}
|
|
112
|
+
if payment:
|
|
113
|
+
body["payment"] = payment
|
|
114
|
+
req = urllib.request.Request(
|
|
115
|
+
self.base_url + "/v1/facilitator/charge",
|
|
116
|
+
data=json.dumps(body).encode(),
|
|
117
|
+
method="POST",
|
|
118
|
+
headers={"authorization": "Bearer " + key, "content-type": "application/json"},
|
|
119
|
+
)
|
|
120
|
+
try:
|
|
121
|
+
with self._open(req, timeout=self.timeout) as r:
|
|
122
|
+
status, raw, got = r.status, r.read(), {k.lower(): v for k, v in r.headers.items()}
|
|
123
|
+
except urllib.error.HTTPError as e:
|
|
124
|
+
status, raw, got = e.code, e.read(), {k.lower(): v for k, v in (e.headers or {}).items()}
|
|
125
|
+
except (urllib.error.URLError, OSError, ValueError):
|
|
126
|
+
return _refusal(503, "facilitator_unreachable", "the payment could not be checked just now; nothing was charged, try again")
|
|
127
|
+
if status == 402:
|
|
128
|
+
out = {"content-type": "application/json"}
|
|
129
|
+
for h in ("payment-required", "www-authenticate"):
|
|
130
|
+
if got.get(h):
|
|
131
|
+
out[h] = got[h]
|
|
132
|
+
return ChargeResult(paid=False, status=402, headers=out, body=raw)
|
|
133
|
+
if status != 200:
|
|
134
|
+
code = "payment_unavailable"
|
|
135
|
+
try:
|
|
136
|
+
code = str(json.loads(raw.decode() or "{}").get("error", {}).get("code") or code)
|
|
137
|
+
except (ValueError, AttributeError):
|
|
138
|
+
pass
|
|
139
|
+
return _refusal(429 if status == 429 else 503, code, "the payment could not be taken just now; nothing was charged, try again")
|
|
140
|
+
try:
|
|
141
|
+
v = json.loads(raw.decode())
|
|
142
|
+
except ValueError:
|
|
143
|
+
return _refusal(503, "payment_unavailable", "the payment could not be taken just now; try again")
|
|
144
|
+
if v.get("paid") is not True or not isinstance(v.get("transaction"), str):
|
|
145
|
+
return _refusal(503, "payment_unavailable", "the payment could not be taken just now; try again")
|
|
146
|
+
response = v.get("payment_response") or got.get("payment-response")
|
|
147
|
+
s = v.get("settlement") or {}
|
|
148
|
+
|
|
149
|
+
def usdf(x: Any) -> str:
|
|
150
|
+
return str((x or {}).get("usdf", ""))
|
|
151
|
+
|
|
152
|
+
return ChargeResult(
|
|
153
|
+
paid=True,
|
|
154
|
+
headers={"payment-response": response, "x-payment-response": response} if response else {},
|
|
155
|
+
payment=Accepted(
|
|
156
|
+
transaction=v["transaction"],
|
|
157
|
+
payer=str(v.get("payer", "")),
|
|
158
|
+
network=str(v.get("network", "")),
|
|
159
|
+
amount=usdf(s.get("amount")),
|
|
160
|
+
fee=usdf(s.get("fee")),
|
|
161
|
+
paid_on=usdf(s.get("paid_on")),
|
|
162
|
+
settle_as="usdg" if s.get("settle_as") == "usdg" else "usdf",
|
|
163
|
+
settlement_id=str(s.get("id", "")),
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def wsgi(self, app: Callable[..., Iterable[bytes]]) -> Callable[..., Iterable[bytes]]:
|
|
168
|
+
"""A WSGI app that charges every request before `app` runs it (the payment in environ["usdf.payment"])."""
|
|
169
|
+
|
|
170
|
+
def wrapped(environ: Dict[str, Any], start_response: Callable[..., Any]) -> Iterable[bytes]:
|
|
171
|
+
headers = {k[5:].replace("_", "-").lower(): v for k, v in environ.items() if k.startswith("HTTP_")}
|
|
172
|
+
scheme = environ.get("HTTP_X_FORWARDED_PROTO", environ.get("wsgi.url_scheme", "https")).split(",")[0].strip()
|
|
173
|
+
host = environ.get("HTTP_X_FORWARDED_HOST") or environ.get("HTTP_HOST") or environ.get("SERVER_NAME", "localhost")
|
|
174
|
+
path = environ.get("PATH_INFO", "/") + (("?" + environ["QUERY_STRING"]) if environ.get("QUERY_STRING") else "")
|
|
175
|
+
result = self.charge(f"{'http' if scheme == 'http' else 'https'}://{host}{path}", headers)
|
|
176
|
+
if not result.paid:
|
|
177
|
+
reason = {402: "Payment Required", 429: "Too Many Requests", 500: "Internal Server Error"}.get(result.status, "Service Unavailable")
|
|
178
|
+
start_response(f"{result.status} {reason}", list(result.headers.items()))
|
|
179
|
+
return [result.body]
|
|
180
|
+
environ["usdf.payment"] = result.payment
|
|
181
|
+
|
|
182
|
+
def start(status: str, response_headers: list, exc_info: Any = None) -> Any:
|
|
183
|
+
return start_response(status, list(response_headers) + list(result.headers.items()), exc_info) if exc_info else start_response(status, list(response_headers) + list(result.headers.items()))
|
|
184
|
+
|
|
185
|
+
return app(environ, start)
|
|
186
|
+
|
|
187
|
+
return wrapped
|