kranth 0.2.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.
- kranth/__init__.py +98 -0
- kranth/_async.py +416 -0
- kranth/_client.py +438 -0
- kranth/_errors.py +47 -0
- kranth/_models.py +291 -0
- kranth/_transport.py +63 -0
- kranth-0.2.0.dist-info/METADATA +152 -0
- kranth-0.2.0.dist-info/RECORD +9 -0
- kranth-0.2.0.dist-info/WHEEL +4 -0
kranth/__init__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Kranth — stress-test ideas with hundreds of AI personas.
|
|
2
|
+
|
|
3
|
+
The whole surface is two clients:
|
|
4
|
+
|
|
5
|
+
>>> from kranth import Kranth
|
|
6
|
+
>>> client = Kranth(api_key="kr_live_…")
|
|
7
|
+
>>> sim = client.sims.create(
|
|
8
|
+
... idea_text="Launch a paid Rust newsletter.",
|
|
9
|
+
... persona_count=50,
|
|
10
|
+
... model_id="claude-sonnet-4-6",
|
|
11
|
+
... )
|
|
12
|
+
>>> for event in client.sims.stream(sim.sim_id):
|
|
13
|
+
... print(event.kind, event.data)
|
|
14
|
+
|
|
15
|
+
>>> from kranth import AsyncKranth
|
|
16
|
+
>>> async with AsyncKranth(api_key="kr_live_…") as client:
|
|
17
|
+
... models = await client.models.list()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from kranth._async import AsyncKranth
|
|
21
|
+
from kranth._client import Kranth
|
|
22
|
+
from kranth._errors import (
|
|
23
|
+
KranthAPIError,
|
|
24
|
+
KranthAuthError,
|
|
25
|
+
KranthError,
|
|
26
|
+
KranthPaymentRequired,
|
|
27
|
+
KranthRateLimited,
|
|
28
|
+
KranthValidationError,
|
|
29
|
+
)
|
|
30
|
+
from kranth._models import (
|
|
31
|
+
ApiKey,
|
|
32
|
+
AudienceReaction,
|
|
33
|
+
CreateApiKeyResponse,
|
|
34
|
+
CreateReconResponse,
|
|
35
|
+
CreateSimResponse,
|
|
36
|
+
Debate,
|
|
37
|
+
DebateDetail,
|
|
38
|
+
DebateEvent,
|
|
39
|
+
DebateSynthesis,
|
|
40
|
+
DebateTurn,
|
|
41
|
+
ListDebatesResponse,
|
|
42
|
+
ListSimsResponse,
|
|
43
|
+
Me,
|
|
44
|
+
ModelInfo,
|
|
45
|
+
ReconDetail,
|
|
46
|
+
ReconEvent,
|
|
47
|
+
ReconFinding,
|
|
48
|
+
ReconRun,
|
|
49
|
+
ReconSource,
|
|
50
|
+
ReconSummary,
|
|
51
|
+
ReconTier,
|
|
52
|
+
SimEvent,
|
|
53
|
+
SimSummary,
|
|
54
|
+
SpeakerPreview,
|
|
55
|
+
Usage,
|
|
56
|
+
VerdictReport,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"Kranth",
|
|
61
|
+
"AsyncKranth",
|
|
62
|
+
"KranthError",
|
|
63
|
+
"KranthAPIError",
|
|
64
|
+
"KranthAuthError",
|
|
65
|
+
"KranthValidationError",
|
|
66
|
+
"KranthPaymentRequired",
|
|
67
|
+
"KranthRateLimited",
|
|
68
|
+
"ApiKey",
|
|
69
|
+
"CreateApiKeyResponse",
|
|
70
|
+
"CreateSimResponse",
|
|
71
|
+
"ListSimsResponse",
|
|
72
|
+
"Me",
|
|
73
|
+
"ModelInfo",
|
|
74
|
+
"SimEvent",
|
|
75
|
+
"SimSummary",
|
|
76
|
+
"Usage",
|
|
77
|
+
"VerdictReport",
|
|
78
|
+
# Recon
|
|
79
|
+
"CreateReconResponse",
|
|
80
|
+
"ReconSummary",
|
|
81
|
+
"ReconDetail",
|
|
82
|
+
"ReconRun",
|
|
83
|
+
"ReconFinding",
|
|
84
|
+
"ReconSource",
|
|
85
|
+
"ReconTier",
|
|
86
|
+
"ReconEvent",
|
|
87
|
+
# Debate
|
|
88
|
+
"Debate",
|
|
89
|
+
"DebateDetail",
|
|
90
|
+
"DebateTurn",
|
|
91
|
+
"DebateSynthesis",
|
|
92
|
+
"AudienceReaction",
|
|
93
|
+
"SpeakerPreview",
|
|
94
|
+
"ListDebatesResponse",
|
|
95
|
+
"DebateEvent",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
__version__ = "0.2.0"
|
kranth/_async.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Async client.
|
|
2
|
+
|
|
3
|
+
>>> async with AsyncKranth(api_key="kr_live_…") as client:
|
|
4
|
+
... models = await client.models.list()
|
|
5
|
+
... sim = await client.sims.create(idea_text="…", persona_count=50, model_id="claude-sonnet-4-6")
|
|
6
|
+
... async for ev in client.sims.stream(sim.sim_id):
|
|
7
|
+
... ...
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any, AsyncIterator
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from kranth._models import (
|
|
18
|
+
ApiKey,
|
|
19
|
+
AudienceReaction,
|
|
20
|
+
CreateApiKeyResponse,
|
|
21
|
+
CreateReconResponse,
|
|
22
|
+
CreateSimResponse,
|
|
23
|
+
Debate,
|
|
24
|
+
DebateDetail,
|
|
25
|
+
DebateEvent,
|
|
26
|
+
DebateSynthesis,
|
|
27
|
+
DebateTurn,
|
|
28
|
+
ListDebatesResponse,
|
|
29
|
+
ListSimsResponse,
|
|
30
|
+
Me,
|
|
31
|
+
ModelInfo,
|
|
32
|
+
ReconDetail,
|
|
33
|
+
ReconEvent,
|
|
34
|
+
ReconFinding,
|
|
35
|
+
ReconSource,
|
|
36
|
+
ReconSummary,
|
|
37
|
+
ReconRun,
|
|
38
|
+
ReconTier,
|
|
39
|
+
SimEvent,
|
|
40
|
+
SimSummary,
|
|
41
|
+
SpeakerPreview,
|
|
42
|
+
Usage,
|
|
43
|
+
)
|
|
44
|
+
from kranth._transport import DEFAULT_BASE_URL, build_headers, raise_for_status
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AsyncKranth:
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
api_key: str,
|
|
51
|
+
*,
|
|
52
|
+
base_url: str | None = None,
|
|
53
|
+
timeout: float = 30.0,
|
|
54
|
+
client: httpx.AsyncClient | None = None,
|
|
55
|
+
):
|
|
56
|
+
if not api_key:
|
|
57
|
+
raise ValueError("api_key is required")
|
|
58
|
+
self.api_key = api_key
|
|
59
|
+
self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
60
|
+
self._owns_client = client is None
|
|
61
|
+
self._http = client or httpx.AsyncClient(timeout=timeout)
|
|
62
|
+
self.sims = _AsyncSims(self)
|
|
63
|
+
self.recon = _AsyncRecon(self)
|
|
64
|
+
self.debates = _AsyncDebates(self)
|
|
65
|
+
self.api_keys = _AsyncApiKeys(self)
|
|
66
|
+
self.models = _AsyncModels(self)
|
|
67
|
+
self.billing = _AsyncBilling(self)
|
|
68
|
+
|
|
69
|
+
async def __aenter__(self) -> "AsyncKranth":
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
73
|
+
await self.aclose()
|
|
74
|
+
|
|
75
|
+
async def aclose(self) -> None:
|
|
76
|
+
if self._owns_client:
|
|
77
|
+
await self._http.aclose()
|
|
78
|
+
|
|
79
|
+
async def _request(
|
|
80
|
+
self,
|
|
81
|
+
method: str,
|
|
82
|
+
path: str,
|
|
83
|
+
*,
|
|
84
|
+
json_body: Any | None = None,
|
|
85
|
+
params: dict[str, Any] | None = None,
|
|
86
|
+
idempotency_key: str | None = None,
|
|
87
|
+
) -> Any:
|
|
88
|
+
resp = await self._http.request(
|
|
89
|
+
method,
|
|
90
|
+
f"{self.base_url}{path}",
|
|
91
|
+
headers=build_headers(self.api_key, idempotency_key),
|
|
92
|
+
json=json_body,
|
|
93
|
+
params=params,
|
|
94
|
+
)
|
|
95
|
+
raise_for_status(resp)
|
|
96
|
+
if resp.status_code == 204 or not resp.content:
|
|
97
|
+
return None
|
|
98
|
+
return resp.json()
|
|
99
|
+
|
|
100
|
+
async def me(self) -> Me:
|
|
101
|
+
return Me(**await self._request("GET", "/v1/me"))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class _AsyncSims:
|
|
105
|
+
def __init__(self, parent: AsyncKranth):
|
|
106
|
+
self._p = parent
|
|
107
|
+
|
|
108
|
+
async def create(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
idea_text: str,
|
|
112
|
+
persona_count: int,
|
|
113
|
+
model_id: str,
|
|
114
|
+
training_opt_in: bool | None = None,
|
|
115
|
+
idempotency_key: str | None = None,
|
|
116
|
+
) -> CreateSimResponse:
|
|
117
|
+
body: dict[str, Any] = {
|
|
118
|
+
"idea_text": idea_text,
|
|
119
|
+
"persona_count": persona_count,
|
|
120
|
+
"model_id": model_id,
|
|
121
|
+
}
|
|
122
|
+
if training_opt_in is not None:
|
|
123
|
+
body["training_opt_in"] = training_opt_in
|
|
124
|
+
data = await self._p._request("POST", "/v1/sims", json_body=body, idempotency_key=idempotency_key)
|
|
125
|
+
return CreateSimResponse(**data)
|
|
126
|
+
|
|
127
|
+
async def get(self, sim_id: str) -> SimSummary:
|
|
128
|
+
return SimSummary(**await self._p._request("GET", f"/v1/sims/{sim_id}"))
|
|
129
|
+
|
|
130
|
+
async def list(
|
|
131
|
+
self,
|
|
132
|
+
*,
|
|
133
|
+
status: str | None = None,
|
|
134
|
+
cursor: str | None = None,
|
|
135
|
+
limit: int | None = None,
|
|
136
|
+
) -> ListSimsResponse:
|
|
137
|
+
params: dict[str, Any] = {}
|
|
138
|
+
if status is not None:
|
|
139
|
+
params["status"] = status
|
|
140
|
+
if cursor is not None:
|
|
141
|
+
params["cursor"] = cursor
|
|
142
|
+
if limit is not None:
|
|
143
|
+
params["limit"] = limit
|
|
144
|
+
data = await self._p._request("GET", "/v1/sims", params=params)
|
|
145
|
+
return ListSimsResponse(
|
|
146
|
+
sims=[SimSummary(**s) for s in data["sims"]],
|
|
147
|
+
next_cursor=data.get("next_cursor"),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def cancel(self, sim_id: str) -> dict[str, Any]:
|
|
151
|
+
return await self._p._request("POST", f"/v1/sims/{sim_id}/cancel")
|
|
152
|
+
|
|
153
|
+
async def export(self, sim_id: str) -> dict[str, Any]:
|
|
154
|
+
return await self._p._request("GET", f"/v1/sims/{sim_id}/export")
|
|
155
|
+
|
|
156
|
+
async def stream(self, sim_id: str) -> AsyncIterator[SimEvent]:
|
|
157
|
+
token_resp = await self._p._request("POST", f"/v1/sims/{sim_id}/stream-token")
|
|
158
|
+
token = token_resp["token"]
|
|
159
|
+
url = f"{self._p.base_url}/v1/sims/{sim_id}/events"
|
|
160
|
+
async with self._p._http.stream(
|
|
161
|
+
"GET",
|
|
162
|
+
url,
|
|
163
|
+
params={"token": token},
|
|
164
|
+
headers={"accept": "text/event-stream"},
|
|
165
|
+
) as resp:
|
|
166
|
+
raise_for_status(resp)
|
|
167
|
+
buf: list[str] = []
|
|
168
|
+
async for raw in resp.aiter_lines():
|
|
169
|
+
if raw == "":
|
|
170
|
+
payload = "\n".join(l[5:].lstrip() for l in buf if l.startswith("data:"))
|
|
171
|
+
buf = []
|
|
172
|
+
if not payload:
|
|
173
|
+
continue
|
|
174
|
+
try:
|
|
175
|
+
obj = json.loads(payload)
|
|
176
|
+
except json.JSONDecodeError:
|
|
177
|
+
continue
|
|
178
|
+
yield SimEvent(
|
|
179
|
+
kind=obj.get("kind", ""),
|
|
180
|
+
sim_id=obj.get("sim_id", sim_id),
|
|
181
|
+
data=obj.get("data") or {},
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
buf.append(raw)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class _AsyncRecon:
|
|
188
|
+
"""Recon — web-grounded research mode (async)."""
|
|
189
|
+
|
|
190
|
+
def __init__(self, parent: AsyncKranth):
|
|
191
|
+
self._p = parent
|
|
192
|
+
|
|
193
|
+
async def create(
|
|
194
|
+
self,
|
|
195
|
+
*,
|
|
196
|
+
idea_text: str,
|
|
197
|
+
tier: str,
|
|
198
|
+
worker_model: str | None = None,
|
|
199
|
+
synth_model: str | None = None,
|
|
200
|
+
audience: str | None = None,
|
|
201
|
+
company_url: str | None = None,
|
|
202
|
+
idempotency_key: str | None = None,
|
|
203
|
+
) -> CreateReconResponse:
|
|
204
|
+
body: dict[str, Any] = {"idea_text": idea_text, "tier": tier}
|
|
205
|
+
if worker_model is not None:
|
|
206
|
+
body["worker_model"] = worker_model
|
|
207
|
+
if synth_model is not None:
|
|
208
|
+
body["synth_model"] = synth_model
|
|
209
|
+
if audience is not None:
|
|
210
|
+
body["audience"] = audience
|
|
211
|
+
if company_url is not None:
|
|
212
|
+
body["company_url"] = company_url
|
|
213
|
+
data = await self._p._request("POST", "/v1/recon", json_body=body, idempotency_key=idempotency_key)
|
|
214
|
+
return CreateReconResponse(**data)
|
|
215
|
+
|
|
216
|
+
async def get(self, recon_id: str) -> ReconDetail:
|
|
217
|
+
data = await self._p._request("GET", f"/v1/recon/{recon_id}")
|
|
218
|
+
return ReconDetail(
|
|
219
|
+
run=ReconRun(**data["run"]),
|
|
220
|
+
findings=[ReconFinding(**f) for f in data.get("findings", [])],
|
|
221
|
+
sources=[ReconSource(**s) for s in data.get("sources", [])],
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
async def list(self) -> list[ReconSummary]:
|
|
225
|
+
data = await self._p._request("GET", "/v1/recon")
|
|
226
|
+
return [ReconSummary(**r) for r in data["recons"]]
|
|
227
|
+
|
|
228
|
+
async def tiers(self) -> list[ReconTier]:
|
|
229
|
+
data = await self._p._request("GET", "/v1/recon/tiers")
|
|
230
|
+
return [ReconTier(**t) for t in data["tiers"]]
|
|
231
|
+
|
|
232
|
+
async def export(self, recon_id: str) -> dict[str, Any]:
|
|
233
|
+
return await self._p._request("GET", f"/v1/recon/{recon_id}/export")
|
|
234
|
+
|
|
235
|
+
async def stream(self, recon_id: str) -> AsyncIterator[ReconEvent]:
|
|
236
|
+
token = (await self._p._request("POST", f"/v1/recon/{recon_id}/stream-token"))["token"]
|
|
237
|
+
url = f"{self._p.base_url}/v1/recon/{recon_id}/events"
|
|
238
|
+
async with self._p._http.stream(
|
|
239
|
+
"GET", url, params={"token": token}, headers={"accept": "text/event-stream"}
|
|
240
|
+
) as resp:
|
|
241
|
+
raise_for_status(resp)
|
|
242
|
+
buf: list[str] = []
|
|
243
|
+
async for raw in resp.aiter_lines():
|
|
244
|
+
if raw == "":
|
|
245
|
+
payload = "\n".join(l[5:].lstrip() for l in buf if l.startswith("data:"))
|
|
246
|
+
buf = []
|
|
247
|
+
if not payload:
|
|
248
|
+
continue
|
|
249
|
+
try:
|
|
250
|
+
obj = json.loads(payload)
|
|
251
|
+
except json.JSONDecodeError:
|
|
252
|
+
continue
|
|
253
|
+
yield ReconEvent(
|
|
254
|
+
kind=obj.get("kind", ""),
|
|
255
|
+
recon_id=obj.get("recon_id", recon_id),
|
|
256
|
+
data=obj.get("data") or {},
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
buf.append(raw)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _parse_debate_detail(data: dict[str, Any]) -> DebateDetail:
|
|
263
|
+
data = dict(data)
|
|
264
|
+
synth = data.pop("synthesis", None)
|
|
265
|
+
audience = data.pop("audience", None) or []
|
|
266
|
+
return DebateDetail(
|
|
267
|
+
debate=Debate(**data),
|
|
268
|
+
synthesis=DebateSynthesis(**synth) if synth else None,
|
|
269
|
+
audience=[AudienceReaction(**a) for a in audience],
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class _AsyncDebates:
|
|
274
|
+
"""Debates — adversarial multi-persona panel mode (async)."""
|
|
275
|
+
|
|
276
|
+
def __init__(self, parent: AsyncKranth):
|
|
277
|
+
self._p = parent
|
|
278
|
+
|
|
279
|
+
async def create(
|
|
280
|
+
self,
|
|
281
|
+
*,
|
|
282
|
+
topic: str,
|
|
283
|
+
mode: str,
|
|
284
|
+
participants: int | None = None,
|
|
285
|
+
model_id: str | None = None,
|
|
286
|
+
training_opt_in: bool | None = None,
|
|
287
|
+
public: bool | None = None,
|
|
288
|
+
voice_enabled: bool | None = None,
|
|
289
|
+
archetype_slugs: list[str] | None = None,
|
|
290
|
+
idempotency_key: str | None = None,
|
|
291
|
+
) -> Debate:
|
|
292
|
+
body: dict[str, Any] = {"topic": topic, "mode": mode}
|
|
293
|
+
if participants is not None:
|
|
294
|
+
body["participants"] = participants
|
|
295
|
+
if model_id is not None:
|
|
296
|
+
body["model_id"] = model_id
|
|
297
|
+
if training_opt_in is not None:
|
|
298
|
+
body["training_opt_in"] = training_opt_in
|
|
299
|
+
if public is not None:
|
|
300
|
+
body["public"] = public
|
|
301
|
+
if voice_enabled is not None:
|
|
302
|
+
body["voice_enabled"] = voice_enabled
|
|
303
|
+
if archetype_slugs is not None:
|
|
304
|
+
body["archetype_slugs"] = archetype_slugs
|
|
305
|
+
data = await self._p._request("POST", "/v1/debates", json_body=body, idempotency_key=idempotency_key)
|
|
306
|
+
return Debate(**data)
|
|
307
|
+
|
|
308
|
+
async def get(self, debate_id: str) -> DebateDetail:
|
|
309
|
+
return _parse_debate_detail(await self._p._request("GET", f"/v1/debates/{debate_id}"))
|
|
310
|
+
|
|
311
|
+
async def list(self) -> ListDebatesResponse:
|
|
312
|
+
data = await self._p._request("GET", "/v1/debates")
|
|
313
|
+
return ListDebatesResponse(
|
|
314
|
+
debates=[Debate(**d) for d in data["debates"]],
|
|
315
|
+
speakers={
|
|
316
|
+
k: [SpeakerPreview(**s) for s in v]
|
|
317
|
+
for k, v in (data.get("speakers") or {}).items()
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
async def turns(self, debate_id: str) -> list[DebateTurn]:
|
|
322
|
+
data = await self._p._request("GET", f"/v1/debates/{debate_id}/turns")
|
|
323
|
+
return [DebateTurn(**t) for t in data["turns"]]
|
|
324
|
+
|
|
325
|
+
async def set_public(self, debate_id: str, public: bool) -> DebateDetail:
|
|
326
|
+
return _parse_debate_detail(
|
|
327
|
+
await self._p._request("PATCH", f"/v1/debates/{debate_id}", json_body={"public": public})
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
async def cancel(self, debate_id: str) -> dict[str, Any]:
|
|
331
|
+
return await self._p._request("POST", f"/v1/debates/{debate_id}/cancel")
|
|
332
|
+
|
|
333
|
+
async def export(self, debate_id: str) -> dict[str, Any]:
|
|
334
|
+
return await self._p._request("GET", f"/v1/debates/{debate_id}/export")
|
|
335
|
+
|
|
336
|
+
async def stream(self, debate_id: str) -> AsyncIterator[DebateEvent]:
|
|
337
|
+
token = (await self._p._request("POST", f"/v1/debates/{debate_id}/stream-token"))["token"]
|
|
338
|
+
url = f"{self._p.base_url}/v1/debates/{debate_id}/events"
|
|
339
|
+
async with self._p._http.stream(
|
|
340
|
+
"GET", url, params={"token": token}, headers={"accept": "text/event-stream"}
|
|
341
|
+
) as resp:
|
|
342
|
+
raise_for_status(resp)
|
|
343
|
+
buf: list[str] = []
|
|
344
|
+
async for raw in resp.aiter_lines():
|
|
345
|
+
if raw == "":
|
|
346
|
+
payload = "\n".join(l[5:].lstrip() for l in buf if l.startswith("data:"))
|
|
347
|
+
buf = []
|
|
348
|
+
if not payload:
|
|
349
|
+
continue
|
|
350
|
+
try:
|
|
351
|
+
obj = json.loads(payload)
|
|
352
|
+
except json.JSONDecodeError:
|
|
353
|
+
continue
|
|
354
|
+
yield DebateEvent(
|
|
355
|
+
kind=obj.get("kind", ""),
|
|
356
|
+
debate_id=obj.get("debate_id", debate_id),
|
|
357
|
+
data=obj.get("data") or {},
|
|
358
|
+
)
|
|
359
|
+
else:
|
|
360
|
+
buf.append(raw)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class _AsyncApiKeys:
|
|
364
|
+
def __init__(self, parent: AsyncKranth):
|
|
365
|
+
self._p = parent
|
|
366
|
+
|
|
367
|
+
async def create(self, name: str, *, env: str = "live") -> CreateApiKeyResponse:
|
|
368
|
+
data = await self._p._request("POST", "/v1/api-keys", json_body={"name": name, "env": env})
|
|
369
|
+
return CreateApiKeyResponse(**data)
|
|
370
|
+
|
|
371
|
+
async def list(self) -> list[ApiKey]:
|
|
372
|
+
data = await self._p._request("GET", "/v1/api-keys")
|
|
373
|
+
return [ApiKey(**k) for k in data["keys"]]
|
|
374
|
+
|
|
375
|
+
async def revoke(self, key_id: str) -> None:
|
|
376
|
+
await self._p._request("DELETE", f"/v1/api-keys/{key_id}")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class _AsyncModels:
|
|
380
|
+
def __init__(self, parent: AsyncKranth):
|
|
381
|
+
self._p = parent
|
|
382
|
+
|
|
383
|
+
async def list(self) -> list[ModelInfo]:
|
|
384
|
+
data = await self._p._request("GET", "/v1/models")
|
|
385
|
+
return [ModelInfo(**m) for m in data["models"]]
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class _AsyncBilling:
|
|
389
|
+
def __init__(self, parent: AsyncKranth):
|
|
390
|
+
self._p = parent
|
|
391
|
+
|
|
392
|
+
async def usage(self) -> Usage:
|
|
393
|
+
return Usage(**await self._p._request("GET", "/v1/usage"))
|
|
394
|
+
|
|
395
|
+
async def checkout_url(
|
|
396
|
+
self,
|
|
397
|
+
plan: str,
|
|
398
|
+
*,
|
|
399
|
+
billing_period: str = "monthly",
|
|
400
|
+
success_url: str | None = None,
|
|
401
|
+
cancel_url: str | None = None,
|
|
402
|
+
) -> str:
|
|
403
|
+
body: dict[str, Any] = {"plan": plan, "billing_period": billing_period}
|
|
404
|
+
if success_url:
|
|
405
|
+
body["success_url"] = success_url
|
|
406
|
+
if cancel_url:
|
|
407
|
+
body["cancel_url"] = cancel_url
|
|
408
|
+
data = await self._p._request("POST", "/v1/billing/checkout", json_body=body)
|
|
409
|
+
return data["url"]
|
|
410
|
+
|
|
411
|
+
async def portal_url(self, return_url: str | None = None) -> str:
|
|
412
|
+
body: dict[str, Any] = {}
|
|
413
|
+
if return_url:
|
|
414
|
+
body["return_url"] = return_url
|
|
415
|
+
data = await self._p._request("POST", "/v1/billing/portal", json_body=body)
|
|
416
|
+
return data["url"]
|