commoncompute 0.1.0__tar.gz → 0.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commoncompute
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Official Python SDK for Common Compute — the batch AI bill you shouldn't be paying.
5
5
  Project-URL: Homepage, https://commoncompute.ai
6
6
  Project-URL: Documentation, https://commoncompute.ai/docs
@@ -61,7 +61,8 @@ import commoncompute as cc
61
61
 
62
62
  client = cc.Client() # CC_API_KEY, or ~/.commoncompute/config
63
63
 
64
- job = client.transcription.create("s3://my-bucket/meeting.mp3", timestamps=True)
64
+ job = client.submit("coreml_embed", {"input": ["what is the neural engine?"]},
65
+ model_id="bge-base")
65
66
  print(job.job_id, job.locked_price_usd, job.eta_seconds) # price known BEFORE it runs
66
67
 
67
68
  result = client.result(job) # waits, downloads, attaches the receipt
@@ -69,7 +70,10 @@ print(result.output)
69
70
  print(result.receipt) # signed proof of what ran, where, for how much
70
71
  ```
71
72
 
72
- ## Examples
73
+ Prefer the OpenAI-style surface? `client.embeddings.create(input=[...],
74
+ model="bge-base")` returns vectors synchronously.
75
+
76
+ ## Examples — live today
73
77
 
74
78
  ### 1. OCR / document extraction — vs AWS Textract
75
79
 
@@ -78,38 +82,40 @@ job = client.ocr.extract("https://example.com/contracts.pdf", structured=True)
78
82
  result = client.result(job) # blocks + bounding boxes as JSON
79
83
  ```
80
84
 
81
- ### 2. Transcription in bulk
85
+ ### 2. Embeddings in bulk
82
86
 
83
87
  ```python
84
- jobs = client.transcription.create_many(
85
- [f"s3://recordings/call-{i}.mp3" for i in range(200)],
86
- language="en",
88
+ for result in client.submit_many(
89
+ "coreml_embed",
90
+ [{"input": chunked(doc)} for doc in corpus],
87
91
  max_concurrent=16,
88
- )
92
+ ):
93
+ save(result.output) # results stream as they complete
89
94
  ```
90
95
 
91
- ### 3. Reranking — sharpen RAG retrieval
96
+ ### 3. Background removal
92
97
 
93
98
  ```python
94
- job = client.rerank("what is the neural engine?", documents, top_n=5)
95
- top = client.result(job).output
99
+ job = client.images.remove_background("product-shot.png")
96
100
  ```
97
101
 
98
- ### 4. Translation
102
+ Also live: speech synthesis, image classification/detection/pose, barcode
103
+ reading, HEIC conversion — plus the generic `client.submit(workload_id,
104
+ payload)` for anything marked **live** in
105
+ [the catalog](https://commoncompute.ai/workloads).
99
106
 
100
- ```python
101
- job = client.translate(catalog_descriptions, target_lang="de")
102
- ```
107
+ ### In preview
103
108
 
104
- ### 5. Background removal
109
+ `client.translate(...)` and `client.video.transcode(...)` run on preview
110
+ capacity — functional, not yet GA.
105
111
 
106
- ```python
107
- job = client.images.remove_background("product-shot.png")
108
- ```
112
+ ### Not live yet
109
113
 
110
- Also available: `client.video.transcode(...)`, `client.build.ios_test(...)`,
111
- `client.images.generate(...)`, `client.embeddings.create(...)`, and the
112
- generic `client.submit(workload_id, payload)` for anything in
114
+ `client.transcription.*`, `client.rerank(...)`, `client.images.generate(...)`,
115
+ `client.chat.*`, and `client.build.ios_test(...)` target workloads still
116
+ marked *coming soon*: the API refuses them with a clear
117
+ `workload_not_available` error (HTTP 409) rather than queueing a job that
118
+ won't run. They activate automatically as those lanes go live — watch
113
119
  [the catalog](https://commoncompute.ai/workloads).
114
120
 
115
121
  ## Price before execution, hard caps, dry runs
@@ -26,7 +26,8 @@ import commoncompute as cc
26
26
 
27
27
  client = cc.Client() # CC_API_KEY, or ~/.commoncompute/config
28
28
 
29
- job = client.transcription.create("s3://my-bucket/meeting.mp3", timestamps=True)
29
+ job = client.submit("coreml_embed", {"input": ["what is the neural engine?"]},
30
+ model_id="bge-base")
30
31
  print(job.job_id, job.locked_price_usd, job.eta_seconds) # price known BEFORE it runs
31
32
 
32
33
  result = client.result(job) # waits, downloads, attaches the receipt
@@ -34,7 +35,10 @@ print(result.output)
34
35
  print(result.receipt) # signed proof of what ran, where, for how much
35
36
  ```
36
37
 
37
- ## Examples
38
+ Prefer the OpenAI-style surface? `client.embeddings.create(input=[...],
39
+ model="bge-base")` returns vectors synchronously.
40
+
41
+ ## Examples — live today
38
42
 
39
43
  ### 1. OCR / document extraction — vs AWS Textract
40
44
 
@@ -43,38 +47,40 @@ job = client.ocr.extract("https://example.com/contracts.pdf", structured=True)
43
47
  result = client.result(job) # blocks + bounding boxes as JSON
44
48
  ```
45
49
 
46
- ### 2. Transcription in bulk
50
+ ### 2. Embeddings in bulk
47
51
 
48
52
  ```python
49
- jobs = client.transcription.create_many(
50
- [f"s3://recordings/call-{i}.mp3" for i in range(200)],
51
- language="en",
53
+ for result in client.submit_many(
54
+ "coreml_embed",
55
+ [{"input": chunked(doc)} for doc in corpus],
52
56
  max_concurrent=16,
53
- )
57
+ ):
58
+ save(result.output) # results stream as they complete
54
59
  ```
55
60
 
56
- ### 3. Reranking — sharpen RAG retrieval
61
+ ### 3. Background removal
57
62
 
58
63
  ```python
59
- job = client.rerank("what is the neural engine?", documents, top_n=5)
60
- top = client.result(job).output
64
+ job = client.images.remove_background("product-shot.png")
61
65
  ```
62
66
 
63
- ### 4. Translation
67
+ Also live: speech synthesis, image classification/detection/pose, barcode
68
+ reading, HEIC conversion — plus the generic `client.submit(workload_id,
69
+ payload)` for anything marked **live** in
70
+ [the catalog](https://commoncompute.ai/workloads).
64
71
 
65
- ```python
66
- job = client.translate(catalog_descriptions, target_lang="de")
67
- ```
72
+ ### In preview
68
73
 
69
- ### 5. Background removal
74
+ `client.translate(...)` and `client.video.transcode(...)` run on preview
75
+ capacity — functional, not yet GA.
70
76
 
71
- ```python
72
- job = client.images.remove_background("product-shot.png")
73
- ```
77
+ ### Not live yet
74
78
 
75
- Also available: `client.video.transcode(...)`, `client.build.ios_test(...)`,
76
- `client.images.generate(...)`, `client.embeddings.create(...)`, and the
77
- generic `client.submit(workload_id, payload)` for anything in
79
+ `client.transcription.*`, `client.rerank(...)`, `client.images.generate(...)`,
80
+ `client.chat.*`, and `client.build.ios_test(...)` target workloads still
81
+ marked *coming soon*: the API refuses them with a clear
82
+ `workload_not_available` error (HTTP 409) rather than queueing a job that
83
+ won't run. They activate automatically as those lanes go live — watch
78
84
  [the catalog](https://commoncompute.ai/workloads).
79
85
 
80
86
  ## Price before execution, hard caps, dry runs
@@ -11,33 +11,26 @@ Quickstart::
11
11
  client = cc.Client(api_key="cc_live_...")
12
12
  print(client.balance())
13
13
 
14
- # OpenAI-compatible chat
15
- reply = client.chat.create(
16
- model="qwen-2.5-7b-instruct",
17
- messages=[{"role": "user", "content": "hi"}],
18
- )
19
- print(reply["choices"][0]["message"]["content"])
20
-
21
- # Native job submission
14
+ # Native job submission (embeddings — live today)
22
15
  job = client.jobs.submit(
23
16
  workload_id="coreml_embed",
24
- payload={"inputs": ["hello world"]},
17
+ payload={"input": ["hello world"]},
25
18
  )
26
19
  result = client.jobs.wait(job["id"])
27
20
 
28
- Streaming chat::
21
+ # OpenAI-compatible embeddings
22
+ vectors = client.embeddings.create(input=["hello world"], model="bge-base")
29
23
 
30
- for chunk in client.chat.stream(model="qwen-2.5-7b-instruct",
31
- messages=[{"role": "user", "content": "hi"}]):
32
- delta = chunk["choices"][0].get("delta", {}).get("content", "")
33
- print(delta, end="", flush=True)
24
+ `client.chat.*` targets LLM workloads that are still marked *coming soon*
25
+ in the catalog — the API declines them with `workload_not_available`
26
+ (HTTP 409) until they go live. See https://commoncompute.ai/workloads.
34
27
  """
35
28
 
36
29
  # MUST come before any submodule imports — _transport.py reads
37
30
  # `commoncompute.__version__` at import time for the User-Agent header,
38
31
  # and the previous ordering (`__version__` defined at the bottom) made
39
32
  # every clean install of the SDK fail with a circular ImportError.
40
- __version__ = "0.1.0"
33
+ __version__ = "0.1.2"
41
34
 
42
35
  from .client import Client, AsyncClient
43
36
  from .errors import (
@@ -25,6 +25,19 @@ KNOWN_WORKLOAD_IDS: frozenset[str] = frozenset({
25
25
  "vision_face",
26
26
  "vision_detect",
27
27
  "vision_pose",
28
+ "vision_docseg",
29
+ "vision_rectangles",
30
+ "vision_hand_pose",
31
+ "vision_animal_pose",
32
+ "vision_saliency",
33
+ "vision_animals",
34
+ "vision_classify",
35
+ "vision_person_seg",
36
+ "vision_trajectory",
37
+ "vision_optical_flow",
38
+ "av_compose",
39
+ "pdfkit_ops",
40
+ "createml_train",
28
41
  "vision_barcode",
29
42
  "vision_segment",
30
43
  "coreml_upscale",
@@ -43,6 +56,16 @@ KNOWN_WORKLOAD_IDS: frozenset[str] = frozenset({
43
56
  "mlx_finetune",
44
57
  "pytorch_mps",
45
58
  "blender_metal",
59
+ "speech_transcribe",
60
+ "mlx_vlm",
61
+ "mlx_llm_cluster",
62
+ "foundation_llm",
63
+ "vision_similarity",
64
+ "image_filter",
65
+ "audio_normalize",
66
+ "webkit_test",
67
+ "notarize",
68
+ "coreml_convert",
46
69
  "cpu_bench",
47
70
  })
48
71
 
@@ -27,7 +27,10 @@ from . import __version__
27
27
  from .errors import CommonComputeError, NetworkError, _from_status
28
28
 
29
29
  DEFAULT_BASE_URL = "https://api.commoncompute.ai"
30
- DEFAULT_TIMEOUT = 60.0
30
+ # Above the API's 120s synchronous poll windows (chat/images block server-side
31
+ # up to 120s) — a 60s default made every slow-but-successful sync call time
32
+ # out client-side and retry into an idempotent-replay 202.
33
+ DEFAULT_TIMEOUT = 180.0
31
34
  DEFAULT_MAX_RETRIES = 2
32
35
 
33
36
 
@@ -40,8 +43,14 @@ def _key_from_config_files() -> Optional[str]:
40
43
  """
41
44
  import pathlib
42
45
 
46
+ base = pathlib.Path(os.environ.get("CC_CONFIG_DIR") or (pathlib.Path.home() / ".commoncompute"))
43
47
  candidates = [
44
- pathlib.Path(os.environ.get("CC_CONFIG_DIR") or (pathlib.Path.home() / ".commoncompute")) / "config",
48
+ base / "config",
49
+ # connect()/`cc login` save here (respects CC_CONFIG_DIR) — the
50
+ # transport previously only checked the hard-coded legacy path, so a
51
+ # key saved under a custom CC_CONFIG_DIR was invisible to new
52
+ # processes.
53
+ base / "credentials",
45
54
  pathlib.Path.home() / ".config" / "commoncompute" / "credentials",
46
55
  ]
47
56
  for path in candidates:
@@ -87,10 +96,13 @@ def _retry_delay(attempt: int, response: Optional[httpx.Response]) -> float:
87
96
  ra = response.headers.get("retry-after")
88
97
  if ra:
89
98
  try:
90
- return min(max(float(ra), 0.0), 30.0)
99
+ # Honor the server's window up to 60s — the API sends
100
+ # Retry-After: 60 on 429s, and clamping to 30 guaranteed the
101
+ # retry landed inside the same rate-limit window.
102
+ return min(max(float(ra), 0.0), 60.0)
91
103
  except ValueError:
92
104
  pass
93
- # 0.5s, 1.5s (+ jitter) — bounded so 3 attempts fit a 60s caller budget.
105
+ # 0.5s, 1.5s (+ jitter).
94
106
  return 0.5 * (3**attempt) + random.random() * 0.25
95
107
 
96
108
 
@@ -203,6 +215,7 @@ class SyncTransport(_BaseTransport):
203
215
  data: Any = None,
204
216
  files: Any = None,
205
217
  headers: Optional[Mapping[str, str]] = None,
218
+ raw: bool = False,
206
219
  ) -> Any:
207
220
  hdrs = _build_headers(self._api_key, self._org, headers)
208
221
  # Fresh Idempotency-Key per logical call makes retried POSTs replay
@@ -233,7 +246,9 @@ class SyncTransport(_BaseTransport):
233
246
  time.sleep(_retry_delay(attempt, resp))
234
247
  continue
235
248
  _raise_for(resp)
236
- return _parse_body(resp)
249
+ # raw=True hands back the httpx.Response so binary bodies
250
+ # (media results) aren't UTF-8-mangled by the text fallback.
251
+ return resp if raw else _parse_body(resp)
237
252
  if isinstance(last_exc, httpx.TransportError):
238
253
  raise NetworkError(str(last_exc), code="network_error") from last_exc
239
254
  raise last_exc if last_exc else CommonComputeError("request failed", status_code=None, code="request_failed")
@@ -431,7 +431,7 @@ def keys_revoke(key_id: str) -> None:
431
431
  @app.command()
432
432
  def chat(
433
433
  prompt: str = typer.Argument(..., help="Your message."),
434
- model: str = typer.Option("qwen-2.5-7b-instruct", "--model", "-m"),
434
+ model: str = typer.Option("qwen-2.5-7b", "--model", "-m"),
435
435
  stream: bool = typer.Option(True, "--stream/--no-stream"),
436
436
  ) -> None:
437
437
  """Send a one-shot chat message to a model."""
@@ -439,7 +439,14 @@ def chat(
439
439
  try:
440
440
  if stream:
441
441
  for chunk in c.chat.stream(model=model, messages=[{"role": "user", "content": prompt}]):
442
- delta = chunk.get("choices", [{}])[0].get("delta", {}).get("content", "") or ""
442
+ choice = chunk.get("choices", [{}])[0]
443
+ # delta-shaped per the OpenAI stream contract; fall back to a
444
+ # message-shaped chunk (older deployments sent those).
445
+ delta = (
446
+ choice.get("delta", {}).get("content", "")
447
+ or choice.get("message", {}).get("content", "")
448
+ or ""
449
+ )
443
450
  sys.stdout.write(delta)
444
451
  sys.stdout.flush()
445
452
  sys.stdout.write("\n")
@@ -14,7 +14,7 @@ from pathlib import Path
14
14
  from typing import Any, AsyncIterator, BinaryIO, Iterator, Mapping, Optional, Sequence, Union
15
15
 
16
16
  from ._transport import AsyncTransport, SyncTransport
17
- from .errors import JobTimeoutError, NotFoundError
17
+ from .errors import CommonComputeError, JobTimeoutError, NotFoundError
18
18
  from .models import Job, Quote, Receipt, TaskResult
19
19
  from .tasks import (
20
20
  OCR,
@@ -114,19 +114,36 @@ class _JobsSync:
114
114
 
115
115
  def download(self, task_id: str, *, dest: Optional[Union[str, Path]] = None) -> Union[bytes, Path]:
116
116
  """Download the job result. Returns bytes, or the path if ``dest`` is given."""
117
- data = self._t.request("GET", f"/v1/jobs/download/{task_id}")
118
- if isinstance(data, dict):
119
- # The API may return a pre-signed URL instead of inlined bytes.
120
- url = data.get("url") or data.get("download_url")
121
- if url:
117
+ # Raw-bytes fetch of GET /v1/jobs/:id/result: the parsed-transport
118
+ # path UTF-8-mangled binary media outputs, and a 202 "Result not
119
+ # ready" body was silently saved to disk as the result.
120
+ resp = self._t.request("GET", f"/v1/jobs/{task_id}/result", raw=True)
121
+ if resp.status_code == 202:
122
+ state = "unknown"
123
+ try:
124
+ state = resp.json().get("state", "unknown")
125
+ except Exception:
126
+ pass
127
+ raise CommonComputeError(
128
+ f"Result not ready for job {task_id} (state={state}) — wait() for completion first.",
129
+ status_code=202,
130
+ code="result_not_ready",
131
+ )
132
+ content_type = resp.headers.get("content-type", "")
133
+ blob: bytes
134
+ if "application/json" in content_type:
135
+ try:
136
+ data = resp.json()
137
+ except ValueError:
138
+ data = None
139
+ if isinstance(data, dict) and (data.get("url") or data.get("download_url")):
140
+ # The API may return a pre-signed URL instead of inlined bytes.
122
141
  import httpx as _httpx
123
- blob = _httpx.get(url, timeout=60.0).content
142
+ blob = _httpx.get(data.get("url") or data.get("download_url"), timeout=60.0).content
124
143
  else:
125
- blob = str(data).encode()
126
- elif isinstance(data, (bytes, bytearray)):
127
- blob = bytes(data)
144
+ blob = resp.content
128
145
  else:
129
- blob = str(data).encode()
146
+ blob = resp.content
130
147
  if dest is None:
131
148
  return blob
132
149
  dest_path = Path(dest)
File without changes
@@ -230,7 +230,7 @@ class Images(_TaskBase):
230
230
  self,
231
231
  prompt: str,
232
232
  *,
233
- model: str = "flux-schnell",
233
+ model: Optional[str] = None,
234
234
  n: int = 1,
235
235
  size: Optional[str] = None,
236
236
  max_spend_usd: Optional[float] = None,
@@ -239,7 +239,12 @@ class Images(_TaskBase):
239
239
  payload: dict = {"prompt": prompt, "n": n}
240
240
  if size:
241
241
  payload["size"] = size
242
- return self._run("mlx_image", payload, units=n, max_spend_usd=max_spend_usd, dry_run=dry_run)
242
+ # model was previously accepted and silently dropped (and its old
243
+ # default "flux-schnell" belongs to sd_mlx, not mlx_image) — forward
244
+ # it so the server can route/validate it.
245
+ return self._run(
246
+ "mlx_image", payload, units=n, model_id=model, max_spend_usd=max_spend_usd, dry_run=dry_run
247
+ )
243
248
 
244
249
 
245
250
  class Video(_TaskBase):
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "commoncompute"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Official Python SDK for Common Compute — the batch AI bill you shouldn't be paying."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes