craterview 0.4.22 → 0.4.24

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.
Files changed (3) hide show
  1. package/README.md +15 -11
  2. package/index.ts +49 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -144,7 +144,7 @@ the same job.
144
144
  ## Errors
145
145
 
146
146
  Every answer the API or storage gives that is not a success is thrown as a
147
- `CraterViewError`. `.status` carries the HTTP status where there was one, and `.traceId` the
147
+ `CraterViewError`. `.status` carries the HTTP status where there was one, `.code` the API's stable name for a refusal — branch on it, not on the message — and `.traceId` the
148
148
  API's `X-Trace-Id` for a request it answered and refused — the id to quote when you ask us
149
149
  about it. `.jobId` names the job an error is about, where there is one. A request that got
150
150
  no answer at all throws the runtime's own error from `fetch`, which is how the retry loop
@@ -152,7 +152,7 @@ above tells the two apart.
152
152
 
153
153
  | Class | Meaning |
154
154
  |---|---|
155
- | `RateLimited` | 429 — one of five limits, and the message says which: the key's request rate, the account's upload URLs a minute, the account's new keys an hour, the account's jobs in flight, or a full queue. `.retryAfter` is seconds to wait: until the window rolls over for the two per-minute limits, until a new key can be made for the hourly one, or an interval to poll on for the other two. |
155
+ | `RateLimited` | 429 — one of five limits, and `.code` says which: `rate_limited` (the key's request rate), `too_many_uploads`, `too_many_keys`, `too_many_jobs` or `queue_full`. `.retryAfter` is seconds to wait: until the window rolls over for the two per-minute limits, until a new key can be made for the hourly one, or an interval to poll on for the other two. |
156
156
  | `JobFailed` | The job ran and did not succeed. `.message` says what you can do about it; `.errorCode` is the half to branch on; `.jobId` is the job. |
157
157
  | `InvalidSignature` | A webhook delivery did not verify. Do not act on its body. |
158
158
  | `CraterViewError` | Everything else, including 4xx and 5xx from the API, and storage refusing an upload or a download. |
@@ -172,7 +172,7 @@ Every field the API publishes on a job is exposed here.
172
172
  | `succeeded`, `done` | `done` covers both terminal states |
173
173
  | `error` | Set when the job failed. Safe to show a user |
174
174
  | `errorCode` | The same fact, as a stable identifier. Branch on this, show the other |
175
- | `credits` | **What you were billed** |
175
+ | `credits` | **What you were billed.** Nothing past your balance is collected |
176
176
  | `etaSeconds` | Seconds until the job is expected to finish, recomputed on every read — it counts down while the job runs. Absent once the job has settled. Estimated for your image's size when `upload()` could read it (or when you pass `inputMegapixels` to `submit()`), for a typical image otherwise |
177
177
  | `community` | True when the job is on the community queue, which runs on shared, free capacity and can wait longer at busy times |
178
178
  | `outputUrl`, `downloadUrl` | The result, presigned. One to display, one to save |
@@ -199,9 +199,9 @@ in hand, which is what makes it impossible to get wrong.
199
199
  **Running out of credit does not stop you.** A job submitted against a balance of zero is
200
200
  accepted and run, and charged when it succeeds — it goes to the community queue, which runs
201
201
  on shared, free capacity and can wait longer at busy times. It comes back with `community`
202
- set. There is no payment error to handle: paying — with credit, or with a subscription —
203
- moves work onto paid compute that scales with demand, rather than buying the right to
204
- submit.
202
+ set, and `usage()` reports the same flag for your next job before you send it. There is no
203
+ payment error to handle: paying — with credit, or with a subscription — moves work onto paid
204
+ compute that scales with demand, rather than buying the right to submit.
205
205
  `etaSeconds` covers the whole wait, queue time included, so a community job simply reports
206
206
  a longer one.
207
207
 
@@ -213,15 +213,19 @@ result rather than storing the link.
213
213
  page of jobs costs kilobytes, and the picture the model worked from so a result can be
214
214
  shown against it. Where you named a region, `inputUrl` is that region — so a before-and-after
215
215
  is a true pair, and what was used is something you can look at rather than something to take
216
- on trust. It is not the file you uploaded: yours stays yours and is removed on its own
217
- schedule. Both expire with the job's other pictures. A model that produces no file has no
218
- result to preview, so `thumbnailUrl` is a preview of the picture it answered about.
216
+ on trust. It is not the file you uploaded but a new one written from it, not copied —
217
+ so do not expect a hash of it to match your file's, and a JPEG or WebP is compressed
218
+ again. Yours is removed on its own schedule. Both expire with the job's other pictures. A
219
+ model that produces no file has no result to preview, so `thumbnailUrl` is a preview of the
220
+ picture it answered about.
219
221
 
220
222
  ## Webhooks
221
223
 
222
224
  Pass `webhookUrl` on submit — an absolute `http` or `https` URL, refused at submit if it is
223
225
  not one — and we `POST` to it once the job settles. Verify every delivery before you act on
224
- it: anyone who learns your URL can send you a plausible-looking body.
226
+ it: anyone who learns your URL can send you a plausible-looking body. Retries stop within a
227
+ minute; to find a delivery that never arrived, keep the ids of jobs you submitted and have not
228
+ heard about, and read each with `cv.job(id)`.
225
229
 
226
230
  ```ts
227
231
  import { CraterView, verifyWebhook, InvalidSignature } from "craterview";
@@ -293,7 +297,7 @@ old one. You cannot revoke the key you are calling with.
293
297
  ```ts
294
298
  await cv.models(); // models, what each takes and answers with, which queue you are on
295
299
  await cv.job("job_..."); // one job by id
296
- await cv.usage(); // credit balance, spend and job counts
300
+ await cv.usage(); // credit balance, next job's queue, spend and job counts
297
301
 
298
302
  for await (const job of cv.jobs({ limit: 50, status: "succeeded" })) {
299
303
  console.log(job.id, job.status); // newest first, paging transparently
package/index.ts CHANGED
@@ -18,7 +18,7 @@ import { imageSize } from "image-size";
18
18
  // Mirrored from package.json, which is the number a release bumps. It cannot be imported
19
19
  // from there — this ships as TypeScript, so the import would have to resolve in the
20
20
  // consumer's toolchain — so test/version.test.ts asserts the two agree.
21
- export const VERSION = "0.4.22";
21
+ export const VERSION = "0.4.24";
22
22
  const DEFAULT_BASE_URL = "https://api.craterview.ai";
23
23
  // The server rejects a longer wait outright, so asking for one costs a 422 rather than the
24
24
  // wait you asked for. `run()` clamps to this rather than letting that happen.
@@ -131,7 +131,9 @@ function queuedNothing(e: unknown): boolean {
131
131
  }
132
132
 
133
133
  /**
134
- * `status` is the HTTP status where there was one. `traceId` is the API's `X-Trace-Id` for a
134
+ * `status` is the HTTP status where there was one, and `code` the API's stable name for a
135
+ * refusal: branch on `code`, not on the message, and handle one you do not know by its status.
136
+ * `traceId` is the API's `X-Trace-Id` for a
135
137
  * request it answered and refused — the id to quote when asking about it. Absent when the
136
138
  * refusal came from storage rather than the API.
137
139
  *
@@ -141,15 +143,16 @@ function queuedNothing(e: unknown): boolean {
141
143
  */
142
144
  export class CraterViewError extends Error {
143
145
  constructor(message: string, readonly status?: number, readonly traceId?: string,
144
- readonly jobId?: string) {
146
+ readonly jobId?: string, readonly code?: string) {
145
147
  super(message);
146
148
  this.name = "CraterViewError";
147
149
  }
148
150
  }
149
151
 
150
152
  /**
151
- * 429 — too fast, or too much at once. Five different limits answer with this, and the
152
- * message says which:
153
+ * 429 — too fast, or too much at once. Five different limits answer with this, and `code`
154
+ * says which — `rate_limited`, `too_many_keys`, `too_many_uploads`, `too_many_jobs` or
155
+ * `queue_full`, in this order:
153
156
  *
154
157
  * - the key's request rate, per minute;
155
158
  * - the account's new keys, per hour — counted across all its keys, and only from
@@ -167,8 +170,8 @@ export class CraterViewError extends Error {
167
170
  * on, since nothing here predicts when a slot frees.
168
171
  */
169
172
  export class RateLimited extends CraterViewError {
170
- constructor(message: string, readonly retryAfter?: number, traceId?: string) {
171
- super(message, 429, traceId);
173
+ constructor(message: string, readonly retryAfter?: number, traceId?: string, code?: string) {
174
+ super(message, 429, traceId, undefined, code);
172
175
  this.name = "RateLimited";
173
176
  }
174
177
  }
@@ -412,6 +415,19 @@ export interface ModelInfo {
412
415
  * `denoise`. Zero means this axis is unbounded for you.
413
416
  */
414
417
  max_denoise_input_megapixels?: number | null;
418
+ /**
419
+ * The most of a picture this model works on in one job with `tiled` sent as true, in
420
+ * megapixels: the region you name with `roi`, or the whole image, worked on at its own
421
+ * resolution and returned at its own size. Without `tiled`, a larger picture is reduced
422
+ * rather than refused: see `working_megapixels`. Null for a model that takes no `tiled`.
423
+ */
424
+ max_tiled_input_megapixels?: number | null;
425
+ /**
426
+ * The most this model works on at a picture's own resolution, in megapixels: a larger
427
+ * picture or region is reduced to about this area first and comes back at about this size,
428
+ * unless `tiled` is sent as true. Null for a model that works at the size it is given.
429
+ */
430
+ working_megapixels?: number | null;
415
431
  /** Whole credits. The price is flat and knowable before you send anything. */
416
432
  credits_per_image: number;
417
433
  /** Whole credits. Null means this model takes stills only. */
@@ -559,6 +575,8 @@ export interface JobData {
559
575
  * A submitted job. Mirrors the API's job representation field for field.
560
576
  *
561
577
  * `credits` is what you were billed, and it is the only figure about cost the API states.
578
+ * What your balance does not cover is never collected: it is written off when credit next
579
+ * arrives or a subscription renews.
562
580
  * For how long a job took, `finishedAt` minus `createdAt` is the wait you actually had,
563
581
  * queue included — both are here and both are on the webhook, which states the same fields
564
582
  * by the same names.
@@ -585,9 +603,10 @@ export interface JobData {
585
603
  *
586
604
  * `community` says the job is on the community queue, which runs on shared, free capacity —
587
605
  * so it waits longer at busy times, and never stops. That is where an account's work goes
588
- * when it is not paying, by holding credit or by subscribing. An empty balance is never itself
589
- * a reason to refuse a job — paying moves work onto paid compute that scales with demand, not
590
- * the right to submit — but on some models the community queue takes smaller files.
606
+ * when it is not paying: no credit, and no subscription with work left this period. An empty
607
+ * balance is never itself a reason to refuse a job — paying moves work onto paid compute that
608
+ * scales with demand, not the right to submit — but on some models the community queue takes
609
+ * smaller files.
591
610
  * `models()` lists each model's limits for your key, and a file over them is refused with
592
611
  * the limit named.
593
612
  */
@@ -642,8 +661,9 @@ export class Job {
642
661
  /**
643
662
  * A link to the picture the model worked from. Where you named a region, this is that
644
663
  * region — so what was used is something you can look at rather than something to take on
645
- * trust. It is not the file you uploaded: yours stays yours and is removed on its own
646
- * schedule.
664
+ * trust. It is not the file you uploaded but a new one written from it, not copied — so do
665
+ * not expect a hash of it to match your file's, and a JPEG or WebP is compressed again.
666
+ * Yours is removed on its own schedule.
647
667
  *
648
668
  * Null for a job that has not finished. A model that answers about a picture rather than
649
669
  * producing one has it too: it is the picture that answer is about.
@@ -823,11 +843,12 @@ export class CraterView {
823
843
 
824
844
  const traceId = resp.headers.get("X-Trace-Id") ?? undefined;
825
845
  if (!resp.ok) {
826
- const detail = await this.detail(resp);
846
+ const { detail, code } = await this.refusal(resp);
827
847
  if (resp.status === 429) {
828
- throw new RateLimited(detail, Number(resp.headers.get("Retry-After") ?? 0), traceId);
848
+ throw new RateLimited(detail, Number(resp.headers.get("Retry-After") ?? 0), traceId,
849
+ code);
829
850
  }
830
- throw new CraterViewError(detail, resp.status, traceId);
851
+ throw new CraterViewError(detail, resp.status, traceId, undefined, code);
831
852
  }
832
853
  // A 204 carries no body, so asking for JSON throws on a call that succeeded.
833
854
  if (resp.status === 204) return undefined as T;
@@ -838,15 +859,18 @@ export class CraterView {
838
859
  }
839
860
  }
840
861
 
841
- /** The API's own words for a refusal, or a sentence saying the answer was not its. */
842
- private async detail(resp: Response): Promise<string> {
862
+ /** The API's own words for a refusal and its code, or a sentence saying the answer was not
863
+ * its. One read of the body, which can be read once. */
864
+ private async refusal(resp: Response): Promise<{ detail: string; code?: string }> {
843
865
  let body: unknown;
844
866
  try {
845
867
  body = await resp.json();
846
868
  } catch {
847
- return this.notTheApi(resp.status);
869
+ return { detail: this.notTheApi(resp.status) };
848
870
  }
849
- return spoken((body as { detail?: unknown } | null)?.detail) ?? this.notTheApi(resp.status);
871
+ const said = body as { detail?: unknown; code?: unknown } | null;
872
+ return { detail: spoken(said?.detail) ?? this.notTheApi(resp.status),
873
+ code: typeof said?.code === "string" ? said.code : undefined };
850
874
  }
851
875
 
852
876
  /**
@@ -973,13 +997,12 @@ export class CraterView {
973
997
  * until the history runs out. 200 is the largest page the API serves, and a bigger number
974
998
  * is fetched as 200 rather than sent and refused.
975
999
  */
976
- async *jobs(options: { limit?: number; status?: JobStatus } = {}): AsyncGenerator<Job> {
1000
+ async *jobs(options: { limit?: number } = {}): AsyncGenerator<Job> {
977
1001
  let before: string | undefined;
978
1002
  for (;;) {
979
1003
  const params = new URLSearchParams({
980
1004
  limit: String(Math.min(options.limit ?? 50, MAX_PAGE)),
981
1005
  });
982
- if (options.status) params.set("status", options.status);
983
1006
  if (before) params.set("before", before);
984
1007
 
985
1008
  const page = await this.request<{ data: JobData[]; has_more: boolean; next_before?: string }>(
@@ -1000,7 +1023,9 @@ export class CraterView {
1000
1023
  * charged when it succeeds, so queued and running work is not in the figures yet, and a
1001
1024
  * failed job costs nothing.
1002
1025
  *
1003
- * `credits_remaining` is always a number, floored at zero. `jobs_in_flight` and
1026
+ * `credits_remaining` is always a number, floored at zero. A balance of zero never stops a
1027
+ * submission: `community` says whether your next job runs on the community queue, which is
1028
+ * free and can wait longer at busy times, or on paid compute. `jobs_in_flight` and
1004
1029
  * `max_jobs_in_flight` are the state of the queue and are what a 429 on submit is about.
1005
1030
  *
1006
1031
  * Account-wide, so every key on an account reports the same figures.
@@ -1010,7 +1035,8 @@ export class CraterView {
1010
1035
  }
1011
1036
 
1012
1037
  /**
1013
- * The secret your callbacks are signed with, created the first time you ask.
1038
+ * The secret your callbacks are signed with, created the first time you ask or with your
1039
+ * first delivery, whichever comes first.
1014
1040
  *
1015
1041
  * Pass it to {@link verifyWebhook}. Unlike an API key this can be read back whenever you
1016
1042
  * need it — but treat it as a credential all the same: anyone holding it can produce a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craterview",
3
- "version": "0.4.22",
3
+ "version": "0.4.24",
4
4
  "dependencies": {
5
5
  "image-size": "^2.0.4"
6
6
  },