corent 0.1.0__tar.gz

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.
@@ -0,0 +1,25 @@
1
+ .env
2
+ .env.local
3
+ .env.*.local
4
+
5
+ # Python
6
+ backend/venv/
7
+ __pycache__/
8
+ *.pyc
9
+ .pytest_cache/
10
+
11
+ # Node / Next.js
12
+ frontend/node_modules/
13
+ .next/
14
+ frontend/out/
15
+
16
+ # OS
17
+ .DS_Store
18
+
19
+ # MCP package
20
+ mcp/node_modules/
21
+ mcp/dist/
22
+ .vercel
23
+
24
+ # fund-agent local state (balance history)
25
+ fund-agent/state/
corent-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: corent
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for Corent — one API for AI image, video, and voice generation with built-in routing, failover, and exact receipts.
5
+ Project-URL: Homepage, https://corent.tech
6
+ Project-URL: Documentation, https://corent.tech/docs
7
+ Author-email: Corent <krrocicrypto@gmail.com>
8
+ License-Expression: MIT
9
+ Keywords: ai,api,corent,image-generation,text-to-speech,video-generation
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.9
15
+ Requires-Dist: httpx>=0.24
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Corent Python SDK
19
+
20
+ One API for AI **image, video, and voice** generation. You pick a quality tier; Corent's router picks the best live model, reroutes failures, verifies the output, and returns the exact charge on every response. Failed generations are never billed.
21
+
22
+ ```bash
23
+ pip install corent
24
+ ```
25
+
26
+ ```python
27
+ from corent import Corent
28
+
29
+ client = Corent("co_live_...") # get a key at https://corent.tech
30
+
31
+ image = client.images.generate("a lighthouse at dusk", tier="premium", aspect_ratio="9:16")
32
+ print(image.url, image.width, image.height, image.cost_cents)
33
+
34
+ video = client.videos.generate("a paper boat drifting across a puddle",
35
+ tier="premium", duration_s=5, resolution="1080p")
36
+ print(video.url, video.resolution, video.cost_cents)
37
+
38
+ speech = client.speech.generate("Welcome to Corent.")
39
+ print(speech.url, speech.cost_cents)
40
+ ```
41
+
42
+ ## What the SDK handles for you
43
+
44
+ - **Timeout-safe renders** — images submit as background jobs and are polled; a network hiccup can never lose a finished (and billed) result.
45
+ - **Safe retries** — every generate call carries an auto idempotency key; retries can never double-charge.
46
+ - **Backoff** — 429/5xx are retried with `Retry-After` respected.
47
+ - **Honest receipts** — `width`/`height` are the *measured* pixels of the delivered file, and `cost_cents` is the exact charge.
48
+
49
+ ## Fine-grained control
50
+
51
+ ```python
52
+ job = client.images.generate("...", tier="max_pro", wait=False) # returns immediately
53
+ job = client.jobs.wait(job.id) # resume any time
54
+
55
+ client.tiers() # live catalog with honest min-max price ranges
56
+ client.balance_cents() # prepaid balance
57
+ ```
58
+
59
+ Tiers: `air` | `lite` | `premium` | `pro` | `max_pro` — see [corent.tech/pricing](https://corent.tech/pricing). Docs: [corent.tech/docs](https://corent.tech/docs).
corent-0.1.0/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # Corent Python SDK
2
+
3
+ One API for AI **image, video, and voice** generation. You pick a quality tier; Corent's router picks the best live model, reroutes failures, verifies the output, and returns the exact charge on every response. Failed generations are never billed.
4
+
5
+ ```bash
6
+ pip install corent
7
+ ```
8
+
9
+ ```python
10
+ from corent import Corent
11
+
12
+ client = Corent("co_live_...") # get a key at https://corent.tech
13
+
14
+ image = client.images.generate("a lighthouse at dusk", tier="premium", aspect_ratio="9:16")
15
+ print(image.url, image.width, image.height, image.cost_cents)
16
+
17
+ video = client.videos.generate("a paper boat drifting across a puddle",
18
+ tier="premium", duration_s=5, resolution="1080p")
19
+ print(video.url, video.resolution, video.cost_cents)
20
+
21
+ speech = client.speech.generate("Welcome to Corent.")
22
+ print(speech.url, speech.cost_cents)
23
+ ```
24
+
25
+ ## What the SDK handles for you
26
+
27
+ - **Timeout-safe renders** — images submit as background jobs and are polled; a network hiccup can never lose a finished (and billed) result.
28
+ - **Safe retries** — every generate call carries an auto idempotency key; retries can never double-charge.
29
+ - **Backoff** — 429/5xx are retried with `Retry-After` respected.
30
+ - **Honest receipts** — `width`/`height` are the *measured* pixels of the delivered file, and `cost_cents` is the exact charge.
31
+
32
+ ## Fine-grained control
33
+
34
+ ```python
35
+ job = client.images.generate("...", tier="max_pro", wait=False) # returns immediately
36
+ job = client.jobs.wait(job.id) # resume any time
37
+
38
+ client.tiers() # live catalog with honest min-max price ranges
39
+ client.balance_cents() # prepaid balance
40
+ ```
41
+
42
+ Tiers: `air` | `lite` | `premium` | `pro` | `max_pro` — see [corent.tech/pricing](https://corent.tech/pricing). Docs: [corent.tech/docs](https://corent.tech/docs).
@@ -0,0 +1,31 @@
1
+ """Corent Python SDK — one API for AI image, video, and voice.
2
+
3
+ from corent import Corent
4
+
5
+ client = Corent("co_live_...")
6
+ image = client.images.generate("a lighthouse at dusk", tier="premium")
7
+ print(image.url, image.width, image.height, image.cost_cents)
8
+ """
9
+
10
+ from .client import Corent
11
+ from .errors import (
12
+ CorentError,
13
+ InsufficientBalanceError,
14
+ InvalidRequestError,
15
+ RateLimitedError,
16
+ )
17
+ from .types import GeneratedImage, GeneratedSpeech, GeneratedVideo, Job
18
+
19
+ __all__ = [
20
+ "Corent",
21
+ "CorentError",
22
+ "InsufficientBalanceError",
23
+ "InvalidRequestError",
24
+ "RateLimitedError",
25
+ "GeneratedImage",
26
+ "GeneratedVideo",
27
+ "GeneratedSpeech",
28
+ "Job",
29
+ ]
30
+
31
+ __version__ = "0.1.0"
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ """The Corent client. Hides everything the raw HTTP caller has to hand-roll:
4
+ auth headers, idempotency keys, 429/5xx retries with backoff, and job polling.
5
+
6
+ Design choices worth knowing:
7
+
8
+ - Image generation submits with async=true and polls the job. A finished
9
+ render can then never be lost to a client-side timeout — the job id exists
10
+ from the first response, and the SDK just resumes polling.
11
+ - Every generate call sends an auto-generated Idempotency-Key, so the SDK's
12
+ own retries (and yours) can never double-charge.
13
+ - Failed generations are never billed by the API; the SDK surfaces them as
14
+ GenerationFailedError with the API's sanitized error code.
15
+ """
16
+
17
+ import time
18
+ import uuid
19
+
20
+ import httpx
21
+
22
+ from .errors import (
23
+ CorentError,
24
+ GenerationFailedError,
25
+ InsufficientBalanceError,
26
+ InvalidRequestError,
27
+ RateLimitedError,
28
+ )
29
+ from .types import GeneratedImage, GeneratedSpeech, GeneratedVideo, Job
30
+
31
+ DEFAULT_BASE_URL = "https://api.corent.tech"
32
+ MAX_RETRIES = 3
33
+ POLL_INTERVAL_S = 3.0
34
+ DEFAULT_WAIT_TIMEOUT_S = 900 # videos can take minutes; images far less
35
+
36
+
37
+ class Corent:
38
+ def __init__(
39
+ self,
40
+ api_key: str,
41
+ base_url: str = DEFAULT_BASE_URL,
42
+ timeout: float = 60.0,
43
+ ):
44
+ self._http = httpx.Client(
45
+ base_url=base_url,
46
+ timeout=timeout,
47
+ headers={
48
+ "Authorization": f"Bearer {api_key}",
49
+ "User-Agent": "corent-python/0.1.0",
50
+ },
51
+ )
52
+ self.images = _Images(self)
53
+ self.videos = _Videos(self)
54
+ self.speech = _Speech(self)
55
+ self.jobs = _Jobs(self)
56
+
57
+ # -- account & discovery -------------------------------------------------
58
+
59
+ def balance_cents(self) -> int:
60
+ return self._request("GET", "/v1/account/balance")["balance_cents"]
61
+
62
+ def tiers(self) -> list[dict]:
63
+ """The live tier catalog with honest price ranges. Public endpoint."""
64
+ return self._request("GET", "/v1/tiers")["tiers"]
65
+
66
+ def close(self) -> None:
67
+ self._http.close()
68
+
69
+ def __enter__(self):
70
+ return self
71
+
72
+ def __exit__(self, *exc):
73
+ self.close()
74
+
75
+ # -- plumbing ------------------------------------------------------------
76
+
77
+ def _request(self, method: str, path: str, json: dict | None = None,
78
+ idempotent: bool = False) -> dict:
79
+ headers = {}
80
+ if idempotent:
81
+ headers["Idempotency-Key"] = str(uuid.uuid4())
82
+ last_exc: Exception | None = None
83
+ for attempt in range(MAX_RETRIES + 1):
84
+ try:
85
+ resp = self._http.request(method, path, json=json, headers=headers)
86
+ except httpx.TransportError as exc:
87
+ last_exc = exc
88
+ time.sleep(min(2**attempt, 8))
89
+ continue
90
+ if resp.status_code < 400:
91
+ return resp.json()
92
+ if resp.status_code == 429:
93
+ retry_after = float(resp.headers.get("Retry-After", 2 ** (attempt + 1)))
94
+ if attempt < MAX_RETRIES:
95
+ time.sleep(min(retry_after, 30))
96
+ continue
97
+ raise RateLimitedError("rate limited", retry_after_s=retry_after)
98
+ if resp.status_code in (502, 503) and attempt < MAX_RETRIES:
99
+ time.sleep(min(2 ** (attempt + 1), 8))
100
+ continue
101
+ self._raise_for(resp)
102
+ raise CorentError(f"request failed after {MAX_RETRIES} retries: {last_exc}")
103
+
104
+ @staticmethod
105
+ def _raise_for(resp: httpx.Response) -> None:
106
+ try:
107
+ detail = resp.json().get("detail", resp.text)
108
+ except Exception:
109
+ detail = resp.text
110
+ message = detail if isinstance(detail, str) else str(detail)
111
+ if resp.status_code == 402:
112
+ raise InsufficientBalanceError(message, 402)
113
+ if resp.status_code in (400, 422):
114
+ raise InvalidRequestError(message, resp.status_code)
115
+ raise CorentError(message, resp.status_code)
116
+
117
+ def _wait(self, job_id: str, timeout_s: float) -> dict:
118
+ deadline = time.monotonic() + timeout_s
119
+ while time.monotonic() < deadline:
120
+ job = self._request("GET", f"/v1/jobs/{job_id}")
121
+ if job["status"] == "completed":
122
+ return job
123
+ if job["status"] == "failed":
124
+ raise GenerationFailedError(job.get("error") or "generation_failed")
125
+ time.sleep(POLL_INTERVAL_S)
126
+ raise CorentError(f"job {job_id} still processing after {timeout_s:.0f}s; "
127
+ f"poll client.jobs.get('{job_id}') to resume")
128
+
129
+
130
+ class _Images:
131
+ def __init__(self, client: Corent):
132
+ self._c = client
133
+
134
+ def generate(
135
+ self,
136
+ prompt: str,
137
+ tier: str | None = None,
138
+ style: str | None = None,
139
+ aspect_ratio: str = "1:1",
140
+ wait: bool = True,
141
+ timeout_s: float = DEFAULT_WAIT_TIMEOUT_S,
142
+ ) -> "GeneratedImage | Job":
143
+ """Render an image. tier: air | lite | premium | pro | max_pro.
144
+ wait=False returns the Job immediately; poll with client.jobs."""
145
+ body = {"prompt": prompt, "aspect_ratio": aspect_ratio, "async": True}
146
+ if tier:
147
+ body["tier"] = tier
148
+ if style:
149
+ body["style"] = style
150
+ submitted = self._c._request("POST", "/v1/images/generate", json=body, idempotent=True)
151
+ if submitted["status"] == "completed": # idempotent replay of a finished job
152
+ return _image_from_job(submitted)
153
+ if not wait:
154
+ return Job(id=submitted["id"], status=submitted["status"], raw=submitted)
155
+ return _image_from_job(self._c._wait(submitted["id"], timeout_s))
156
+
157
+
158
+ class _Videos:
159
+ def __init__(self, client: Corent):
160
+ self._c = client
161
+
162
+ def generate(
163
+ self,
164
+ prompt: str,
165
+ tier: str | None = None,
166
+ aspect_ratio: str = "16:9",
167
+ duration_s: int | None = None,
168
+ resolution: str | None = None,
169
+ image_url: str | None = None,
170
+ wait: bool = True,
171
+ timeout_s: float = DEFAULT_WAIT_TIMEOUT_S,
172
+ ) -> "GeneratedVideo | Job":
173
+ """Render a video (1-5 minutes typical). resolution: 720p | 1080p | 4k
174
+ (tier-capped; clamped down, never rejected). image_url animates an
175
+ existing image instead of starting from text."""
176
+ body: dict = {"prompt": prompt, "aspect_ratio": aspect_ratio}
177
+ if tier:
178
+ body["tier"] = tier
179
+ if duration_s is not None:
180
+ body["duration_s"] = duration_s
181
+ if resolution:
182
+ body["resolution"] = resolution
183
+ if image_url:
184
+ body["image_url"] = image_url
185
+ submitted = self._c._request("POST", "/v1/videos/generate", json=body, idempotent=True)
186
+ if not wait:
187
+ return Job(id=submitted["id"], status=submitted["status"], raw=submitted)
188
+ return _video_from_job(self._c._wait(submitted["id"], timeout_s))
189
+
190
+
191
+ class _Speech:
192
+ def __init__(self, client: Corent):
193
+ self._c = client
194
+
195
+ def generate(self, text: str, voice_id: str | None = None) -> GeneratedSpeech:
196
+ """Text to speech; synchronous, returns the finished audio."""
197
+ body: dict = {"text": text}
198
+ if voice_id:
199
+ body["voice_id"] = voice_id
200
+ r = self._c._request("POST", "/v1/audio/speech", json=body, idempotent=True)
201
+ meta = r.get("meta") or {}
202
+ return GeneratedSpeech(id=r["id"], url=r["audio_url"],
203
+ model=meta.get("model"), cost_cents=meta.get("cost_cents"))
204
+
205
+
206
+ class _Jobs:
207
+ def __init__(self, client: Corent):
208
+ self._c = client
209
+
210
+ def get(self, job_id: str) -> Job:
211
+ raw = self._c._request("GET", f"/v1/jobs/{job_id}")
212
+ return Job(id=raw["id"], status=raw["status"], raw=raw)
213
+
214
+ def wait(self, job_id: str, timeout_s: float = DEFAULT_WAIT_TIMEOUT_S) -> Job:
215
+ raw = self._c._wait(job_id, timeout_s)
216
+ return Job(id=raw["id"], status=raw["status"], raw=raw)
217
+
218
+
219
+ def _image_from_job(job: dict) -> GeneratedImage:
220
+ image = (job.get("images") or [{}])[0]
221
+ meta = job.get("meta") or {}
222
+ return GeneratedImage(
223
+ id=job["id"], url=image.get("url"), width=image.get("width"),
224
+ height=image.get("height"), model=meta.get("model"), cost_cents=meta.get("cost_cents"),
225
+ )
226
+
227
+
228
+ def _video_from_job(job: dict) -> GeneratedVideo:
229
+ video = (job.get("videos") or [{}])[0]
230
+ meta = job.get("meta") or {}
231
+ return GeneratedVideo(
232
+ id=job["id"], url=video.get("url"), width=video.get("width"),
233
+ height=video.get("height"), resolution=video.get("resolution"),
234
+ duration_s=video.get("duration_s"), model=meta.get("model"),
235
+ cost_cents=meta.get("cost_cents"),
236
+ )
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class CorentError(Exception):
5
+ """Base error: carries the HTTP status and the API's error detail."""
6
+
7
+ def __init__(self, message: str, status_code: int | None = None):
8
+ self.status_code = status_code
9
+ super().__init__(message)
10
+
11
+
12
+ class InvalidRequestError(CorentError):
13
+ """400/422: fix the highlighted field and resend."""
14
+
15
+
16
+ class InsufficientBalanceError(CorentError):
17
+ """402: add funds at https://corent.tech/dashboard."""
18
+
19
+
20
+ class RateLimitedError(CorentError):
21
+ """429 after retries were exhausted: slow down or raise your key limits."""
22
+
23
+ def __init__(self, message: str, retry_after_s: float | None = None):
24
+ self.retry_after_s = retry_after_s
25
+ super().__init__(message, 429)
26
+
27
+
28
+ class GenerationFailedError(CorentError):
29
+ """The job reached a terminal failed state. You were not charged."""
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class GeneratedImage:
8
+ """A finished image. width/height are the MEASURED pixels of the delivered
9
+ file (Corent parses the file itself, never echoes your request)."""
10
+
11
+ id: str
12
+ url: str
13
+ width: int | None
14
+ height: int | None
15
+ model: str | None # Corent tier that served it, e.g. "image-premium"
16
+ cost_cents: int | None
17
+
18
+
19
+ @dataclass
20
+ class GeneratedVideo:
21
+ id: str
22
+ url: str
23
+ width: int | None
24
+ height: int | None
25
+ resolution: str | None
26
+ duration_s: int | None
27
+ model: str | None
28
+ cost_cents: int | None
29
+
30
+
31
+ @dataclass
32
+ class GeneratedSpeech:
33
+ id: str
34
+ url: str
35
+ model: str | None
36
+ cost_cents: int | None
37
+
38
+
39
+ @dataclass
40
+ class Job:
41
+ """Raw job state for callers driving their own polling."""
42
+
43
+ id: str
44
+ status: str # "processing" | "completed" | "failed"
45
+ raw: dict
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "corent"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for Corent — one API for AI image, video, and voice generation with built-in routing, failover, and exact receipts."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Corent", email = "krrocicrypto@gmail.com" }]
13
+ keywords = ["ai", "image-generation", "video-generation", "text-to-speech", "api", "corent"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ ]
20
+ dependencies = ["httpx>=0.24"]
21
+
22
+ [project.urls]
23
+ Homepage = "https://corent.tech"
24
+ Documentation = "https://corent.tech/docs"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["corent"]
@@ -0,0 +1,87 @@
1
+ """SDK behavior tests against a mock transport: no network, no spend."""
2
+
3
+ import json
4
+
5
+ import httpx
6
+ import pytest
7
+
8
+ import corent.client as client_mod
9
+ from corent import Corent, InsufficientBalanceError
10
+ from corent.errors import GenerationFailedError
11
+
12
+
13
+ def make_client(handler) -> Corent:
14
+ c = Corent("co_live_test", base_url="https://api.test")
15
+ c._http = httpx.Client(
16
+ base_url="https://api.test",
17
+ transport=httpx.MockTransport(handler),
18
+ headers={"Authorization": "Bearer co_live_test"},
19
+ )
20
+ return c
21
+
22
+
23
+ def test_image_generate_submits_async_and_polls(monkeypatch):
24
+ monkeypatch.setattr(client_mod, "POLL_INTERVAL_S", 0)
25
+ calls = []
26
+
27
+ def handler(request: httpx.Request) -> httpx.Response:
28
+ calls.append((request.method, request.url.path))
29
+ if request.url.path == "/v1/images/generate":
30
+ body = json.loads(request.content)
31
+ assert body["async"] is True # timeout-safety by default
32
+ assert request.headers.get("Idempotency-Key") # auto retry-safety
33
+ return httpx.Response(202, json={"id": "j1", "status": "processing"})
34
+ if len(calls) < 3:
35
+ return httpx.Response(200, json={"id": "j1", "status": "processing"})
36
+ return httpx.Response(200, json={
37
+ "id": "j1", "status": "completed",
38
+ "images": [{"url": "https://cdn/x.jpg", "width": 608, "height": 1088}],
39
+ "meta": {"model": "image-air", "cost_cents": 2},
40
+ })
41
+
42
+ image = make_client(handler).images.generate("a lighthouse", tier="air", aspect_ratio="9:16")
43
+ assert (image.width, image.height, image.cost_cents) == (608, 1088, 2)
44
+ assert calls[0] == ("POST", "/v1/images/generate")
45
+
46
+
47
+ def test_failed_job_raises_and_names_the_error(monkeypatch):
48
+ monkeypatch.setattr(client_mod, "POLL_INTERVAL_S", 0)
49
+
50
+ def handler(request):
51
+ if request.url.path == "/v1/images/generate":
52
+ return httpx.Response(202, json={"id": "j2", "status": "processing"})
53
+ return httpx.Response(200, json={"id": "j2", "status": "failed", "error": "all_providers_failed"})
54
+
55
+ with pytest.raises(GenerationFailedError, match="all_providers_failed"):
56
+ make_client(handler).images.generate("x")
57
+
58
+
59
+ def test_402_maps_to_insufficient_balance():
60
+ def handler(request):
61
+ return httpx.Response(402, json={"detail": "Insufficient balance"})
62
+
63
+ with pytest.raises(InsufficientBalanceError):
64
+ make_client(handler).images.generate("x")
65
+
66
+
67
+ def test_429_retries_then_succeeds(monkeypatch):
68
+ monkeypatch.setattr(client_mod.time, "sleep", lambda s: None)
69
+ attempts = []
70
+
71
+ def handler(request):
72
+ attempts.append(1)
73
+ if len(attempts) == 1:
74
+ return httpx.Response(429, headers={"Retry-After": "0"})
75
+ return httpx.Response(200, json={"balance_cents": 845})
76
+
77
+ assert make_client(handler).balance_cents() == 845
78
+ assert len(attempts) == 2
79
+
80
+
81
+ def test_wait_false_returns_job_handle():
82
+ def handler(request):
83
+ return httpx.Response(202, json={"id": "j3", "status": "processing"})
84
+
85
+ job = make_client(handler).images.generate("x", wait=False)
86
+ assert job.id == "j3"
87
+ assert job.status == "processing"