diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e65fdf1..023fe8e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,18 +1,22 @@ -name: Validate Formatting & Types +name: Formatting, Types, & Test on: [push, pull_request] jobs: - validate: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 + validate: + runs-on: ubuntu-latest - - uses: denoland/setup-deno@v1 - with: - deno-version: v1.x + steps: + - uses: actions/checkout@v3 - - run: deno fmt --check + - uses: denoland/setup-deno@v1 + with: + deno-version: v1.x - - run: deno check prodia.ts + - run: deno fmt --check + + - run: deno check prodia.ts + + - run: deno test --allow-env --allow-net + env: + PRODIA_TOKEN: ${{ secrets.PRODIA_TOKEN }} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..3928a55 --- /dev/null +++ b/deno.lock @@ -0,0 +1,29 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@std/assert": "jsr:@std/assert@0.225.3", + "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.4" + }, + "jsr": { + "@std/assert@0.225.3": { + "integrity": "b3c2847aecf6955b50644cdb9cf072004ea3d1998dd7579fc0acb99dbb23bd4f", + "dependencies": [ + "jsr:@std/internal@^1.0.0" + ] + }, + "@std/internal@1.0.4": { + "integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422" + } + } + }, + "remote": {}, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:prettier@^3.0.0", + "npm:typescript@^5.1.6" + ] + } + } +} diff --git a/readme.md b/readme.md index 5ac9ae2..69463a9 100644 --- a/readme.md +++ b/readme.md @@ -5,8 +5,8 @@ Official TypeScript library for Prodia's AI inference API. -- [Get an API Key](https://app.prodia.com/api) - +- [Get an v1 API Key or v2 Token](https://app.prodia.com/api) +- [v2 API Explorer](https://app.prodia.com/explorer) - [View Docs + Pricing](https://docs.prodia.com/reference/getting-started) ## Usage @@ -15,6 +15,34 @@ Official TypeScript library for Prodia's AI inference API. npm install prodia --save ``` +## v2 + +As of _October 2024_, we require users to have a **Pro+** or **Enterprise** subscription with us to use our v2 API. This is to ensure quality of service. However, we expect to revisit this by EOY and make it available more broadly. + +```javascript +import { createProdia } from "prodia/v2"; // v2 :) + +const prodia = createProdia({ + token: "...", // grab a token from https://app.prodia.com/api +}); + +(async () => { + // run a flux dev generation + const job = await client.job({ + "type": "inference.flux.dev.txt2img.v1", + "config": { + "prompt": "puppies in a cloud, 4k", + "steps": 25, + }, + }); + + const image = await job.arrayBuffer(); + // display your image +})(); +``` + +## v1 Legacy API + ```javascript import { createProdia } from "prodia"; @@ -32,3 +60,7 @@ const prodia = createProdia({ // check status and view your image :) })(); ``` + +## help + +Email us at [hello@prodia.com](mailto:hello@prodia.com). diff --git a/test/v2.test.ts b/test/v2.test.ts new file mode 100644 index 0000000..4fe76d3 --- /dev/null +++ b/test/v2.test.ts @@ -0,0 +1,34 @@ +import { assertEquals } from "jsr:@std/assert"; +import { createProdia } from "../v2/index.ts"; + +const token = Deno.env.get("PRODIA_TOKEN"); + +if (typeof token !== "string") { + throw new Error("PRODIA_TOKEN is not set"); +} + +const isJpeg = (image: ArrayBuffer): boolean => { + const view = new Uint8Array(image); + + return view[0] === 0xFF && view[1] === 0xD8; +}; + +await Deno.test("Example Job: JPEG Output", async () => { + const client = createProdia({ + token, + }); + + const job = await client.job({ + "type": "inference.flux.dev.txt2img.v1", + "config": { + "prompt": "puppies in a cloud, 4k", + "steps": 1, + "width": 1024, + "height": 1024, + }, + }); + + const image = await job.arrayBuffer(); + + assertEquals(isJpeg(image), true, "Image should be a JPEG"); +}); diff --git a/tsconfig.json b/tsconfig.json index beaf29e..d530c5f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "baseUrl": ".", "declaration": true }, - "files": ["prodia.ts"] + "files": ["prodia.ts", "v2/index.ts"] } diff --git a/v2/index.ts b/v2/index.ts new file mode 100644 index 0000000..ffcc996 --- /dev/null +++ b/v2/index.ts @@ -0,0 +1,117 @@ +type JsonObject = + & { [Key in string]: JsonValue } + & { [Key in string]?: JsonValue | undefined }; +type JsonArray = JsonValue[] | readonly JsonValue[]; +type JsonPrimitive = string | number | boolean | null; +type JsonValue = JsonPrimitive | JsonObject | JsonArray; + +/* job and job configuration */ + +export type ProdiaJob = Record; + +export type ProdiaJobOptions = { + accept: + | "image/png" + | "image/jpeg" + | "image/webp" + | "multipart/form-data" + | "video/mp4"; +}; + +const defaultJobOptions: ProdiaJobOptions = { + accept: "image/jpeg", +}; + +export type ProdiaJobResponse = { + arrayBuffer: () => Promise; // we only support direct image response now +}; + +/* client & client configuration*/ + +export type Prodia = { + job: ( + params: ProdiaJob, + options?: Partial, + ) => Promise; +}; + +export type CreateProdiaOptions = { + token: string; + baseUrl?: string; + maxErrors?: number; + maxRetries?: number; +}; + +/* error types */ + +export class ProdiaCapacityError extends Error {} +export class ProdiaBadResponseError extends Error {} + +export const createProdia = ({ + token, + baseUrl = "https://inference.prodia.com/v2", + maxErrors = 1, + maxRetries = Infinity, +}: CreateProdiaOptions): Prodia => { + const job = async ( + params: ProdiaJob, + _options?: Partial, + ) => { + const options = { + ...defaultJobOptions, + ..._options, + }; + + let response: Response; + + let errors = 0; + let retries = 0; + + do { + response = await fetch(`${baseUrl}/job`, { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Accept": options.accept, + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + + if (response.status === 429) { + retries += 1; + } else if (response.status < 200 || response.status > 299) { + errors += 1; + } + + const retryAfter = Number(response.headers.get("Retry-After")) || 1; + await new Promise((resolve) => + setTimeout(resolve, retryAfter * 1000) + ); + } while ( + (response.status < 200 || response.status > 299) && + errors <= maxErrors && + retries <= maxRetries + ); + + if (response.status === 429) { + throw new ProdiaCapacityError( + "ProdiaCapacityError: Unable to schedule job with current token", + ); + } + + if (response.status < 200 || response.status > 299) { + throw new ProdiaBadResponseError( + `ProdiaBadResponseError: ${response.status} ${response.statusText}`, + ); + } + + return { + arrayBuffer: () => response.arrayBuffer(), + }; + }; + + return { + job, + }; +};