From 9b4bb64227633c5d8aaa864714229987694cea61 Mon Sep 17 00:00:00 2001 From: Mattt Date: Mon, 10 Jun 2024 03:55:14 -0700 Subject: [PATCH 01/68] Delete explicit `Content-Type` for `FormData` input (#268) * Delete explicit Content-Type for FormData input * Update tests --- index.js | 9 ++++++--- index.test.ts | 6 +----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 70cbe862..bf53449a 100644 --- a/index.js +++ b/index.js @@ -220,12 +220,13 @@ class Replicate { url.searchParams.append(key, value); } - const headers = {}; + const headers = { + "Content-Type": "application/json", + "User-Agent": userAgent, + }; if (auth) { headers["Authorization"] = `Bearer ${auth}`; } - headers["Content-Type"] = "application/json"; - headers["User-Agent"] = userAgent; if (options.headers) { for (const [key, value] of Object.entries(options.headers)) { headers[key] = value; @@ -235,6 +236,8 @@ class Replicate { let body = undefined; if (data instanceof FormData) { body = data; + // biome-ignore lint/performance/noDelete: + delete headers["Content-Type"]; // Use automatic content type header } else if (data) { body = JSON.stringify(data); } diff --git a/index.test.ts b/index.test.ts index 834b786f..8ecb5ae2 100644 --- a/index.test.ts +++ b/index.test.ts @@ -296,7 +296,6 @@ describe("Replicate client", () => { nock(BASE_URL) .post("/files") - .matchHeader("Content-Type", "multipart/form-data") .reply(201, { urls: { get: "https://replicate.com/api/files/123", @@ -317,7 +316,6 @@ describe("Replicate client", () => { prompt: "Tell me a story", data, }, - stream: true, }); expect(client.fetch).toHaveBeenCalledWith( @@ -325,9 +323,7 @@ describe("Replicate client", () => { { method: "POST", body: expect.any(FormData), - headers: expect.objectContaining({ - "Content-Type": "multipart/form-data", - }), + headers: expect.any(Object), } ); const form = mockedFetch.mock.calls[0][1]?.body as FormData; From a345d0d721e654e180dfe898fc95e094d324d3f5 Mon Sep 17 00:00:00 2001 From: Mattt Date: Mon, 10 Jun 2024 04:05:17 -0700 Subject: [PATCH 02/68] Update blob creation to handle Buffer inputs correctly in createFile (#269) --- lib/files.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/files.js b/lib/files.js index f6620e92..520f2aa2 100644 --- a/lib/files.js +++ b/lib/files.js @@ -15,7 +15,11 @@ async function createFile(file, metadata = {}) { blob = file; } else if (Buffer.isBuffer(file)) { filename = `buffer_${Date.now()}`; - blob = new Blob(file, { type: "application/octet-stream" }); + const bytes = new Uint8Array(file); + blob = new Blob([bytes], { + type: "application/octet-stream", + name: filename, + }); } else { throw new Error("Invalid file argument, must be a Blob, File or Buffer"); } From 4689e2bed0e44b1a099cd82f0c392a355b6c2fae Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Mon, 10 Jun 2024 04:05:37 -0700 Subject: [PATCH 03/68] 0.30.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f268ac61..710a1e9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "0.30.1", + "version": "0.30.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "0.30.1", + "version": "0.30.2", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index 502ee210..f31fb229 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "0.30.1", + "version": "0.30.2", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From fa38a54e08220ab1e4f835c318c9e142a4cd9439 Mon Sep 17 00:00:00 2001 From: Mattt Date: Tue, 11 Jun 2024 05:44:06 -0700 Subject: [PATCH 04/68] Revert "add PR template (#263)" (#271) This reverts commit 87bd3ab3e9df312a193a18787c35401956035bdd. --- .github/pull_request_template.md | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 58baaeca..00000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,4 +0,0 @@ - - -- [ ] Tests -- [ ] Docs \ No newline at end of file From 9b5df12c0ad71efaeaacd40f1387f204e70cefb3 Mon Sep 17 00:00:00 2001 From: Mattt Date: Tue, 2 Jul 2024 09:32:31 -0700 Subject: [PATCH 05/68] Document file operations (#276) * Reword file input recommendations * Document file methods * Document fileEncodingStrategy client constructor option --- README.md | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 29c31c6f..797af77c 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,9 @@ console.log(prediction.output); // ['https://replicate.delivery/pbxt/RoaxeXqhL0xaYyLm6w3bpGwF5RaNBjADukfFnMbhOyeoWBdhA/out-0.png'] ``` -To run a model that takes a file input you can pass the data directly or pass a URL to a publicly accessible file. +To run a model that takes a file input you can pass either +a URL to a publicly accessible file on the Internet +or a handle to a file on your local device. ```js const fs = require("node:fs/promises"); @@ -93,6 +95,10 @@ const output = await replicate.run(model, { input }); // ['https://replicate.delivery/mgxm/e7b0e122-9daa-410e-8cde-006c7308ff4d/output.png'] ``` +> [!NOTE] +> File handle inputs are automatically uploaded to Replicate. +> See [`replicate.files.create`](#replicatefilescreate) for more information. + ### Webhooks Webhooks provide real-time updates about your prediction. Specify an endpoint when you create a prediction, and Replicate will send HTTP POST requests to that URL when the prediction is created, updated, and finished. @@ -179,7 +185,7 @@ export async function POST(request) { console.log(body); return NextResponse.json({ detail: "Webhook received (but not validated)" }, { status: 200 }); } - + const webhookIsValid = await validateWebhook(request.clone(), secret); if (!webhookIsValid) { @@ -209,12 +215,14 @@ Currently in order to support the module format used by `replicate` you'll need const replicate = new Replicate(options); ``` -| name | type | description | -| ------------------- | -------- | --------------------------------------------------------------------------------- | -| `options.auth` | string | **Required**. API access token | -| `options.userAgent` | string | Identifier of your app. Defaults to `replicate-javascript/${packageJSON.version}` | -| `options.baseUrl` | string | Defaults to https://api.replicate.com/v1 | -| `options.fetch` | function | Fetch function to use. Defaults to `globalThis.fetch` | +| name | type | description | +| ------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `options.auth` | string | **Required**. API access token | +| `options.userAgent` | string | Identifier of your app. Defaults to `replicate-javascript/${packageJSON.version}` | +| `options.baseUrl` | string | Defaults to https://api.replicate.com/v1 | +| `options.fetch` | function | Fetch function to use. Defaults to `globalThis.fetch` | +| `options.fileEncodingStrategy` | string | Determines the file encoding strategy to use. Possible values: `"default"`, `"upload"`, or `"data-uri"`. Defaults to `"default"` | + The client makes requests to Replicate's API using [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch). @@ -983,6 +991,99 @@ const response = await replicate.deployments.update(deploymentOwner, deploymentN } ``` +### `replicate.files.create` + +Upload a file to Replicate. + +> [!TIP] +> The client library calls this endpoint automatically to upload the contents of +> file handles provided as prediction and training inputs. +> You don't need to call this method directly unless you want more control. +> For example, you might want to reuse a file across multiple predictions +> without re-uploading it each time, +> or you may want to set custom metadata on the file resource. +> +> You can configure how a client handles file handle inputs +> by setting the `fileEncodingStrategy` option in the +> [client constructor](#constructor). + +```js +const response = await replicate.files.create(file, metadata); +``` + +| name | type | description | +| ---------- | --------------------- | ---------------------------------------------------------- | +| `file` | Blob, File, or Buffer | **Required**. The file to upload. | +| `metadata` | object | Optional. User-provided metadata associated with the file. | + +```jsonc +{ + "id": "MTQzODcyMDct0YjZkLWE1ZGYtMmRjZTViNWIwOGEyNjNhNS0", + "name": "photo.webp", + "content_type": "image/webp", + "size": 96936, + "etag": "f211779ff7502705bbf42e9874a17ab3", + "checksums": { + "sha256": "7282eb6991fa4f38d80c312dc207d938c156d714c94681623aedac846488e7d3", + "md5": "f211779ff7502705bbf42e9874a17ab3" + }, + "metadata": { + "customer_reference_id": "123" + }, + "created_at": "2024-06-28T10:16:04.062Z", + "expires_at": "2024-06-29T10:16:04.062Z", + "urls": { + "get": "https://api.replicate.com/v1/files/MTQzODcyMDct0YjZkLWE1ZGYtMmRjZTViNWIwOGEyNjNhNS0" + } +} +``` + +Files uploaded to Replicate using this endpoint expire after 24 hours. + +Pass the `urls.get` property of a file resource +to use it as an input when running a model on Replicate. +The value of `urls.get` is opaque, +and shouldn't be inferred from other attributes. + +The contents of a file are only made accessible to a model running on Replicate, +and only when passed as a prediction or training input +by the user or organization who created the file. + +### `replicate.files.list` + +List all files you've uploaded. + +```js +const response = await replicate.files.list(); +``` + +### `replicate.files.get` + +Get metadata for a specific file. + +```js +const response = await replicate.files.get(file_id); +``` + +| name | type | description | +| --------- | ------ | --------------------------------- | +| `file_id` | string | **Required**. The ID of the file. | + +### `replicate.files.delete` + +Delete a file. + +Files uploaded using the `replicate.files.create` method expire after 24 hours. +You can use this method to delete them sooner. + +```js +const response = await replicate.files.delete(file_id); +``` + +| name | type | description | +| --------- | ------ | --------------------------------- | +| `file_id` | string | **Required**. The ID of the file. | + ### `replicate.paginate` Pass another method as an argument to iterate over results From d83117da4d338772ba16e97a5a7ba43cb127b44d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:33:04 -0700 Subject: [PATCH 06/68] Bump braces from 3.0.2 to 3.0.3 (#272) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 710a1e9c..6af93034 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1930,12 +1930,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2701,9 +2701,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" From a3adcb3fb53a892a8b225333da10533be0e464e1 Mon Sep 17 00:00:00 2001 From: Mattt Date: Wed, 3 Jul 2024 14:49:25 -0700 Subject: [PATCH 07/68] Add support for deployments.delete endpoint (#279) --- index.d.ts | 4 ++++ index.js | 1 + index.test.ts | 14 ++++++++++++++ lib/deployments.js | 19 +++++++++++++++++++ 4 files changed, 38 insertions(+) diff --git a/index.d.ts b/index.d.ts index 1ef9e89c..e6fd2d9c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -236,6 +236,10 @@ declare module "replicate" { | { max_instances: number } ) ): Promise; + delete( + deployment_owner: string, + deployment_name: string + ): Promise; list(): Promise>; }; diff --git a/index.js b/index.js index bf53449a..b92503ec 100644 --- a/index.js +++ b/index.js @@ -71,6 +71,7 @@ class Replicate { get: deployments.get.bind(this), create: deployments.create.bind(this), update: deployments.update.bind(this), + delete: deployments.delete.bind(this), list: deployments.list.bind(this), predictions: { create: deployments.predictions.create.bind(this), diff --git a/index.test.ts b/index.test.ts index 8ecb5ae2..9e22cbe0 100644 --- a/index.test.ts +++ b/index.test.ts @@ -999,6 +999,20 @@ describe("Replicate client", () => { // Add more tests for error handling, edge cases, etc. }); + describe("deployments.delete", () => { + test("Calls the correct API route with the correct payload", async () => { + nock(BASE_URL) + .delete("/deployments/acme/my-app-image-generator") + .reply(204); + + const success = await client.deployments.delete( + "acme", + "my-app-image-generator" + ); + expect(success).toBe(true); + }); + }); + describe("deployments.list", () => { test("Calls the correct API route", async () => { nock(BASE_URL) diff --git a/lib/deployments.js b/lib/deployments.js index 27a2f6a2..56ed2406 100644 --- a/lib/deployments.js +++ b/lib/deployments.js @@ -118,6 +118,24 @@ async function updateDeployment( return response.json(); } +/** + * Delete a deployment + * + * @param {string} deployment_owner - Required. The username of the user or organization who owns the deployment + * @param {string} deployment_name - Required. The name of the deployment + * @returns {Promise} Resolves with true if the deployment was deleted + */ +async function deleteDeployment(deployment_owner, deployment_name) { + const response = await this.request( + `/deployments/${deployment_owner}/${deployment_name}`, + { + method: "DELETE", + } + ); + + return response.status === 204; +} + /** * List all deployments * @@ -139,4 +157,5 @@ module.exports = { create: createDeployment, update: updateDeployment, list: listDeployments, + delete: deleteDeployment, }; From 7f2bd81f83ff5b1ac217289684e524d2d0a999d6 Mon Sep 17 00:00:00 2001 From: Mattt Date: Wed, 3 Jul 2024 15:44:01 -0700 Subject: [PATCH 08/68] Export files methods to client (#278) * Export files methods to client * Add type definition for file operations * Add test coverage for file methods --- index.d.ts | 10 ++++ index.js | 8 +++ index.test.ts | 140 +++++++++++++++++++++++++++++++++++++++++--------- lib/files.js | 4 +- 4 files changed, 135 insertions(+), 27 deletions(-) diff --git a/index.d.ts b/index.d.ts index e6fd2d9c..7d4ef0a9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -243,6 +243,16 @@ declare module "replicate" { list(): Promise>; }; + files: { + create( + file: Blob | Buffer, + metadata?: Record + ): Promise; + list(): Promise>; + get(file_id: string): Promise; + delete(file_id: string): Promise; + }; + hardware: { list(): Promise; }; diff --git a/index.js b/index.js index b92503ec..21e83f95 100644 --- a/index.js +++ b/index.js @@ -11,6 +11,7 @@ const { const accounts = require("./lib/accounts"); const collections = require("./lib/collections"); const deployments = require("./lib/deployments"); +const files = require("./lib/files"); const hardware = require("./lib/hardware"); const models = require("./lib/models"); const predictions = require("./lib/predictions"); @@ -78,6 +79,13 @@ class Replicate { }, }; + this.files = { + create: files.create.bind(this), + get: files.get.bind(this), + list: files.list.bind(this), + delete: files.delete.bind(this), + }; + this.hardware = { list: hardware.list.bind(this), }; diff --git a/index.test.ts b/index.test.ts index 9e22cbe0..2645ca4d 100644 --- a/index.test.ts +++ b/index.test.ts @@ -15,6 +15,31 @@ const BASE_URL = "https://api.replicate.com/v1"; nock.disableNetConnect(); +const fileTestCases = [ + // Skip test case if File type is not available + ...(typeof File !== "undefined" + ? [ + { + type: "file", + value: new File(["hello world"], "file_hello.txt", { + type: "text/plain", + }), + expected: "data:text/plain;base64,aGVsbG8gd29ybGQ=", + }, + ] + : []), + { + type: "blob", + value: new Blob(["hello world"], { type: "text/plain" }), + expected: "data:text/plain;base64,aGVsbG8gd29ybGQ=", + }, + { + type: "buffer", + value: Buffer.from("hello world"), + expected: "data:application/octet-stream;base64,aGVsbG8gd29ybGQ=", + }, +]; + describe("Replicate client", () => { let unmatched: any[] = []; const handleNoMatch = (req: unknown, options: any, body: string) => @@ -264,31 +289,6 @@ describe("Replicate client", () => { } ); - const fileTestCases = [ - // Skip test case if File type is not available - ...(typeof File !== "undefined" - ? [ - { - type: "file", - value: new File(["hello world"], "file_hello.txt", { - type: "text/plain", - }), - expected: "data:text/plain;base64,aGVsbG8gd29ybGQ=", - }, - ] - : []), - { - type: "blob", - value: new Blob(["hello world"], { type: "text/plain" }), - expected: "data:text/plain;base64,aGVsbG8gd29ybGQ=", - }, - { - type: "buffer", - value: Buffer.from("hello world"), - expected: "data:application/octet-stream;base64,aGVsbG8gd29ybGQ=", - }, - ]; - test.each(fileTestCases)( "converts a $type input into a Replicate file URL", async ({ value: data, type }) => { @@ -1072,6 +1072,96 @@ describe("Replicate client", () => { // Add more tests for error handling, edge cases, etc. }); + describe("files.create", () => { + test("Calls the correct API route with the correct payload", async () => { + for (const testCase of fileTestCases) { + nock(BASE_URL) + .post("/files") + .reply(200, { + id: "123", + name: "test-file", + content_type: "application/octet-stream", + size: 1024, + etag: "abc123", + checksum: "sha256:1234567890abcdef", + metadata: {}, + created_at: "2023-01-01T00:00:00Z", + expires_at: null, + urls: { + get: "https://api.replicate.com/v1/files/123", + }, + }); + const file = await client.files.create(testCase.value); + expect(file.id).toBe("123"); + expect(file.name).toBe("test-file"); + } + }); + }); + + describe("files.get", () => { + test("Calls the correct API route", async () => { + nock(BASE_URL) + .get("/files/123") + .reply(200, { + id: "123", + name: "test-file", + content_type: "application/octet-stream", + size: 1024, + etag: "abc123", + checksum: "sha256:1234567890abcdef", + metadata: {}, + created_at: "2023-01-01T00:00:00Z", + expires_at: null, + urls: { + get: "https://api.replicate.com/v1/files/123", + }, + }); + + const file = await client.files.get("123"); + expect(file.id).toBe("123"); + expect(file.name).toBe("test-file"); + }); + }); + + describe("files.list", () => { + test("Calls the correct API route", async () => { + nock(BASE_URL) + .get("/files") + .reply(200, { + next: null, + previous: null, + results: [ + { + id: "123", + name: "test-file", + content_type: "application/octet-stream", + size: 1024, + etag: "abc123", + checksum: "sha256:1234567890abcdef", + metadata: {}, + created_at: "2023-01-01T00:00:00Z", + expires_at: null, + urls: { + get: "https://api.replicate.com/v1/files/123", + }, + }, + ], + }); + + const files = await client.files.list(); + expect(files.results.length).toBe(1); + expect(files.results[0].id).toBe("123"); + }); + }); + + describe("files.delete", () => { + test("Calls the correct API route", async () => { + nock(BASE_URL).delete("/files/123").reply(204); + const success = await client.files.delete("123"); + expect(success).toBe(true); + }); + }); + describe("hardware.list", () => { test("Calls the correct API route", async () => { nock(BASE_URL) diff --git a/lib/files.js b/lib/files.js index 520f2aa2..de49c580 100644 --- a/lib/files.js +++ b/lib/files.js @@ -72,14 +72,14 @@ async function getFile(file_id) { * Delete a file * * @param {string} file_id - Required. The ID of the file. - * @returns {Promise} - Resolves with the deletion confirmation + * @returns {Promise} - Resolves with true if the file was deleted */ async function deleteFile(file_id) { const response = await this.request(`/files/${file_id}`, { method: "DELETE", }); - return response.json(); + return response.status === 204; } module.exports = { From 91218056871de60f3bcc3c6d9a7a0b664d180102 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Wed, 3 Jul 2024 15:49:15 -0700 Subject: [PATCH 09/68] 0.31.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6af93034..c6906dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "0.30.2", + "version": "0.31.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "0.30.2", + "version": "0.31.0", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index f31fb229..9d9b60bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "0.30.2", + "version": "0.31.0", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From 7fec3af05e70a503abd440f72f066e1a6368b33a Mon Sep 17 00:00:00 2001 From: Mattt Date: Fri, 5 Jul 2024 04:35:02 -0700 Subject: [PATCH 10/68] Add integration test for Deno (#281) * Add integration test for Deno * Test local build of replicate --- .github/workflows/ci.yml | 28 ++++++++++- integration/deno/deno.json | 5 ++ integration/deno/deno.lock | 88 ++++++++++++++++++++++++++++++++++ integration/deno/index.test.ts | 26 ++++++++++ integration/deno/index.ts | 16 +++++++ 5 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 integration/deno/deno.json create mode 100644 integration/deno/deno.lock create mode 100644 integration/deno/index.test.ts create mode 100644 integration/deno/index.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0a90e49..87b1f75e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,7 +141,6 @@ jobs: npm --prefix integration/${{ matrix.suite }} install "./${{ needs.build.outputs.tarball-name }}" npm --prefix integration/${{ matrix.suite }} test - integration-bun: needs: [test, build] runs-on: ubuntu-latest @@ -172,6 +171,33 @@ jobs: bun test && break || echo "Test failed, retrying..." done + integration-deno: + needs: [test, build] + runs-on: ubuntu-latest + + env: + REPLICATE_API_TOKEN: ${{ secrets.REPLICATE_API_TOKEN }} + + strategy: + matrix: + deno-version: [v1.x] + suite: [deno] + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v3 + with: + name: package-tarball + - name: Use Deno ${{ matrix.deno-version }} + uses: denoland/setup-deno@v1 + with: + deno-version: ${{ matrix.deno-version }} + - run: | + cd integration/deno + deno cache --node-modules-dir index.ts + tar -xzf ../../${{ needs.build.outputs.tarball-name }} --strip-components=1 -C node_modules/replicate + deno test --allow-env --allow-net --node-modules-dir index.test.ts + integration-nextjs: needs: [test, build] runs-on: ubuntu-latest diff --git a/integration/deno/deno.json b/integration/deno/deno.json new file mode 100644 index 00000000..6b3e0a05 --- /dev/null +++ b/integration/deno/deno.json @@ -0,0 +1,5 @@ +{ + "imports": { + "replicate": "npm:replicate" + } +} diff --git a/integration/deno/deno.lock b/integration/deno/deno.lock new file mode 100644 index 00000000..057d6122 --- /dev/null +++ b/integration/deno/deno.lock @@ -0,0 +1,88 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@std/assert": "jsr:@std/assert@0.226.0", + "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.1", + "npm:replicate": "npm:replicate@0.31.0" + }, + "jsr": { + "@std/assert@0.226.0": { + "integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3", + "dependencies": [ + "jsr:@std/internal@^1.0.0" + ] + }, + "@std/internal@1.0.1": { + "integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6" + } + }, + "npm": { + "abort-controller@3.0.0": { + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "event-target-shim@5.0.1" + } + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dependencies": {} + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": { + "base64-js": "base64-js@1.5.1", + "ieee754": "ieee754@1.2.1" + } + }, + "event-target-shim@5.0.1": { + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dependencies": {} + }, + "events@3.3.0": { + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dependencies": {} + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dependencies": {} + }, + "process@0.11.10": { + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dependencies": {} + }, + "readable-stream@4.5.2": { + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "abort-controller@3.0.0", + "buffer": "buffer@6.0.3", + "events": "events@3.3.0", + "process": "process@0.11.10", + "string_decoder": "string_decoder@1.3.0" + } + }, + "replicate@0.31.0": { + "integrity": "sha512-BQl52LqndfY2sLQ384jyspaJI5ia301+IN1zOBbKZa2dB5EnayUxS0ynFueOdwo/4qRfQTR0GKJwpKFK/mb3zw==", + "dependencies": { + "readable-stream": "readable-stream@4.5.2" + } + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dependencies": {} + }, + "string_decoder@1.3.0": { + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + } + } + }, + "remote": {}, + "workspace": { + "dependencies": [ + "npm:replicate" + ] + } +} diff --git a/integration/deno/index.test.ts b/integration/deno/index.test.ts new file mode 100644 index 00000000..28965d57 --- /dev/null +++ b/integration/deno/index.test.ts @@ -0,0 +1,26 @@ +import { assertEquals } from "jsr:@std/assert"; +import main from "./index.ts"; + +// Verify exported types. +import type { + Status, + Visibility, + WebhookEventType, + ApiError, + Collection, + Hardware, + Model, + ModelVersion, + Prediction, + Training, + Page, + ServerSentEvent, +} from "replicate"; + +Deno.test({ + name: "main", + async fn() { + const output = await main(); + assertEquals({ output }, { output: "hello Deno the dinosaur" }); + }, +}); diff --git a/integration/deno/index.ts b/integration/deno/index.ts new file mode 100644 index 00000000..069e28a0 --- /dev/null +++ b/integration/deno/index.ts @@ -0,0 +1,16 @@ +import Replicate from "replicate"; + +const replicate = new Replicate({ + auth: Deno.env.get("REPLICATE_API_TOKEN"), +}); + +export default async function main() { + return await replicate.run( + "replicate/hello-world:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + { + input: { + text: "Deno the dinosaur", + }, + } + ); +} From 90de8f866f2f476a86e4585a18e18bebd3ce4a2a Mon Sep 17 00:00:00 2001 From: Mattt Date: Fri, 5 Jul 2024 04:40:35 -0700 Subject: [PATCH 11/68] Fix Deno integration test (#282) Add missing package.json file --- integration/deno/package.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 integration/deno/package.json diff --git a/integration/deno/package.json b/integration/deno/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/integration/deno/package.json @@ -0,0 +1 @@ +{} From f70b3d967a6664f9b1681bfa06599a6407c2d233 Mon Sep 17 00:00:00 2001 From: Mattt Date: Fri, 5 Jul 2024 05:07:22 -0700 Subject: [PATCH 12/68] Document supported platforms (#280) * Document supported platforms * Document support for Deno >= 1.28 --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 797af77c..0f26281d 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,18 @@ and everything else you can do with > For more information about how to build a web application > check out our ["Build a website with Next.js"](https://replicate.com/docs/get-started/nextjs) guide. -## Installation +## Supported platforms + +- [Node.js](https://nodejs.org) >= 18 +- [Bun](https://bun.sh) >= 1.0 +- [Deno](https://deno.com) >= 1.28 -This library requires Node.js >= 18. +You can also use this client library on most serverless platforms, including +[Cloudflare Workers](https://developers.cloudflare.com/workers/), +[Vercel functions](https://vercel.com/docs/functions), and +[AWS Lambda](https://aws.amazon.com/lambda/). + +## Installation Install it from npm: @@ -230,7 +239,7 @@ By default, the `globalThis.fetch` function is used, which is available on [Node.js 18](https://nodejs.org/en/blog/announcements/v18-release-announce#fetch-experimental) and later, as well as [Cloudflare Workers](https://developers.cloudflare.com/workers/runtime-apis/fetch/), -[Vercel Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions), +[Vercel Functions](https://vercel.com/docs/functions), and other environments. On earlier versions of Node.js From 8d8e59914b59410c39d82483968d1e3d806e7a08 Mon Sep 17 00:00:00 2001 From: Mattt Date: Fri, 5 Jul 2024 05:57:56 -0700 Subject: [PATCH 13/68] Run integration tests with `replicate/canary` model (#283) * Run integration tests with replicate/canary model * Update expected output * Add test matrix for Next.js * Reorder test matrix configurations * Add test matrix for build step --- .github/workflows/ci.yml | 29 +++++++++++++++++++--------- integration/bun/index.test.ts | 2 +- integration/commonjs/index.js | 5 +++-- integration/commonjs/index.test.js | 2 +- integration/deno/index.test.ts | 2 +- integration/deno/index.ts | 7 ++++--- integration/esm/index.js | 5 +++-- integration/esm/index.test.js | 2 +- integration/typescript/index.test.ts | 2 +- integration/typescript/index.ts | 7 ++++--- 10 files changed, 39 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87b1f75e..27a982cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ jobs: strategy: matrix: + suite: [node] # See supported Node.js release schedule at https://nodejs.org/en/about/previous-releases node-version: [18.x, 20.x, 22.x] @@ -32,15 +33,20 @@ jobs: build: runs-on: ubuntu-latest + strategy: + matrix: + # See supported Node.js release schedule at https://nodejs.org/en/about/previous-releases + node-version: [20.x] + outputs: tarball-name: ${{ steps.pack.outputs.tarball-name }} steps: - uses: actions/checkout@v4 - - name: Use Node.js + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: ${{ matrix.node-version }} cache: "npm" - name: Build tarball id: pack @@ -61,9 +67,9 @@ jobs: strategy: matrix: + suite: [commonjs, esm, typescript] # See supported Node.js release schedule at https://nodejs.org/en/about/previous-releases node-version: [18.x, 20.x] - suite: [commonjs, esm, typescript] fail-fast: false steps: @@ -90,9 +96,9 @@ jobs: strategy: matrix: - node-version: [20.x] - browser: ["chromium", "firefox", "webkit"] suite: ["browser"] + browser: ["chromium", "firefox", "webkit"] + node-version: [20.x] fail-fast: false steps: @@ -122,8 +128,8 @@ jobs: strategy: matrix: - node-version: [20.x] suite: [cloudflare-worker] + node-version: [20.x] steps: - uses: actions/checkout@v4 @@ -150,8 +156,8 @@ jobs: strategy: matrix: - bun-version: [1.0.11] suite: [bun] + bun-version: [1.0.11] steps: - uses: actions/checkout@v4 @@ -180,8 +186,8 @@ jobs: strategy: matrix: - deno-version: [v1.x] suite: [deno] + deno-version: [v1.x] steps: - uses: actions/checkout@v4 @@ -202,6 +208,11 @@ jobs: needs: [test, build] runs-on: ubuntu-latest + strategy: + matrix: + suite: [nextjs] + node-version: [20.x] + env: REPLICATE_API_TOKEN: ${{ secrets.REPLICATE_API_TOKEN }} @@ -213,7 +224,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: ${{ matrix.node-version }} cache: "npm" - run: | npm --prefix integration/next install diff --git a/integration/bun/index.test.ts b/integration/bun/index.test.ts index 1357e5ba..f0665a6d 100644 --- a/integration/bun/index.test.ts +++ b/integration/bun/index.test.ts @@ -19,5 +19,5 @@ import type { test("main", async () => { const output = await main(); - expect(output).toContain("Brünnhilde Bun"); + expect(output).toEqual("hello there, Brünnhilde Bun"); }); diff --git a/integration/commonjs/index.js b/integration/commonjs/index.js index 40f00cd0..6d8c436b 100644 --- a/integration/commonjs/index.js +++ b/integration/commonjs/index.js @@ -5,12 +5,13 @@ const replicate = new Replicate({ }); module.exports = async function main() { - return await replicate.run( - "replicate/hello-world:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + const output = await replicate.run( + "replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272", { input: { text: "Claire CommonJS", }, } ); + return output.join("").trim(); }; diff --git a/integration/commonjs/index.test.js b/integration/commonjs/index.test.js index 6e0f8b2a..afc3c014 100644 --- a/integration/commonjs/index.test.js +++ b/integration/commonjs/index.test.js @@ -4,5 +4,5 @@ const main = require("./index"); test("main", async () => { const output = await main(); - assert.equal(output, "hello Claire CommonJS"); + assert.equal(output, "hello there, Claire CommonJS"); }); diff --git a/integration/deno/index.test.ts b/integration/deno/index.test.ts index 28965d57..e944d770 100644 --- a/integration/deno/index.test.ts +++ b/integration/deno/index.test.ts @@ -21,6 +21,6 @@ Deno.test({ name: "main", async fn() { const output = await main(); - assertEquals({ output }, { output: "hello Deno the dinosaur" }); + assertEquals({ output }, { output: "hello there, Deno the dinosaur" }); }, }); diff --git a/integration/deno/index.ts b/integration/deno/index.ts index 069e28a0..a235699b 100644 --- a/integration/deno/index.ts +++ b/integration/deno/index.ts @@ -5,12 +5,13 @@ const replicate = new Replicate({ }); export default async function main() { - return await replicate.run( - "replicate/hello-world:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + const output = (await replicate.run( + "replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272", { input: { text: "Deno the dinosaur", }, } - ); + )) as string[]; + return output.join("").trim(); } diff --git a/integration/esm/index.js b/integration/esm/index.js index 9264f3c0..48feccb7 100644 --- a/integration/esm/index.js +++ b/integration/esm/index.js @@ -5,12 +5,13 @@ const replicate = new Replicate({ }); export default async function main() { - return await replicate.run( - "replicate/hello-world:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + const output = await replicate.run( + "replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272", { input: { text: "Evelyn ESM", }, } ); + return Array.isArray(output) ? output.join("").trim() : String(output).trim(); } diff --git a/integration/esm/index.test.js b/integration/esm/index.test.js index 7549e909..c95ab8b0 100644 --- a/integration/esm/index.test.js +++ b/integration/esm/index.test.js @@ -4,5 +4,5 @@ import main from "./index.js"; test("main", async () => { const output = await main(); - assert.equal(output, "hello Evelyn ESM"); + assert.equal(output, "hello there, Evelyn ESM"); }); diff --git a/integration/typescript/index.test.ts b/integration/typescript/index.test.ts index b7928c5d..0f0f356b 100644 --- a/integration/typescript/index.test.ts +++ b/integration/typescript/index.test.ts @@ -20,5 +20,5 @@ import type { test("main", async () => { const output = await main(); - assert.equal(output, "hello Tracy TypeScript"); + assert.equal(output, "hello there, Tracy TypeScript"); }); diff --git a/integration/typescript/index.ts b/integration/typescript/index.ts index e49f75c2..1b276bc9 100644 --- a/integration/typescript/index.ts +++ b/integration/typescript/index.ts @@ -5,12 +5,13 @@ const replicate = new Replicate({ }); export default async function main() { - return await replicate.run( - "replicate/hello-world:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + const output = (await replicate.run( + "replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272", { input: { text: "Tracy TypeScript", }, } - ); + )) as string[]; + return output.join("").trim(); } From ba5614ed263808046db9101d7e150cc9388a92d4 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Fri, 5 Jul 2024 16:45:38 +0100 Subject: [PATCH 14/68] Document alternate interface to `validateWebhook()` Fixes #261 --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 0f26281d..401bfadb 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,19 @@ export async function POST(request) { } ``` +If your environment doesn't support `Request` objects, you can pass the required information to `validateWebhook` directly: + +```js +const requestData = { + id: "123", // the `Webhook-Id` header + timestamp: "0123456", // the `Webhook-Timestamp` header + signature: "xyz", // the `Webhook-Signature` header + body: "{...}", // the request body as a string, ArrayBuffer or ReadableStream + secret: "shhh", // the webhook secret, obtained from the `replicate.webhooks.defaul.secret` endpoint +}; +const webhookIsValid = await validateWebhook(requestData); +``` + ## TypeScript The `Replicate` constructor and all `replicate.*` methods are fully typed. From 7d4c04cd8b394112a99297b260be16196b9934f0 Mon Sep 17 00:00:00 2001 From: Mattt Date: Fri, 5 Jul 2024 11:12:27 -0700 Subject: [PATCH 15/68] Document 100MiB file upload limit (#287) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 401bfadb..8f4350b1 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,10 @@ const output = await replicate.run(model, { input }); > [!NOTE] > File handle inputs are automatically uploaded to Replicate. > See [`replicate.files.create`](#replicatefilescreate) for more information. +> The maximum size for uploaded files is 100MiB. +> To run a model with a larger file as an input, +> upload the file to your own storage provider +> and pass a publicly accessible URL. ### Webhooks From 997da157836be911de09c4bf7cdf93f7bc58688e Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Fri, 5 Jul 2024 19:13:40 +0100 Subject: [PATCH 16/68] Re-raise 4xx responses from server when uploading files (#284) Fixes #270 We were falling back to base64 encoding the file data when requests to upload the file failed. However if the client makes an invalid request such as failing to include Authorization headers by forgetting to auth then we should surface these errors. This commit now re-raises any 4xx errors returned while attempting to upload a file and adds tests to verify the behavior. --- index.test.ts | 427 +++++++++++++++++++++----------------------------- lib/util.js | 35 ++--- 2 files changed, 189 insertions(+), 273 deletions(-) diff --git a/index.test.ts b/index.test.ts index 2645ca4d..c4d7e067 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1,11 +1,5 @@ import { expect, jest, test } from "@jest/globals"; -import Replicate, { - ApiError, - Model, - Prediction, - validateWebhook, - parseProgressFromLogs, -} from "replicate"; +import Replicate, { ApiError, Model, Prediction, validateWebhook, parseProgressFromLogs } from "replicate"; import nock from "nock"; import { Readable } from "node:stream"; import { createReadableStream } from "./lib/stream"; @@ -42,8 +36,7 @@ const fileTestCases = [ describe("Replicate client", () => { let unmatched: any[] = []; - const handleNoMatch = (req: unknown, options: any, body: string) => - unmatched.push({ req, options, body }); + const handleNoMatch = (req: unknown, options: any, body: string) => unmatched.push({ req, options, body }); beforeEach(() => { client = new Replicate({ auth: "test-token" }); @@ -123,8 +116,7 @@ describe("Replicate client", () => { { name: "Super resolution", slug: "super-resolution", - description: - "Upscaling models that create high-quality images from low-quality images.", + description: "Upscaling models that create high-quality images from low-quality images.", }, { name: "Image classification", @@ -147,8 +139,7 @@ describe("Replicate client", () => { nock(BASE_URL).get("/collections/super-resolution").reply(200, { name: "Super resolution", slug: "super-resolution", - description: - "Upscaling models that create high-quality images from low-quality images.", + description: "Upscaling models that create high-quality images from low-quality images.", models: [], }); @@ -188,9 +179,7 @@ describe("Replicate client", () => { results: [{ url: "https://replicate.com/some-user/model-1" }], next: "https://api.replicate.com/v1/models?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw", }) - .get( - "/models?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw" - ) + .get("/models?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw") .reply(200, { results: [{ url: "https://replicate.com/some-user/model-2" }], next: null, @@ -248,12 +237,10 @@ describe("Replicate client", () => { expectedResponse: { id: "ufawqhfynnddngldkgtslldrkq", model: "replicate/hello-world", - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", urls: { get: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq", - cancel: - "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", + cancel: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", }, input: testCase.input, created_at: "2022-04-26T22:13:06.224088Z", @@ -263,79 +250,64 @@ describe("Replicate client", () => { }, })); - test.each(predictionTestCases)( - "$description", - async ({ input, expectedResponse }) => { - nock(BASE_URL) - .post("/predictions", { - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", - input: input as Record, - webhook: "http://test.host/webhook", - webhook_events_filter: ["output", "completed"], - }) - .reply(200, expectedResponse); - - const response = await client.predictions.create({ - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + test.each(predictionTestCases)("$description", async ({ input, expectedResponse }) => { + nock(BASE_URL) + .post("/predictions", { + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: input as Record, webhook: "http://test.host/webhook", webhook_events_filter: ["output", "completed"], - }); + }) + .reply(200, expectedResponse); - expect(response.input).toEqual(input); - expect(response.status).toBe(expectedResponse.status); - } - ); + const response = await client.predictions.create({ + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + input: input as Record, + webhook: "http://test.host/webhook", + webhook_events_filter: ["output", "completed"], + }); - test.each(fileTestCases)( - "converts a $type input into a Replicate file URL", - async ({ value: data, type }) => { - const mockedFetch = jest.spyOn(client, "fetch"); + expect(response.input).toEqual(input); + expect(response.status).toBe(expectedResponse.status); + }); - nock(BASE_URL) - .post("/files") - .reply(201, { - urls: { - get: "https://replicate.com/api/files/123", - }, - }) - .post( - "/predictions", - (body) => body.input.data === "https://replicate.com/api/files/123" - ) - .reply(201, (_uri: string, body: Record) => { - return body; - }); + test.each(fileTestCases)("converts a $type input into a Replicate file URL", async ({ value: data, type }) => { + const mockedFetch = jest.spyOn(client, "fetch"); - const prediction = await client.predictions.create({ - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", - input: { - prompt: "Tell me a story", - data, + nock(BASE_URL) + .post("/files") + .reply(201, { + urls: { + get: "https://replicate.com/api/files/123", }, + }) + .post("/predictions", (body) => body.input.data === "https://replicate.com/api/files/123") + .reply(201, (_uri: string, body: Record) => { + return body; }); - expect(client.fetch).toHaveBeenCalledWith( - new URL("https://api.replicate.com/v1/files"), - { - method: "POST", - body: expect.any(FormData), - headers: expect.any(Object), - } - ); - const form = mockedFetch.mock.calls[0][1]?.body as FormData; - // @ts-ignore - expect(form?.get("content")?.name).toMatch(new RegExp(`^${type}_`)); - - expect(prediction.input).toEqual({ + const prediction = await client.predictions.create({ + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + input: { prompt: "Tell me a story", - data: "https://replicate.com/api/files/123", - }); - } - ); + data, + }, + }); + + expect(client.fetch).toHaveBeenCalledWith(new URL("https://api.replicate.com/v1/files"), { + method: "POST", + body: expect.any(FormData), + headers: expect.any(Object), + }); + const form = mockedFetch.mock.calls[0][1]?.body as FormData; + // @ts-ignore + expect(form?.get("content")?.name).toMatch(new RegExp(`^${type}_`)); + + expect(prediction.input).toEqual({ + prompt: "Tell me a story", + data: "https://replicate.com/api/files/123", + }); + }); test.each(fileTestCases)( "converts a $type input into a base64 encoded string", @@ -351,8 +323,7 @@ describe("Replicate client", () => { }); await client.predictions.create({ - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { prompt: "Tell me a story", data, @@ -361,7 +332,38 @@ describe("Replicate client", () => { }); expect(actual?.input.data).toEqual(expected); - } + }, + ); + + test.each(fileTestCases)( + "raises an error when the file upload fails with 4xx error for a $type input", + async ({ value: data, expected }) => { + let actual: Record | undefined; + nock(BASE_URL) + .post("/files") + .reply(401, "Unauthorized") + .post("/predictions") + .reply(201, (_uri: string, body: Record) => { + actual = body; + return body; + }); + + await expect(async () => { + await client.predictions.create({ + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + input: { + prompt: "Tell me a story", + data, + }, + stream: true, + }); + }).rejects.toThrowError( + expect.objectContaining({ + name: "ApiError", + message: expect.stringContaining("401"), + }), + ); + }, ); test("Passes stream parameter to API endpoint", async () => { @@ -373,8 +375,7 @@ describe("Replicate client", () => { }); await client.predictions.create({ - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { prompt: "Tell me a story", }, @@ -385,8 +386,7 @@ describe("Replicate client", () => { test("Throws an error if webhook URL is invalid", async () => { await expect(async () => { await client.predictions.create({ - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { text: "Alice", }, @@ -402,15 +402,14 @@ describe("Replicate client", () => { status: 400, detail: "Invalid input", }, - { "Content-Type": "application/json" } + { "Content-Type": "application/json" }, ); try { expect.hasAssertions(); await client.predictions.create({ - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { text: null, }, @@ -429,15 +428,14 @@ describe("Replicate client", () => { { detail: "Too many requests", }, - { "Content-Type": "application/json", "Retry-After": "1" } + { "Content-Type": "application/json", "Retry-After": "1" }, ) .post("/predictions") .reply(201, { id: "ufawqhfynnddngldkgtslldrkq", }); const prediction = await client.predictions.create({ - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { text: "Alice", }, @@ -451,19 +449,18 @@ describe("Replicate client", () => { { detail: "Internal server error", }, - { "Content-Type": "application/json" } + { "Content-Type": "application/json" }, ); await expect( client.predictions.create({ - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { text: "Alice", }, - }) + }), ).rejects.toThrow( - `Request to https://api.replicate.com/v1/predictions failed with status 500 Internal Server Error: {"detail":"Internal server error"}.` + `Request to https://api.replicate.com/v1/predictions failed with status 500 Internal Server Error: {"detail":"Internal server error"}.`, ); }); }); @@ -475,12 +472,10 @@ describe("Replicate client", () => { .reply(200, { id: "rrr4z55ocneqzikepnug6xezpe", model: "stability-ai/stable-diffusion", - version: - "be04660a5b93ef2aff61e3668dedb4cbeb14941e62a3fd5998364a32d613e35e", + version: "be04660a5b93ef2aff61e3668dedb4cbeb14941e62a3fd5998364a32d613e35e", urls: { get: "https://api.replicate.com/v1/predictions/rrr4z55ocneqzikepnug6xezpe", - cancel: - "https://api.replicate.com/v1/predictions/rrr4z55ocneqzikepnug6xezpe/cancel", + cancel: "https://api.replicate.com/v1/predictions/rrr4z55ocneqzikepnug6xezpe/cancel", }, created_at: "2022-09-13T22:54:18.578761Z", started_at: "2022-09-13T22:54:19.438525Z", @@ -499,9 +494,7 @@ describe("Replicate client", () => { predict_time: 4.484541, }, }); - const prediction = await client.predictions.get( - "rrr4z55ocneqzikepnug6xezpe" - ); + const prediction = await client.predictions.get("rrr4z55ocneqzikepnug6xezpe"); expect(prediction.id).toBe("rrr4z55ocneqzikepnug6xezpe"); }); @@ -513,16 +506,14 @@ describe("Replicate client", () => { { detail: "Too many requests", }, - { "Content-Type": "application/json", "Retry-After": "1" } + { "Content-Type": "application/json", "Retry-After": "1" }, ) .get("/predictions/rrr4z55ocneqzikepnug6xezpe") .reply(200, { id: "rrr4z55ocneqzikepnug6xezpe", }); - const prediction = await client.predictions.get( - "rrr4z55ocneqzikepnug6xezpe" - ); + const prediction = await client.predictions.get("rrr4z55ocneqzikepnug6xezpe"); expect(prediction.id).toBe("rrr4z55ocneqzikepnug6xezpe"); }); @@ -534,16 +525,14 @@ describe("Replicate client", () => { { detail: "Internal server error", }, - { "Content-Type": "application/json" } + { "Content-Type": "application/json" }, ) .get("/predictions/rrr4z55ocneqzikepnug6xezpe") .reply(200, { id: "rrr4z55ocneqzikepnug6xezpe", }); - const prediction = await client.predictions.get( - "rrr4z55ocneqzikepnug6xezpe" - ); + const prediction = await client.predictions.get("rrr4z55ocneqzikepnug6xezpe"); expect(prediction.id).toBe("rrr4z55ocneqzikepnug6xezpe"); }); }); @@ -555,12 +544,10 @@ describe("Replicate client", () => { .reply(200, { id: "ufawqhfynnddngldkgtslldrkq", model: "replicate/hello-world", - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", urls: { get: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq", - cancel: - "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", + cancel: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", }, created_at: "2022-04-26T22:13:06.224088Z", started_at: "2022-04-26T22:13:06.224088Z", @@ -575,9 +562,7 @@ describe("Replicate client", () => { metrics: {}, }); - const prediction = await client.predictions.cancel( - "ufawqhfynnddngldkgtslldrkq" - ); + const prediction = await client.predictions.cancel("ufawqhfynnddngldkgtslldrkq"); expect(prediction.status).toBe("canceled"); }); @@ -595,12 +580,10 @@ describe("Replicate client", () => { { id: "jpzd7hm5gfcapbfyt4mqytarku", model: "stability-ai/stable-diffusion", - version: - "b21cbe271e65c1718f2999b038c18b45e21e4fba961181fbfae9342fc53b9e05", + version: "b21cbe271e65c1718f2999b038c18b45e21e4fba961181fbfae9342fc53b9e05", urls: { get: "https://api.replicate.com/v1/predictions/jpzd7hm5gfcapbfyt4mqytarku", - cancel: - "https://api.replicate.com/v1/predictions/jpzd7hm5gfcapbfyt4mqytarku/cancel", + cancel: "https://api.replicate.com/v1/predictions/jpzd7hm5gfcapbfyt4mqytarku/cancel", }, created_at: "2022-04-26T20:00:40.658234Z", started_at: "2022-04-26T20:00:84.583803Z", @@ -623,9 +606,7 @@ describe("Replicate client", () => { results: [{ id: "ufawqhfynnddngldkgtslldrkq" }], next: "https://api.replicate.com/v1/predictions?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw", }) - .get( - "/predictions?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw" - ) + .get("/predictions?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw") .reply(200, { results: [{ id: "rrr4z55ocneqzikepnug6xezpe" }], next: null, @@ -635,10 +616,7 @@ describe("Replicate client", () => { for await (const batch of client.paginate(client.predictions.list)) { results.push(...batch); } - expect(results).toEqual([ - { id: "ufawqhfynnddngldkgtslldrkq" }, - { id: "rrr4z55ocneqzikepnug6xezpe" }, - ]); + expect(results).toEqual([{ id: "ufawqhfynnddngldkgtslldrkq" }, { id: "rrr4z55ocneqzikepnug6xezpe" }]); // Add more tests for error handling, edge cases, etc. }); @@ -647,13 +625,10 @@ describe("Replicate client", () => { describe("trainings.create", () => { test("Calls the correct API route with the correct payload", async () => { nock(BASE_URL) - .post( - "/models/owner/model/versions/632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532/trainings" - ) + .post("/models/owner/model/versions/632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532/trainings") .reply(200, { id: "zz4ibbonubfz7carwiefibzgga", - version: - "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", + version: "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", status: "starting", input: { text: "...", @@ -675,25 +650,20 @@ describe("Replicate client", () => { input: { text: "...", }, - } + }, ); expect(training.id).toBe("zz4ibbonubfz7carwiefibzgga"); }); test("Throws an error if webhook is not a valid URL", async () => { await expect( - client.trainings.create( - "owner", - "model", - "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", - { - destination: "new_owner/new_model", - input: { - text: "...", - }, - webhook: "invalid-url", - } - ) + client.trainings.create("owner", "model", "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", { + destination: "new_owner/new_model", + input: { + text: "...", + }, + webhook: "invalid-url", + }), ).rejects.toThrow("Invalid webhook URL"); }); @@ -753,9 +723,7 @@ describe("Replicate client", () => { completed_at: null, }); - const training = await client.trainings.cancel( - "zz4ibbonubfz7carwiefibzgga" - ); + const training = await client.trainings.cancel("zz4ibbonubfz7carwiefibzgga"); expect(training.status).toBe("canceled"); }); @@ -773,12 +741,10 @@ describe("Replicate client", () => { { id: "jpzd7hm5gfcapbfyt4mqytarku", model: "stability-ai/sdxl", - version: - "b21cbe271e65c1718f2999b038c18b45e21e4fba961181fbfae9342fc53b9e05", + version: "b21cbe271e65c1718f2999b038c18b45e21e4fba961181fbfae9342fc53b9e05", urls: { get: "https://api.replicate.com/v1/trainings/jpzd7hm5gfcapbfyt4mqytarku", - cancel: - "https://api.replicate.com/v1/trainings/jpzd7hm5gfcapbfyt4mqytarku/cancel", + cancel: "https://api.replicate.com/v1/trainings/jpzd7hm5gfcapbfyt4mqytarku/cancel", }, created_at: "2022-04-26T20:00:40.658234Z", started_at: "2022-04-26T20:00:84.583803Z", @@ -801,9 +767,7 @@ describe("Replicate client", () => { results: [{ id: "ufawqhfynnddngldkgtslldrkq" }], next: "https://api.replicate.com/v1/trainings?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw", }) - .get( - "/trainings?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw" - ) + .get("/trainings?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw") .reply(200, { results: [{ id: "rrr4z55ocneqzikepnug6xezpe" }], next: null, @@ -813,10 +777,7 @@ describe("Replicate client", () => { for await (const batch of client.paginate(client.trainings.list)) { results.push(...batch); } - expect(results).toEqual([ - { id: "ufawqhfynnddngldkgtslldrkq" }, - { id: "rrr4z55ocneqzikepnug6xezpe" }, - ]); + expect(results).toEqual([{ id: "ufawqhfynnddngldkgtslldrkq" }, { id: "rrr4z55ocneqzikepnug6xezpe" }]); // Add more tests for error handling, edge cases, etc. }); @@ -829,12 +790,10 @@ describe("Replicate client", () => { .reply(200, { id: "mfrgcyzzme2wkmbwgzrgmntcg", model: "replicate/hello-world", - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", urls: { get: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq", - cancel: - "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", + cancel: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", }, created_at: "2022-09-10T09:44:22.165836Z", started_at: null, @@ -848,17 +807,13 @@ describe("Replicate client", () => { logs: null, metrics: {}, }); - const prediction = await client.deployments.predictions.create( - "replicate", - "greeter", - { - input: { - text: "Alice", - }, - webhook: "http://test.host/webhook", - webhook_events_filter: ["output", "completed"], - } - ); + const prediction = await client.deployments.predictions.create("replicate", "greeter", { + input: { + text: "Alice", + }, + webhook: "http://test.host/webhook", + webhook_events_filter: ["output", "completed"], + }); expect(prediction.id).toBe("mfrgcyzzme2wkmbwgzrgmntcg"); }); // Add more tests for error handling, edge cases, etc. @@ -874,8 +829,7 @@ describe("Replicate client", () => { current_release: { number: 1, model: "stability-ai/sdxl", - version: - "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", + version: "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", created_at: "2024-02-15T16:32:57.018467Z", created_by: { type: "organization", @@ -891,10 +845,7 @@ describe("Replicate client", () => { }, }); - const deployment = await client.deployments.get( - "acme", - "my-app-image-generator" - ); + const deployment = await client.deployments.get("acme", "my-app-image-generator"); expect(deployment.owner).toBe("acme"); expect(deployment.name).toBe("my-app-image-generator"); @@ -913,8 +864,7 @@ describe("Replicate client", () => { current_release: { number: 1, model: "stability-ai/sdxl", - version: - "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", + version: "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", created_at: "2024-02-15T16:32:57.018467Z", created_by: { type: "organization", @@ -933,8 +883,7 @@ describe("Replicate client", () => { const deployment = await client.deployments.create({ name: "my-app-image-generator", model: "stability-ai/sdxl", - version: - "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", + version: "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", hardware: "gpu-t4", min_instances: 1, max_instances: 5, @@ -957,8 +906,7 @@ describe("Replicate client", () => { current_release: { number: 2, model: "stability-ai/sdxl", - version: - "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", + version: "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", created_at: "2024-02-16T08:14:22.345678Z", created_by: { type: "organization", @@ -974,25 +922,18 @@ describe("Replicate client", () => { }, }); - const deployment = await client.deployments.update( - "acme", - "my-app-image-generator", - { - version: - "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", - hardware: "gpu-a40-large", - min_instances: 3, - max_instances: 10, - } - ); + const deployment = await client.deployments.update("acme", "my-app-image-generator", { + version: "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", + hardware: "gpu-a40-large", + min_instances: 3, + max_instances: 10, + }); expect(deployment.current_release.number).toBe(2); expect(deployment.current_release.version).toBe( - "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532" - ); - expect(deployment.current_release.configuration.hardware).toBe( - "gpu-a40-large" + "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", ); + expect(deployment.current_release.configuration.hardware).toBe("gpu-a40-large"); expect(deployment.current_release.configuration.min_instances).toBe(3); expect(deployment.current_release.configuration.max_instances).toBe(10); }); @@ -1001,14 +942,9 @@ describe("Replicate client", () => { describe("deployments.delete", () => { test("Calls the correct API route with the correct payload", async () => { - nock(BASE_URL) - .delete("/deployments/acme/my-app-image-generator") - .reply(204); + nock(BASE_URL).delete("/deployments/acme/my-app-image-generator").reply(204); - const success = await client.deployments.delete( - "acme", - "my-app-image-generator" - ); + const success = await client.deployments.delete("acme", "my-app-image-generator"); expect(success).toBe(true); }); }); @@ -1054,8 +990,7 @@ describe("Replicate client", () => { status: "starting", created_at: "2023-11-27T13:35:45.99397566Z", urls: { - cancel: - "https://api.replicate.com/v1/predictions/heat2o3bzn3ahtr6bjfftvbaci/cancel", + cancel: "https://api.replicate.com/v1/predictions/heat2o3bzn3ahtr6bjfftvbaci/cancel", get: "https://api.replicate.com/v1/predictions/heat2o3bzn3ahtr6bjfftvbaci", }, }); @@ -1265,7 +1200,7 @@ describe("Replicate client", () => { (prediction) => { const progress = parseProgressFromLogs(prediction); callback(prediction, progress); - } + }, ); expect(output).toBe("Goodbye!"); @@ -1277,7 +1212,7 @@ describe("Replicate client", () => { status: "starting", logs: null, }, - null + null, ); expect(callback).toHaveBeenNthCalledWith( @@ -1291,7 +1226,7 @@ describe("Replicate client", () => { percentage: 0.4, current: 2, total: 5, - } + }, ); expect(callback).toHaveBeenNthCalledWith( @@ -1305,7 +1240,7 @@ describe("Replicate client", () => { percentage: 0.8, current: 4, total: 5, - } + }, ); expect(callback).toHaveBeenNthCalledWith( @@ -1320,7 +1255,7 @@ describe("Replicate client", () => { percentage: 1.0, current: 5, total: 5, - } + }, ); expect(callback).toHaveBeenCalledTimes(4); @@ -1354,7 +1289,7 @@ describe("Replicate client", () => { input: { text: "Hello, world!" }, wait: { interval: 1 }, }, - progress + progress, ); expect(output).toBe("Goodbye!"); @@ -1397,9 +1332,7 @@ describe("Replicate client", () => { output: "foobar", }); - await expect( - client.run("a/b-1.0:abc123", { input: { text: "Hello, world!" } }) - ).resolves.not.toThrow(); + await expect(client.run("a/b-1.0:abc123", { input: { text: "Hello, world!" } })).resolves.not.toThrow(); }); test("Throws an error for invalid identifiers", async () => { @@ -1416,15 +1349,12 @@ describe("Replicate client", () => { test("Throws an error if webhook URL is invalid", async () => { await expect(async () => { - await client.run( - "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", - { - input: { - text: "Alice", - }, - webhook: "invalid-url", - } - ); + await client.run("owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", { + input: { + text: "Alice", + }, + webhook: "invalid-url", + }); }).rejects.toThrow("Invalid webhook URL"); }); @@ -1463,7 +1393,7 @@ describe("Replicate client", () => { input: { text: "Hello, world!" }, signal, }, - onProgress + onProgress, ); expect(body).toBeDefined(); @@ -1475,19 +1405,19 @@ describe("Replicate client", () => { 1, expect.objectContaining({ status: "processing", - }) + }), ); expect(onProgress).toHaveBeenNthCalledWith( 2, expect.objectContaining({ status: "processing", - }) + }), ); expect(onProgress).toHaveBeenNthCalledWith( 3, expect.objectContaining({ status: "canceled", - }) + }), ); scope.done(); @@ -1512,8 +1442,7 @@ describe("Replicate client", () => { "Content-Type": "application/json", "Webhook-ID": "msg_p5jXN8AQM9LWM0D4loKWxJek", "Webhook-Timestamp": "1614265330", - "Webhook-Signature": - "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=", + "Webhook-Signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=", }, body: `{"test": 2432232314}`, }); @@ -1556,7 +1485,7 @@ describe("Replicate client", () => { id: EVENT_2 data: {} - `.replace(/^[ ]+/gm, "") + `.replace(/^[ ]+/gm, ""), ); const iterator = stream[Symbol.asyncIterator](); @@ -1587,7 +1516,7 @@ describe("Replicate client", () => { id: EVENT_3 data: {} - `.replace(/^[ ]+/gm, "") + `.replace(/^[ ]+/gm, ""), ); const iterator = stream[Symbol.asyncIterator](); @@ -1621,7 +1550,7 @@ describe("Replicate client", () => { id: EVENT_2 data: {} - `.replace(/^[ ]+/gm, "") + `.replace(/^[ ]+/gm, ""), ); const iterator = stream[Symbol.asyncIterator](); @@ -1653,7 +1582,7 @@ describe("Replicate client", () => { id: EVENT_2 data: {} - `.replace(/^[ ]+/gm, "") + `.replace(/^[ ]+/gm, ""), ); const iterator = stream[Symbol.asyncIterator](); @@ -1774,7 +1703,7 @@ describe("Replicate client", () => { id: EVENT_1 data: hello world - `.replace(/^[ ]+/gm, "") + `.replace(/^[ ]+/gm, ""), ); const iterator = stream[Symbol.asyncIterator](); @@ -1796,7 +1725,7 @@ describe("Replicate client", () => { id: EVENT_2 data: An unexpected error occurred - `.replace(/^[ ]+/gm, "") + `.replace(/^[ ]+/gm, ""), ); const iterator = stream[Symbol.asyncIterator](); @@ -1804,9 +1733,7 @@ describe("Replicate client", () => { done: false, value: { event: "output", id: "EVENT_1", data: "hello world" }, }); - await expect(iterator.next()).rejects.toThrowError( - "An unexpected error occurred" - ); + await expect(iterator.next()).rejects.toThrowError("An unexpected error occurred"); expect(await iterator.next()).toEqual({ done: true }); }); @@ -1814,7 +1741,7 @@ describe("Replicate client", () => { const stream = createStream("{}", 500); const iterator = stream[Symbol.asyncIterator](); await expect(iterator.next()).rejects.toThrowError( - "Request to https://stream.replicate.com/fake_stream failed with status 500" + "Request to https://stream.replicate.com/fake_stream failed with status 500", ); expect(await iterator.next()).toEqual({ done: true }); }); diff --git a/lib/util.js b/lib/util.js index 3745d9f0..b4483aed 100644 --- a/lib/util.js +++ b/lib/util.js @@ -67,18 +67,11 @@ async function validateWebhook(requestData, secret) { const signedContent = `${id}.${timestamp}.${body}`; - const computedSignature = await createHMACSHA256( - signingSecret.split("_").pop(), - signedContent - ); + const computedSignature = await createHMACSHA256(signingSecret.split("_").pop(), signedContent); - const expectedSignatures = signature - .split(" ") - .map((sig) => sig.split(",")[1]); + const expectedSignatures = signature.split(" ").map((sig) => sig.split(",")[1]); - return expectedSignatures.some( - (expectedSignature) => expectedSignature === computedSignature - ); + return expectedSignatures.some((expectedSignature) => expectedSignature === computedSignature); } /** @@ -105,13 +98,9 @@ async function createHMACSHA256(secret, data) { crypto = require.call(null, "node:crypto").webcrypto; } - const key = await crypto.subtle.importKey( - "raw", - base64ToBytes(secret), - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"] - ); + const key = await crypto.subtle.importKey("raw", base64ToBytes(secret), { name: "HMAC", hash: "SHA-256" }, false, [ + "sign", + ]); const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data)); return bytesToBase64(signature); @@ -235,6 +224,9 @@ async function transformFileInputs(client, inputs, strategy) { try { return await transformFileInputsToReplicateFileURLs(client, inputs); } catch (error) { + if (error instanceof ApiError && error.response.status >= 400 && error.response.status < 500) { + throw error; + } return await transformFileInputsToBase64EncodedDataURIs(inputs); } default: @@ -296,7 +288,7 @@ async function transformFileInputsToBase64EncodedDataURIs(inputs) { totalBytes += buffer.byteLength; if (totalBytes > MAX_DATA_URI_SIZE) { throw new Error( - `Combined filesize of prediction ${totalBytes} bytes exceeds 10mb limit for inline encoding, please provide URLs instead` + `Combined filesize of prediction ${totalBytes} bytes exceeds 10mb limit for inline encoding, please provide URLs instead`, ); } @@ -354,14 +346,11 @@ function isPlainObject(value) { if (proto === null) { return true; } - const Ctor = - Object.prototype.hasOwnProperty.call(proto, "constructor") && - proto.constructor; + const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor; return ( typeof Ctor === "function" && Ctor instanceof Ctor && - Function.prototype.toString.call(Ctor) === - Function.prototype.toString.call(Object) + Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object) ); } From 4c04a57dcaab4ed1d74673ed74bf3ef33ab5b90c Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Fri, 5 Jul 2024 11:16:21 -0700 Subject: [PATCH 17/68] 0.31.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6906dc8..7c454426 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "0.31.0", + "version": "0.31.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "0.31.0", + "version": "0.31.1", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index 9d9b60bf..7e13d359 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "0.31.0", + "version": "0.31.1", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From 2fd28906405b6caa82421314d93a3b5e77a84bff Mon Sep 17 00:00:00 2001 From: Mattt Date: Fri, 19 Jul 2024 10:41:30 -0700 Subject: [PATCH 18/68] Pin to Node 22.4 in CI workflow (#293) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27a982cb..a0a2241c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: matrix: suite: [node] # See supported Node.js release schedule at https://nodejs.org/en/about/previous-releases - node-version: [18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.4] # TODO: unpin to 22.x once https://github.com/nodejs/node/issues/53902 is resolved steps: - uses: actions/checkout@v4 From 9702c17d9e0da4afcaaa038471d58e7d9c60e634 Mon Sep 17 00:00:00 2001 From: Mattt Date: Thu, 25 Jul 2024 12:14:00 -0700 Subject: [PATCH 19/68] Revert "Pin to Node 22.4 in CI workflow (#293)" (#294) This reverts commit 2fd28906405b6caa82421314d93a3b5e77a84bff. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0a2241c..27a982cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: matrix: suite: [node] # See supported Node.js release schedule at https://nodejs.org/en/about/previous-releases - node-version: [18.x, 20.x, 22.4] # TODO: unpin to 22.x once https://github.com/nodejs/node/issues/53902 is resolved + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 From cdd0c9883e7996bb5127c52c0d0988f5d93f6655 Mon Sep 17 00:00:00 2001 From: Mattt Date: Thu, 25 Jul 2024 12:15:32 -0700 Subject: [PATCH 20/68] Add support for models.search endpoint (#292) --- README.md | 12 ++ index.d.ts | 1 + index.js | 1 + index.test.ts | 445 +++++++++++++++++++++++++++++++++----------------- lib/models.js | 19 +++ 5 files changed, 327 insertions(+), 151 deletions(-) diff --git a/README.md b/README.md index 8f4350b1..dabee2ee 100644 --- a/README.md +++ b/README.md @@ -455,6 +455,18 @@ const response = await replicate.models.list(); } ``` +### `replicate.models.search` + +Search for public models on Replicate. + +```js +const response = await replicate.models.search(query); +``` + +| name | type | description | +| ------- | ------ | -------------------------------------- | +| `query` | string | **Required**. The search query string. | + ### `replicate.models.create` Create a new public or private model. diff --git a/index.d.ts b/index.d.ts index 7d4ef0a9..25cd51af 100644 --- a/index.d.ts +++ b/index.d.ts @@ -281,6 +281,7 @@ declare module "replicate" { version_id: string ): Promise; }; + search(query: string): Promise>; }; predictions: { diff --git a/index.js b/index.js index 21e83f95..6b35db65 100644 --- a/index.js +++ b/index.js @@ -98,6 +98,7 @@ class Replicate { list: models.versions.list.bind(this), get: models.versions.get.bind(this), }, + search: models.search.bind(this), }; this.predictions = { diff --git a/index.test.ts b/index.test.ts index c4d7e067..7f9fcf2f 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1,5 +1,11 @@ import { expect, jest, test } from "@jest/globals"; -import Replicate, { ApiError, Model, Prediction, validateWebhook, parseProgressFromLogs } from "replicate"; +import Replicate, { + ApiError, + Model, + Prediction, + validateWebhook, + parseProgressFromLogs, +} from "replicate"; import nock from "nock"; import { Readable } from "node:stream"; import { createReadableStream } from "./lib/stream"; @@ -36,7 +42,8 @@ const fileTestCases = [ describe("Replicate client", () => { let unmatched: any[] = []; - const handleNoMatch = (req: unknown, options: any, body: string) => unmatched.push({ req, options, body }); + const handleNoMatch = (req: unknown, options: any, body: string) => + unmatched.push({ req, options, body }); beforeEach(() => { client = new Replicate({ auth: "test-token" }); @@ -116,7 +123,8 @@ describe("Replicate client", () => { { name: "Super resolution", slug: "super-resolution", - description: "Upscaling models that create high-quality images from low-quality images.", + description: + "Upscaling models that create high-quality images from low-quality images.", }, { name: "Image classification", @@ -139,7 +147,8 @@ describe("Replicate client", () => { nock(BASE_URL).get("/collections/super-resolution").reply(200, { name: "Super resolution", slug: "super-resolution", - description: "Upscaling models that create high-quality images from low-quality images.", + description: + "Upscaling models that create high-quality images from low-quality images.", models: [], }); @@ -179,7 +188,9 @@ describe("Replicate client", () => { results: [{ url: "https://replicate.com/some-user/model-1" }], next: "https://api.replicate.com/v1/models?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw", }) - .get("/models?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw") + .get( + "/models?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw" + ) .reply(200, { results: [{ url: "https://replicate.com/some-user/model-2" }], next: null, @@ -237,10 +248,12 @@ describe("Replicate client", () => { expectedResponse: { id: "ufawqhfynnddngldkgtslldrkq", model: "replicate/hello-world", - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", urls: { get: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq", - cancel: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", + cancel: + "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", }, input: testCase.input, created_at: "2022-04-26T22:13:06.224088Z", @@ -250,64 +263,79 @@ describe("Replicate client", () => { }, })); - test.each(predictionTestCases)("$description", async ({ input, expectedResponse }) => { - nock(BASE_URL) - .post("/predictions", { - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + test.each(predictionTestCases)( + "$description", + async ({ input, expectedResponse }) => { + nock(BASE_URL) + .post("/predictions", { + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + input: input as Record, + webhook: "http://test.host/webhook", + webhook_events_filter: ["output", "completed"], + }) + .reply(200, expectedResponse); + + const response = await client.predictions.create({ + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: input as Record, webhook: "http://test.host/webhook", webhook_events_filter: ["output", "completed"], - }) - .reply(200, expectedResponse); + }); - const response = await client.predictions.create({ - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", - input: input as Record, - webhook: "http://test.host/webhook", - webhook_events_filter: ["output", "completed"], - }); + expect(response.input).toEqual(input); + expect(response.status).toBe(expectedResponse.status); + } + ); - expect(response.input).toEqual(input); - expect(response.status).toBe(expectedResponse.status); - }); + test.each(fileTestCases)( + "converts a $type input into a Replicate file URL", + async ({ value: data, type }) => { + const mockedFetch = jest.spyOn(client, "fetch"); - test.each(fileTestCases)("converts a $type input into a Replicate file URL", async ({ value: data, type }) => { - const mockedFetch = jest.spyOn(client, "fetch"); + nock(BASE_URL) + .post("/files") + .reply(201, { + urls: { + get: "https://replicate.com/api/files/123", + }, + }) + .post( + "/predictions", + (body) => body.input.data === "https://replicate.com/api/files/123" + ) + .reply(201, (_uri: string, body: Record) => { + return body; + }); - nock(BASE_URL) - .post("/files") - .reply(201, { - urls: { - get: "https://replicate.com/api/files/123", + const prediction = await client.predictions.create({ + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + input: { + prompt: "Tell me a story", + data, }, - }) - .post("/predictions", (body) => body.input.data === "https://replicate.com/api/files/123") - .reply(201, (_uri: string, body: Record) => { - return body; }); - const prediction = await client.predictions.create({ - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", - input: { - prompt: "Tell me a story", - data, - }, - }); - - expect(client.fetch).toHaveBeenCalledWith(new URL("https://api.replicate.com/v1/files"), { - method: "POST", - body: expect.any(FormData), - headers: expect.any(Object), - }); - const form = mockedFetch.mock.calls[0][1]?.body as FormData; - // @ts-ignore - expect(form?.get("content")?.name).toMatch(new RegExp(`^${type}_`)); + expect(client.fetch).toHaveBeenCalledWith( + new URL("https://api.replicate.com/v1/files"), + { + method: "POST", + body: expect.any(FormData), + headers: expect.any(Object), + } + ); + const form = mockedFetch.mock.calls[0][1]?.body as FormData; + // @ts-ignore + expect(form?.get("content")?.name).toMatch(new RegExp(`^${type}_`)); - expect(prediction.input).toEqual({ - prompt: "Tell me a story", - data: "https://replicate.com/api/files/123", - }); - }); + expect(prediction.input).toEqual({ + prompt: "Tell me a story", + data: "https://replicate.com/api/files/123", + }); + } + ); test.each(fileTestCases)( "converts a $type input into a base64 encoded string", @@ -323,7 +351,8 @@ describe("Replicate client", () => { }); await client.predictions.create({ - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { prompt: "Tell me a story", data, @@ -332,7 +361,7 @@ describe("Replicate client", () => { }); expect(actual?.input.data).toEqual(expected); - }, + } ); test.each(fileTestCases)( @@ -350,7 +379,8 @@ describe("Replicate client", () => { await expect(async () => { await client.predictions.create({ - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { prompt: "Tell me a story", data, @@ -361,9 +391,9 @@ describe("Replicate client", () => { expect.objectContaining({ name: "ApiError", message: expect.stringContaining("401"), - }), + }) ); - }, + } ); test("Passes stream parameter to API endpoint", async () => { @@ -375,7 +405,8 @@ describe("Replicate client", () => { }); await client.predictions.create({ - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { prompt: "Tell me a story", }, @@ -386,7 +417,8 @@ describe("Replicate client", () => { test("Throws an error if webhook URL is invalid", async () => { await expect(async () => { await client.predictions.create({ - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { text: "Alice", }, @@ -402,14 +434,15 @@ describe("Replicate client", () => { status: 400, detail: "Invalid input", }, - { "Content-Type": "application/json" }, + { "Content-Type": "application/json" } ); try { expect.hasAssertions(); await client.predictions.create({ - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { text: null, }, @@ -428,14 +461,15 @@ describe("Replicate client", () => { { detail: "Too many requests", }, - { "Content-Type": "application/json", "Retry-After": "1" }, + { "Content-Type": "application/json", "Retry-After": "1" } ) .post("/predictions") .reply(201, { id: "ufawqhfynnddngldkgtslldrkq", }); const prediction = await client.predictions.create({ - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { text: "Alice", }, @@ -449,18 +483,19 @@ describe("Replicate client", () => { { detail: "Internal server error", }, - { "Content-Type": "application/json" }, + { "Content-Type": "application/json" } ); await expect( client.predictions.create({ - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", input: { text: "Alice", }, - }), + }) ).rejects.toThrow( - `Request to https://api.replicate.com/v1/predictions failed with status 500 Internal Server Error: {"detail":"Internal server error"}.`, + `Request to https://api.replicate.com/v1/predictions failed with status 500 Internal Server Error: {"detail":"Internal server error"}.` ); }); }); @@ -472,10 +507,12 @@ describe("Replicate client", () => { .reply(200, { id: "rrr4z55ocneqzikepnug6xezpe", model: "stability-ai/stable-diffusion", - version: "be04660a5b93ef2aff61e3668dedb4cbeb14941e62a3fd5998364a32d613e35e", + version: + "be04660a5b93ef2aff61e3668dedb4cbeb14941e62a3fd5998364a32d613e35e", urls: { get: "https://api.replicate.com/v1/predictions/rrr4z55ocneqzikepnug6xezpe", - cancel: "https://api.replicate.com/v1/predictions/rrr4z55ocneqzikepnug6xezpe/cancel", + cancel: + "https://api.replicate.com/v1/predictions/rrr4z55ocneqzikepnug6xezpe/cancel", }, created_at: "2022-09-13T22:54:18.578761Z", started_at: "2022-09-13T22:54:19.438525Z", @@ -494,7 +531,9 @@ describe("Replicate client", () => { predict_time: 4.484541, }, }); - const prediction = await client.predictions.get("rrr4z55ocneqzikepnug6xezpe"); + const prediction = await client.predictions.get( + "rrr4z55ocneqzikepnug6xezpe" + ); expect(prediction.id).toBe("rrr4z55ocneqzikepnug6xezpe"); }); @@ -506,14 +545,16 @@ describe("Replicate client", () => { { detail: "Too many requests", }, - { "Content-Type": "application/json", "Retry-After": "1" }, + { "Content-Type": "application/json", "Retry-After": "1" } ) .get("/predictions/rrr4z55ocneqzikepnug6xezpe") .reply(200, { id: "rrr4z55ocneqzikepnug6xezpe", }); - const prediction = await client.predictions.get("rrr4z55ocneqzikepnug6xezpe"); + const prediction = await client.predictions.get( + "rrr4z55ocneqzikepnug6xezpe" + ); expect(prediction.id).toBe("rrr4z55ocneqzikepnug6xezpe"); }); @@ -525,14 +566,16 @@ describe("Replicate client", () => { { detail: "Internal server error", }, - { "Content-Type": "application/json" }, + { "Content-Type": "application/json" } ) .get("/predictions/rrr4z55ocneqzikepnug6xezpe") .reply(200, { id: "rrr4z55ocneqzikepnug6xezpe", }); - const prediction = await client.predictions.get("rrr4z55ocneqzikepnug6xezpe"); + const prediction = await client.predictions.get( + "rrr4z55ocneqzikepnug6xezpe" + ); expect(prediction.id).toBe("rrr4z55ocneqzikepnug6xezpe"); }); }); @@ -544,10 +587,12 @@ describe("Replicate client", () => { .reply(200, { id: "ufawqhfynnddngldkgtslldrkq", model: "replicate/hello-world", - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", urls: { get: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq", - cancel: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", + cancel: + "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", }, created_at: "2022-04-26T22:13:06.224088Z", started_at: "2022-04-26T22:13:06.224088Z", @@ -562,7 +607,9 @@ describe("Replicate client", () => { metrics: {}, }); - const prediction = await client.predictions.cancel("ufawqhfynnddngldkgtslldrkq"); + const prediction = await client.predictions.cancel( + "ufawqhfynnddngldkgtslldrkq" + ); expect(prediction.status).toBe("canceled"); }); @@ -580,10 +627,12 @@ describe("Replicate client", () => { { id: "jpzd7hm5gfcapbfyt4mqytarku", model: "stability-ai/stable-diffusion", - version: "b21cbe271e65c1718f2999b038c18b45e21e4fba961181fbfae9342fc53b9e05", + version: + "b21cbe271e65c1718f2999b038c18b45e21e4fba961181fbfae9342fc53b9e05", urls: { get: "https://api.replicate.com/v1/predictions/jpzd7hm5gfcapbfyt4mqytarku", - cancel: "https://api.replicate.com/v1/predictions/jpzd7hm5gfcapbfyt4mqytarku/cancel", + cancel: + "https://api.replicate.com/v1/predictions/jpzd7hm5gfcapbfyt4mqytarku/cancel", }, created_at: "2022-04-26T20:00:40.658234Z", started_at: "2022-04-26T20:00:84.583803Z", @@ -606,7 +655,9 @@ describe("Replicate client", () => { results: [{ id: "ufawqhfynnddngldkgtslldrkq" }], next: "https://api.replicate.com/v1/predictions?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw", }) - .get("/predictions?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw") + .get( + "/predictions?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw" + ) .reply(200, { results: [{ id: "rrr4z55ocneqzikepnug6xezpe" }], next: null, @@ -616,7 +667,10 @@ describe("Replicate client", () => { for await (const batch of client.paginate(client.predictions.list)) { results.push(...batch); } - expect(results).toEqual([{ id: "ufawqhfynnddngldkgtslldrkq" }, { id: "rrr4z55ocneqzikepnug6xezpe" }]); + expect(results).toEqual([ + { id: "ufawqhfynnddngldkgtslldrkq" }, + { id: "rrr4z55ocneqzikepnug6xezpe" }, + ]); // Add more tests for error handling, edge cases, etc. }); @@ -625,10 +679,13 @@ describe("Replicate client", () => { describe("trainings.create", () => { test("Calls the correct API route with the correct payload", async () => { nock(BASE_URL) - .post("/models/owner/model/versions/632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532/trainings") + .post( + "/models/owner/model/versions/632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532/trainings" + ) .reply(200, { id: "zz4ibbonubfz7carwiefibzgga", - version: "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", + version: + "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", status: "starting", input: { text: "...", @@ -650,20 +707,25 @@ describe("Replicate client", () => { input: { text: "...", }, - }, + } ); expect(training.id).toBe("zz4ibbonubfz7carwiefibzgga"); }); test("Throws an error if webhook is not a valid URL", async () => { await expect( - client.trainings.create("owner", "model", "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", { - destination: "new_owner/new_model", - input: { - text: "...", - }, - webhook: "invalid-url", - }), + client.trainings.create( + "owner", + "model", + "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", + { + destination: "new_owner/new_model", + input: { + text: "...", + }, + webhook: "invalid-url", + } + ) ).rejects.toThrow("Invalid webhook URL"); }); @@ -723,7 +785,9 @@ describe("Replicate client", () => { completed_at: null, }); - const training = await client.trainings.cancel("zz4ibbonubfz7carwiefibzgga"); + const training = await client.trainings.cancel( + "zz4ibbonubfz7carwiefibzgga" + ); expect(training.status).toBe("canceled"); }); @@ -741,10 +805,12 @@ describe("Replicate client", () => { { id: "jpzd7hm5gfcapbfyt4mqytarku", model: "stability-ai/sdxl", - version: "b21cbe271e65c1718f2999b038c18b45e21e4fba961181fbfae9342fc53b9e05", + version: + "b21cbe271e65c1718f2999b038c18b45e21e4fba961181fbfae9342fc53b9e05", urls: { get: "https://api.replicate.com/v1/trainings/jpzd7hm5gfcapbfyt4mqytarku", - cancel: "https://api.replicate.com/v1/trainings/jpzd7hm5gfcapbfyt4mqytarku/cancel", + cancel: + "https://api.replicate.com/v1/trainings/jpzd7hm5gfcapbfyt4mqytarku/cancel", }, created_at: "2022-04-26T20:00:40.658234Z", started_at: "2022-04-26T20:00:84.583803Z", @@ -767,7 +833,9 @@ describe("Replicate client", () => { results: [{ id: "ufawqhfynnddngldkgtslldrkq" }], next: "https://api.replicate.com/v1/trainings?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw", }) - .get("/trainings?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw") + .get( + "/trainings?cursor=cD0yMDIyLTAxLTIxKzIzJTNBMTglM0EyNC41MzAzNTclMkIwMCUzQTAw" + ) .reply(200, { results: [{ id: "rrr4z55ocneqzikepnug6xezpe" }], next: null, @@ -777,7 +845,10 @@ describe("Replicate client", () => { for await (const batch of client.paginate(client.trainings.list)) { results.push(...batch); } - expect(results).toEqual([{ id: "ufawqhfynnddngldkgtslldrkq" }, { id: "rrr4z55ocneqzikepnug6xezpe" }]); + expect(results).toEqual([ + { id: "ufawqhfynnddngldkgtslldrkq" }, + { id: "rrr4z55ocneqzikepnug6xezpe" }, + ]); // Add more tests for error handling, edge cases, etc. }); @@ -790,10 +861,12 @@ describe("Replicate client", () => { .reply(200, { id: "mfrgcyzzme2wkmbwgzrgmntcg", model: "replicate/hello-world", - version: "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", urls: { get: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq", - cancel: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", + cancel: + "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", }, created_at: "2022-09-10T09:44:22.165836Z", started_at: null, @@ -807,13 +880,17 @@ describe("Replicate client", () => { logs: null, metrics: {}, }); - const prediction = await client.deployments.predictions.create("replicate", "greeter", { - input: { - text: "Alice", - }, - webhook: "http://test.host/webhook", - webhook_events_filter: ["output", "completed"], - }); + const prediction = await client.deployments.predictions.create( + "replicate", + "greeter", + { + input: { + text: "Alice", + }, + webhook: "http://test.host/webhook", + webhook_events_filter: ["output", "completed"], + } + ); expect(prediction.id).toBe("mfrgcyzzme2wkmbwgzrgmntcg"); }); // Add more tests for error handling, edge cases, etc. @@ -829,7 +906,8 @@ describe("Replicate client", () => { current_release: { number: 1, model: "stability-ai/sdxl", - version: "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", + version: + "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", created_at: "2024-02-15T16:32:57.018467Z", created_by: { type: "organization", @@ -845,7 +923,10 @@ describe("Replicate client", () => { }, }); - const deployment = await client.deployments.get("acme", "my-app-image-generator"); + const deployment = await client.deployments.get( + "acme", + "my-app-image-generator" + ); expect(deployment.owner).toBe("acme"); expect(deployment.name).toBe("my-app-image-generator"); @@ -864,7 +945,8 @@ describe("Replicate client", () => { current_release: { number: 1, model: "stability-ai/sdxl", - version: "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", + version: + "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", created_at: "2024-02-15T16:32:57.018467Z", created_by: { type: "organization", @@ -883,7 +965,8 @@ describe("Replicate client", () => { const deployment = await client.deployments.create({ name: "my-app-image-generator", model: "stability-ai/sdxl", - version: "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", + version: + "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", hardware: "gpu-t4", min_instances: 1, max_instances: 5, @@ -906,7 +989,8 @@ describe("Replicate client", () => { current_release: { number: 2, model: "stability-ai/sdxl", - version: "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", + version: + "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", created_at: "2024-02-16T08:14:22.345678Z", created_by: { type: "organization", @@ -922,18 +1006,25 @@ describe("Replicate client", () => { }, }); - const deployment = await client.deployments.update("acme", "my-app-image-generator", { - version: "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", - hardware: "gpu-a40-large", - min_instances: 3, - max_instances: 10, - }); + const deployment = await client.deployments.update( + "acme", + "my-app-image-generator", + { + version: + "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", + hardware: "gpu-a40-large", + min_instances: 3, + max_instances: 10, + } + ); expect(deployment.current_release.number).toBe(2); expect(deployment.current_release.version).toBe( - "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", + "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532" + ); + expect(deployment.current_release.configuration.hardware).toBe( + "gpu-a40-large" ); - expect(deployment.current_release.configuration.hardware).toBe("gpu-a40-large"); expect(deployment.current_release.configuration.min_instances).toBe(3); expect(deployment.current_release.configuration.max_instances).toBe(10); }); @@ -942,9 +1033,14 @@ describe("Replicate client", () => { describe("deployments.delete", () => { test("Calls the correct API route with the correct payload", async () => { - nock(BASE_URL).delete("/deployments/acme/my-app-image-generator").reply(204); + nock(BASE_URL) + .delete("/deployments/acme/my-app-image-generator") + .reply(204); - const success = await client.deployments.delete("acme", "my-app-image-generator"); + const success = await client.deployments.delete( + "acme", + "my-app-image-generator" + ); expect(success).toBe(true); }); }); @@ -977,7 +1073,7 @@ describe("Replicate client", () => { describe("predictions.create with model", () => { test("Calls the correct API route with the correct payload", async () => { nock(BASE_URL) - .post("/models/meta/llama-2-70b-chat/predictions") + .post("/models/meta/meta-llama-3-70b-instruct/predictions") .reply(200, { id: "heat2o3bzn3ahtr6bjfftvbaci", model: "replicate/lifeboat-70b", @@ -990,12 +1086,13 @@ describe("Replicate client", () => { status: "starting", created_at: "2023-11-27T13:35:45.99397566Z", urls: { - cancel: "https://api.replicate.com/v1/predictions/heat2o3bzn3ahtr6bjfftvbaci/cancel", + cancel: + "https://api.replicate.com/v1/predictions/heat2o3bzn3ahtr6bjfftvbaci/cancel", get: "https://api.replicate.com/v1/predictions/heat2o3bzn3ahtr6bjfftvbaci", }, }); const prediction = await client.predictions.create({ - model: "meta/llama-2-70b-chat", + model: "meta/meta-llama-3-70b-instruct", input: { prompt: "Please write a haiku about llamas.", }, @@ -1140,6 +1237,44 @@ describe("Replicate client", () => { }); }); + describe("models.search", () => { + test("Calls the correct API route with the correct payload", async () => { + nock(BASE_URL) + .intercept("/models", "QUERY") + .reply(200, { + results: [ + { + url: "https://replicate.com/meta/meta-llama-3-70b-instruct", + owner: "meta", + name: "meta-llama-3-70b-instruct", + description: + "Llama 2 is a collection of pretrained and fine-tuned generative text models ranging in scale from 7 billion to 70 billion parameters.", + visibility: "public", + github_url: null, + paper_url: + "https://ai.meta.com/research/publications/llama-2-open-foundation-and-fine-tuned-chat-models/", + license_url: "https://ai.meta.com/llama/license/", + run_count: 1000000, + cover_image_url: + "https://replicate.delivery/pbxt/IJqFrnAKEDiCBnlXyndzVVxkZvfQ7kLjGVEZZPXTRXxOOPkQA/llama2.png", + default_example: null, + latest_version: null, + }, + // ... more results ... + ], + next: null, + previous: null, + }); + + const searchResults = await client.models.search("llama"); + expect(searchResults.results.length).toBeGreaterThan(0); + expect(searchResults.results[0].owner).toBe("meta"); + expect(searchResults.results[0].name).toBe("meta-llama-3-70b-instruct"); + }); + + // Add more tests for error handling, edge cases, etc. + }); + describe("run", () => { test("Calls the correct API routes", async () => { nock(BASE_URL) @@ -1200,7 +1335,7 @@ describe("Replicate client", () => { (prediction) => { const progress = parseProgressFromLogs(prediction); callback(prediction, progress); - }, + } ); expect(output).toBe("Goodbye!"); @@ -1212,7 +1347,7 @@ describe("Replicate client", () => { status: "starting", logs: null, }, - null, + null ); expect(callback).toHaveBeenNthCalledWith( @@ -1226,7 +1361,7 @@ describe("Replicate client", () => { percentage: 0.4, current: 2, total: 5, - }, + } ); expect(callback).toHaveBeenNthCalledWith( @@ -1240,7 +1375,7 @@ describe("Replicate client", () => { percentage: 0.8, current: 4, total: 5, - }, + } ); expect(callback).toHaveBeenNthCalledWith( @@ -1255,7 +1390,7 @@ describe("Replicate client", () => { percentage: 1.0, current: 5, total: 5, - }, + } ); expect(callback).toHaveBeenCalledTimes(4); @@ -1289,7 +1424,7 @@ describe("Replicate client", () => { input: { text: "Hello, world!" }, wait: { interval: 1 }, }, - progress, + progress ); expect(output).toBe("Goodbye!"); @@ -1332,7 +1467,9 @@ describe("Replicate client", () => { output: "foobar", }); - await expect(client.run("a/b-1.0:abc123", { input: { text: "Hello, world!" } })).resolves.not.toThrow(); + await expect( + client.run("a/b-1.0:abc123", { input: { text: "Hello, world!" } }) + ).resolves.not.toThrow(); }); test("Throws an error for invalid identifiers", async () => { @@ -1349,12 +1486,15 @@ describe("Replicate client", () => { test("Throws an error if webhook URL is invalid", async () => { await expect(async () => { - await client.run("owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", { - input: { - text: "Alice", - }, - webhook: "invalid-url", - }); + await client.run( + "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + { + input: { + text: "Alice", + }, + webhook: "invalid-url", + } + ); }).rejects.toThrow("Invalid webhook URL"); }); @@ -1393,7 +1533,7 @@ describe("Replicate client", () => { input: { text: "Hello, world!" }, signal, }, - onProgress, + onProgress ); expect(body).toBeDefined(); @@ -1405,19 +1545,19 @@ describe("Replicate client", () => { 1, expect.objectContaining({ status: "processing", - }), + }) ); expect(onProgress).toHaveBeenNthCalledWith( 2, expect.objectContaining({ status: "processing", - }), + }) ); expect(onProgress).toHaveBeenNthCalledWith( 3, expect.objectContaining({ status: "canceled", - }), + }) ); scope.done(); @@ -1442,7 +1582,8 @@ describe("Replicate client", () => { "Content-Type": "application/json", "Webhook-ID": "msg_p5jXN8AQM9LWM0D4loKWxJek", "Webhook-Timestamp": "1614265330", - "Webhook-Signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=", + "Webhook-Signature": + "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=", }, body: `{"test": 2432232314}`, }); @@ -1485,7 +1626,7 @@ describe("Replicate client", () => { id: EVENT_2 data: {} - `.replace(/^[ ]+/gm, ""), + `.replace(/^[ ]+/gm, "") ); const iterator = stream[Symbol.asyncIterator](); @@ -1516,7 +1657,7 @@ describe("Replicate client", () => { id: EVENT_3 data: {} - `.replace(/^[ ]+/gm, ""), + `.replace(/^[ ]+/gm, "") ); const iterator = stream[Symbol.asyncIterator](); @@ -1550,7 +1691,7 @@ describe("Replicate client", () => { id: EVENT_2 data: {} - `.replace(/^[ ]+/gm, ""), + `.replace(/^[ ]+/gm, "") ); const iterator = stream[Symbol.asyncIterator](); @@ -1582,7 +1723,7 @@ describe("Replicate client", () => { id: EVENT_2 data: {} - `.replace(/^[ ]+/gm, ""), + `.replace(/^[ ]+/gm, "") ); const iterator = stream[Symbol.asyncIterator](); @@ -1703,7 +1844,7 @@ describe("Replicate client", () => { id: EVENT_1 data: hello world - `.replace(/^[ ]+/gm, ""), + `.replace(/^[ ]+/gm, "") ); const iterator = stream[Symbol.asyncIterator](); @@ -1725,7 +1866,7 @@ describe("Replicate client", () => { id: EVENT_2 data: An unexpected error occurred - `.replace(/^[ ]+/gm, ""), + `.replace(/^[ ]+/gm, "") ); const iterator = stream[Symbol.asyncIterator](); @@ -1733,7 +1874,9 @@ describe("Replicate client", () => { done: false, value: { event: "output", id: "EVENT_1", data: "hello world" }, }); - await expect(iterator.next()).rejects.toThrowError("An unexpected error occurred"); + await expect(iterator.next()).rejects.toThrowError( + "An unexpected error occurred" + ); expect(await iterator.next()).toEqual({ done: true }); }); @@ -1741,7 +1884,7 @@ describe("Replicate client", () => { const stream = createStream("{}", 500); const iterator = stream[Symbol.asyncIterator](); await expect(iterator.next()).rejects.toThrowError( - "Request to https://stream.replicate.com/fake_stream failed with status 500", + "Request to https://stream.replicate.com/fake_stream failed with status 500" ); expect(await iterator.next()).toEqual({ done: true }); }); diff --git a/lib/models.js b/lib/models.js index c6a02fca..272d9edb 100644 --- a/lib/models.js +++ b/lib/models.js @@ -89,9 +89,28 @@ async function createModel(model_owner, model_name, options) { return response.json(); } +/** + * Search for public models + * + * @param {string} query - The search query + * @returns {Promise} Resolves with a page of models matching the search query + */ +async function search(query) { + const response = await this.request("/models", { + method: "QUERY", + headers: { + "Content-Type": "text/plain", + }, + data: query, + }); + + return response.json(); +} + module.exports = { get: getModel, list: listModels, create: createModel, versions: { list: listModelVersions, get: getModelVersion }, + search, }; From ac75077c4b5e126798e9b8e820476ca58f7961a5 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 25 Jul 2024 12:20:51 -0700 Subject: [PATCH 21/68] 0.32.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c454426..cdea341f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "0.31.1", + "version": "0.32.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "0.31.1", + "version": "0.32.0", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index 7e13d359..ae45856e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "0.31.1", + "version": "0.32.0", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From 0cf57c4ca6de9913abdb6e9ebeef165438d41b5d Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Thu, 29 Aug 2024 06:52:31 -0600 Subject: [PATCH 22/68] Fix `validateWebhook` backwards incompatibility with older Node.js (#298) * Use backward compatible syntax for accessing object keys on requestData.headers * Stringifying requestData.body when it is of type object. This is backward compatibility for Next.js 12.1.x / Node.js v18 --- lib/util.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/util.js b/lib/util.js index b4483aed..a36c4804 100644 --- a/lib/util.js +++ b/lib/util.js @@ -35,9 +35,15 @@ async function validateWebhook(requestData, secret) { const signingSecret = secret || requestData.secret; if (requestData && requestData.headers && requestData.body) { - id = requestData.headers.get("webhook-id"); - timestamp = requestData.headers.get("webhook-timestamp"); - signature = requestData.headers.get("webhook-signature"); + id = + requestData.headers["webhook-id"] || + requestData.headers.get?.("webhook-id"); + timestamp = + requestData.headers["webhook-timestamp"] || + requestData.headers.get?.("webhook-timestamp"); + signature = + requestData.headers["webhook-signature"] || + requestData.headers.get?.("webhook-signature"); body = requestData.body; } @@ -49,6 +55,8 @@ async function validateWebhook(requestData, secret) { } } else if (isTypedArray(body)) { body = await new Blob([body]).text(); + } else if (typeof body === "object") { + body = JSON.stringify(body); } else if (typeof body !== "string") { throw new Error("Invalid body type"); } From 79ebc5776c272040443535a052673dcfe448477c Mon Sep 17 00:00:00 2001 From: Mattt Date: Thu, 29 Aug 2024 06:02:47 -0700 Subject: [PATCH 23/68] Handle Fetch Headers and subscriptable JS object separately (#300) --- lib/util.js | 58 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/lib/util.js b/lib/util.js index a36c4804..daecfd15 100644 --- a/lib/util.js +++ b/lib/util.js @@ -35,15 +35,17 @@ async function validateWebhook(requestData, secret) { const signingSecret = secret || requestData.secret; if (requestData && requestData.headers && requestData.body) { - id = - requestData.headers["webhook-id"] || - requestData.headers.get?.("webhook-id"); - timestamp = - requestData.headers["webhook-timestamp"] || - requestData.headers.get?.("webhook-timestamp"); - signature = - requestData.headers["webhook-signature"] || - requestData.headers.get?.("webhook-signature"); + if (typeof requestData.headers.get === "function") { + // Headers object (e.g. Fetch API Headers) + id = requestData.headers.get("webhook-id"); + timestamp = requestData.headers.get("webhook-timestamp"); + signature = requestData.headers.get("webhook-signature"); + } else { + // Plain object with header key-value pairs + id = requestData.headers["webhook-id"]; + timestamp = requestData.headers["webhook-timestamp"]; + signature = requestData.headers["webhook-signature"]; + } body = requestData.body; } @@ -75,11 +77,18 @@ async function validateWebhook(requestData, secret) { const signedContent = `${id}.${timestamp}.${body}`; - const computedSignature = await createHMACSHA256(signingSecret.split("_").pop(), signedContent); + const computedSignature = await createHMACSHA256( + signingSecret.split("_").pop(), + signedContent + ); - const expectedSignatures = signature.split(" ").map((sig) => sig.split(",")[1]); + const expectedSignatures = signature + .split(" ") + .map((sig) => sig.split(",")[1]); - return expectedSignatures.some((expectedSignature) => expectedSignature === computedSignature); + return expectedSignatures.some( + (expectedSignature) => expectedSignature === computedSignature + ); } /** @@ -106,9 +115,13 @@ async function createHMACSHA256(secret, data) { crypto = require.call(null, "node:crypto").webcrypto; } - const key = await crypto.subtle.importKey("raw", base64ToBytes(secret), { name: "HMAC", hash: "SHA-256" }, false, [ - "sign", - ]); + const key = await crypto.subtle.importKey( + "raw", + base64ToBytes(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"] + ); const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data)); return bytesToBase64(signature); @@ -232,7 +245,11 @@ async function transformFileInputs(client, inputs, strategy) { try { return await transformFileInputsToReplicateFileURLs(client, inputs); } catch (error) { - if (error instanceof ApiError && error.response.status >= 400 && error.response.status < 500) { + if ( + error instanceof ApiError && + error.response.status >= 400 && + error.response.status < 500 + ) { throw error; } return await transformFileInputsToBase64EncodedDataURIs(inputs); @@ -296,7 +313,7 @@ async function transformFileInputsToBase64EncodedDataURIs(inputs) { totalBytes += buffer.byteLength; if (totalBytes > MAX_DATA_URI_SIZE) { throw new Error( - `Combined filesize of prediction ${totalBytes} bytes exceeds 10mb limit for inline encoding, please provide URLs instead`, + `Combined filesize of prediction ${totalBytes} bytes exceeds 10mb limit for inline encoding, please provide URLs instead` ); } @@ -354,11 +371,14 @@ function isPlainObject(value) { if (proto === null) { return true; } - const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor; + const Ctor = + Object.prototype.hasOwnProperty.call(proto, "constructor") && + proto.constructor; return ( typeof Ctor === "function" && Ctor instanceof Ctor && - Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object) + Function.prototype.toString.call(Ctor) === + Function.prototype.toString.call(Object) ); } From e7a1aacb45457ed3439c2e59308904670a998d47 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 29 Aug 2024 06:03:10 -0700 Subject: [PATCH 24/68] 0.32.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cdea341f..b134381d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "0.32.0", + "version": "0.32.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "0.32.0", + "version": "0.32.1", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index ae45856e..a814ae34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "0.32.0", + "version": "0.32.1", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From 181c512a7455de9a21cb696af902c8c081da9089 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Wed, 11 Sep 2024 11:39:27 +0100 Subject: [PATCH 25/68] Add new FileOutput type to stream.js This takes a url and creates a `ReadableStream` that has two additional methods `url()` and `blob()` for easily working with remote URLs or base64 encoded data. --- lib/stream.js | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/lib/stream.js b/lib/stream.js index 2e0bbde3..875e0202 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -98,7 +98,61 @@ function createReadableStream({ url, fetch, options = {} }) { }); } +function createFileOutput({ url, fetch }) { + let type = "application/octet-stream"; + + class FileOutput extends ReadableStream { + async blob() { + const chunks = []; + for await (const chunk of this) { + chunks.push(chunk); + } + return new Blob(chunks, { type }); + } + + url() { + return new URL(url); + } + + toString() { + return url; + } + } + + return new FileOutput({ + async start(controller) { + const response = await fetch(url); + + if (!response.ok) { + const text = await response.text(); + const request = new Request(url, init); + controller.error( + new ApiError( + `Request to ${url} failed with status ${response.status}: ${text}`, + request, + response + ) + ); + } + + if (response.headers.get("Content-Type")) { + type = response.headers.get("Content-Type"); + } + + try { + for await (const chunk of streamAsyncIterator(response.body)) { + controller.enqueue(chunk); + } + controller.close(); + } catch (err) { + controller.error(err); + } + }, + }); +} + module.exports = { + createFileOutput, createReadableStream, ServerSentEvent, }; From 19d606d779e2dc7eeba11d39752f752039b4823d Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Wed, 11 Sep 2024 11:39:48 +0100 Subject: [PATCH 26/68] Export `transform` function from utils.js --- lib/util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/util.js b/lib/util.js index daecfd15..27afd46a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -452,6 +452,7 @@ async function* streamAsyncIterator(stream) { } module.exports = { + transform, transformFileInputs, validateWebhook, withAutomaticRetries, From 0e609c468bbb5a0305ba01259b32f4cf2b32991d Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Wed, 11 Sep 2024 11:42:42 +0100 Subject: [PATCH 27/68] Return FileOutput from `run()` function This is currently behind the `useFileOutput` flag provided to the Replicate constructor. This allows us to test the feature before rolling it out more widely. When enabled any URLs or data-uris will be converted into a FileOutput type. This is essentially a `ReadableStream` that has two additional methods `url()` to return the underlying URL and `blob()` which will return a `Blob()` object with the file data loaded into memory. The intention here is to make it easier to work with file outputs and allows us to optimize the delivery of file assets to the client in future iterations. --- index.d.ts | 7 ++ index.js | 17 ++++- index.test.ts | 199 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 220 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 25cd51af..4801654d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,6 +8,12 @@ declare module "replicate" { response: Response; } + export interface FileOutput extends ReadableStream { + blob(): Promise; + url(): URL; + toString(): string; + } + export interface Account { type: "user" | "organization"; username: string; @@ -137,6 +143,7 @@ declare module "replicate" { init?: RequestInit ) => Promise; fileEncodingStrategy?: FileEncodingStrategy; + useFileOutput?: boolean; }); auth: string; diff --git a/index.js b/index.js index 6b35db65..4b45d259 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,8 @@ const ApiError = require("./lib/error"); const ModelVersionIdentifier = require("./lib/identifier"); -const { createReadableStream } = require("./lib/stream"); +const { createReadableStream, createFileOutput } = require("./lib/stream"); const { + transform, withAutomaticRetries, validateWebhook, parseProgressFromLogs, @@ -47,6 +48,7 @@ class Replicate { * @param {string} options.userAgent - Identifier of your app * @param {string} [options.baseUrl] - Defaults to https://api.replicate.com/v1 * @param {Function} [options.fetch] - Fetch function to use. Defaults to `globalThis.fetch` + * @param {boolean} [options.useFileOutput] - Set to `true` to return `FileOutput` objects from `run` instead of URLs, defaults to false. * @param {"default" | "upload" | "data-uri"} [options.fileEncodingStrategy] - Determines the file encoding strategy to use */ constructor(options = {}) { @@ -58,6 +60,7 @@ class Replicate { this.baseUrl = options.baseUrl || "https://api.replicate.com/v1"; this.fetch = options.fetch || globalThis.fetch; this.fileEncodingStrategy = options.fileEncodingStrategy ?? "default"; + this.useFileOutput = options.useFileOutput ?? false; this.accounts = { current: accounts.current.bind(this), @@ -196,7 +199,17 @@ class Replicate { throw new Error(`Prediction failed: ${prediction.error}`); } - return prediction.output; + return transform(prediction.output, (value) => { + if ( + typeof value === "string" && + (value.startsWith("https:") || value.startsWith("data:")) + ) { + return this.useFileOutput + ? createFileOutput({ url: value, fetch: this.fetch }) + : value; + } + return value; + }); } /** diff --git a/index.test.ts b/index.test.ts index 7f9fcf2f..5ca3e542 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1,13 +1,13 @@ import { expect, jest, test } from "@jest/globals"; import Replicate, { ApiError, + FileOutput, Model, Prediction, validateWebhook, parseProgressFromLogs, } from "replicate"; import nock from "nock"; -import { Readable } from "node:stream"; import { createReadableStream } from "./lib/stream"; let client: Replicate; @@ -1562,6 +1562,203 @@ describe("Replicate client", () => { scope.done(); }); + + test("returns FileOutput for URLs when useFileOutput is true", async () => { + client = new Replicate({ auth: "foo", useFileOutput: true }); + + nock(BASE_URL) + .post("/predictions") + .reply(201, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "starting", + logs: null, + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "succeeded", + output: "https://example.com", + logs: [].join("\n"), + }); + + nock("https://example.com") + .get("/") + .reply(200, "hello world", { "Content-Type": "text/plain" }); + + const output = (await client.run( + "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + { + input: { text: "Hello, world!" }, + } + )) as FileOutput; + + expect(output).toBeInstanceOf(ReadableStream); + expect(output.url()).toEqual(new URL("https://example.com")); + + const blob = await output.blob(); + expect(blob.type).toEqual("text/plain"); + expect(blob.arrayBuffer()).toEqual( + new Blob(["Hello, world!"]).arrayBuffer() + ); + }); + + test("returns FileOutput for URLs when useFileOutput is true - acts like string", async () => { + client = new Replicate({ auth: "foo", useFileOutput: true }); + + nock(BASE_URL) + .post("/predictions") + .reply(201, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "starting", + logs: null, + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "succeeded", + output: "https://example.com", + logs: [].join("\n"), + }); + + nock("https://example.com") + .get("/") + .reply(200, "hello world", { "Content-Type": "text/plain" }); + + const output = (await client.run( + "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + { + input: { text: "Hello, world!" }, + } + )) as unknown as string; + + expect(fetch(output).then((r) => r.text())).resolves.toEqual( + "hello world" + ); + }); + + test("returns FileOutput for URLs when useFileOutput is true - array output", async () => { + client = new Replicate({ auth: "foo", useFileOutput: true }); + + nock(BASE_URL) + .post("/predictions") + .reply(201, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "starting", + logs: null, + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "succeeded", + output: ["https://example.com"], + logs: [].join("\n"), + }); + + nock("https://example.com") + .get("/") + .reply(200, "hello world", { "Content-Type": "text/plain" }); + + const [output] = (await client.run( + "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + { + input: { text: "Hello, world!" }, + } + )) as FileOutput[]; + + expect(output).toBeInstanceOf(ReadableStream); + expect(output.url()).toEqual(new URL("https://example.com")); + + const blob = await output.blob(); + expect(blob.type).toEqual("text/plain"); + expect(blob.arrayBuffer()).toEqual( + new Blob(["Hello, world!"]).arrayBuffer() + ); + }); + + test("returns FileOutput for URLs when useFileOutput is true - data uri", async () => { + client = new Replicate({ auth: "foo", useFileOutput: true }); + + nock(BASE_URL) + .post("/predictions") + .reply(201, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "starting", + logs: null, + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "succeeded", + output: "data:text/plain;base64,SGVsbG8sIHdvcmxkIQ==", + logs: [].join("\n"), + }); + + const output = (await client.run( + "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + { + input: { text: "Hello, world!" }, + } + )) as FileOutput; + + expect(output).toBeInstanceOf(ReadableStream); + expect(output.url()).toEqual( + new URL("data:text/plain;base64,SGVsbG8sIHdvcmxkIQ==") + ); + + const blob = await output.blob(); + expect(blob.type).toEqual("text/plain"); + expect(blob.arrayBuffer()).toEqual( + new Blob(["Hello, world!"]).arrayBuffer() + ); + }); }); describe("webhooks.default.secret.get", () => { From 3380bbe24d6f6b74069767867e1abbb1b1c5fe36 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Wed, 11 Sep 2024 11:45:47 -0700 Subject: [PATCH 28/68] Add documentation comments to createFileOutput declaration --- lib/stream.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/stream.js b/lib/stream.js index 875e0202..2f72e2c4 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -98,6 +98,15 @@ function createReadableStream({ url, fetch, options = {} }) { }); } +/** + * Create a new readable stream for an output file + * created by running a Replicate model. + * + * @param {object} config + * @param {string} config.url The URL to connect to. + * @param {typeof fetch} [config.fetch] The URL to connect to. + * @returns {ReadableStream} + */ function createFileOutput({ url, fetch }) { let type = "application/octet-stream"; From 445f2abe5f5eb5222b8ee77d715e3143ad506a1b Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Wed, 11 Sep 2024 11:48:39 -0700 Subject: [PATCH 29/68] Replace use of ?? with || --- index.js | 4 ++-- lib/util.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 4b45d259..f2c3e1ea 100644 --- a/index.js +++ b/index.js @@ -59,8 +59,8 @@ class Replicate { options.userAgent || `replicate-javascript/${packageJSON.version}`; this.baseUrl = options.baseUrl || "https://api.replicate.com/v1"; this.fetch = options.fetch || globalThis.fetch; - this.fileEncodingStrategy = options.fileEncodingStrategy ?? "default"; - this.useFileOutput = options.useFileOutput ?? false; + this.fileEncodingStrategy = options.fileEncodingStrategy || "default"; + this.useFileOutput = options.useFileOutput || false; this.accounts = { current: accounts.current.bind(this), diff --git a/lib/util.js b/lib/util.js index 27afd46a..bd3c31e9 100644 --- a/lib/util.js +++ b/lib/util.js @@ -318,7 +318,7 @@ async function transformFileInputsToBase64EncodedDataURIs(inputs) { } const data = bytesToBase64(buffer); - mime = mime ?? "application/octet-stream"; + mime = mime || "application/octet-stream"; return `data:${mime};base64,${data}`; }); From eb9adfd925b18875f075d08ca0f039bf35183ebd Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Mon, 16 Sep 2024 00:31:06 -0700 Subject: [PATCH 30/68] 0.33.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b134381d..7e70966f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "0.32.1", + "version": "0.33.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "0.32.1", + "version": "0.33.0", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index a814ae34..77ae3d42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "0.32.1", + "version": "0.33.0", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From ac47fb43daeda6e52358105f93fdb3621c3913b8 Mon Sep 17 00:00:00 2001 From: Mattt Date: Mon, 23 Sep 2024 04:04:56 -0700 Subject: [PATCH 31/68] Deprecate stream parameter for predictions.create (#291) --- index.d.ts | 1 + index.js | 2 -- index.test.ts | 20 -------------------- lib/predictions.js | 6 ++---- 4 files changed, 3 insertions(+), 26 deletions(-) diff --git a/index.d.ts b/index.d.ts index 4801654d..78457e08 100644 --- a/index.d.ts +++ b/index.d.ts @@ -297,6 +297,7 @@ declare module "replicate" { model?: string; version?: string; input: object; + /** @deprecated */ stream?: boolean; webhook?: string; webhook_events_filter?: WebhookEventType[]; diff --git a/index.js b/index.js index f2c3e1ea..f0d3e757 100644 --- a/index.js +++ b/index.js @@ -318,13 +318,11 @@ class Replicate { prediction = await this.predictions.create({ ...data, version: identifier.version, - stream: true, }); } else if (identifier.owner && identifier.name) { prediction = await this.predictions.create({ ...data, model: `${identifier.owner}/${identifier.name}`, - stream: true, }); } else { throw new Error("Invalid model version identifier"); diff --git a/index.test.ts b/index.test.ts index 5ca3e542..08fbf7c8 100644 --- a/index.test.ts +++ b/index.test.ts @@ -357,7 +357,6 @@ describe("Replicate client", () => { prompt: "Tell me a story", data, }, - stream: true, }); expect(actual?.input.data).toEqual(expected); @@ -385,7 +384,6 @@ describe("Replicate client", () => { prompt: "Tell me a story", data, }, - stream: true, }); }).rejects.toThrowError( expect.objectContaining({ @@ -396,24 +394,6 @@ describe("Replicate client", () => { } ); - test("Passes stream parameter to API endpoint", async () => { - nock(BASE_URL) - .post("/predictions") - .reply(201, (_uri, body) => { - expect((body as any).stream).toBe(true); - return body; - }); - - await client.predictions.create({ - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", - input: { - prompt: "Tell me a story", - }, - stream: true, - }); - }); - test("Throws an error if webhook URL is invalid", async () => { await expect(async () => { await client.predictions.create({ diff --git a/lib/predictions.js b/lib/predictions.js index c290d40f..88bed328 100644 --- a/lib/predictions.js +++ b/lib/predictions.js @@ -9,11 +9,11 @@ const { transformFileInputs } = require("./util"); * @param {object} options.input - Required. An object with the model inputs * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the prediction has new output * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) - * @param {boolean} [options.stream] - Whether to stream the prediction output. Defaults to false + * @param {boolean} [options.stream] - Whether to stream the prediction output. Defaults to false. Streaming is now enabled by default for all predictions. For more information, see https://replicate.com/changelog/2024-07-15-streams-always-available-stream-parameter-deprecated * @returns {Promise} Resolves with the created prediction */ async function createPrediction(options) { - const { model, version, stream, input, ...data } = options; + const { model, version, input, ...data } = options; if (data.webhook) { try { @@ -36,7 +36,6 @@ async function createPrediction(options) { this.fileEncodingStrategy ), version, - stream, }, }); } else if (model) { @@ -49,7 +48,6 @@ async function createPrediction(options) { input, this.fileEncodingStrategy ), - stream, }, }); } else { From 02b06d4057c67011909424455489a294452e0384 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 06:10:09 -0700 Subject: [PATCH 32/68] Bump actions/download-artifact from 3 to 4.1.7 in /.github/workflows (#302) * Bump actions/download-artifact from 3 to 4.1.7 in /.github/workflows Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.1.7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v3...v4.1.7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Update actions/upload-artifact to v4 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mattt Zmuda --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27a982cb..b951d2ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: id: pack run: | echo "tarball-name=$(npm --loglevel error pack)" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: package-tarball path: ${{ steps.pack.outputs.tarball-name }} @@ -74,7 +74,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.7 with: name: package-tarball - name: Use Node.js ${{ matrix.node-version }} @@ -103,7 +103,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.7 with: name: package-tarball - name: Use Node.js ${{ matrix.node-version }} @@ -133,7 +133,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.7 with: name: package-tarball - name: Use Node.js ${{ matrix.node-version }} @@ -161,7 +161,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.7 with: name: package-tarball - name: Use Bun ${{ matrix.bun-version }} @@ -191,7 +191,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.7 with: name: package-tarball - name: Use Deno ${{ matrix.deno-version }} @@ -218,7 +218,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.7 with: name: package-tarball - name: Use Node.js From 3c7003a7144f874f209283ff4a0ed8bf5e7f199d Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Tue, 24 Sep 2024 13:12:44 -0700 Subject: [PATCH 33/68] document typescript usage tidy up example Co-Authored-By: Charlie Gleason --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index dabee2ee..f42b4265 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,32 @@ const output = await replicate.run(model, { input }); > upload the file to your own storage provider > and pass a publicly accessible URL. +## TypeScript usage + +This library exports TypeScript definitions. You can import them like this: + +```ts +import Replicate, { Prediction } from 'replicate'; +``` + +Here's an example that uses the `Prediction` type with a custom `onProgress` callback: + +```ts +import Replicate, { Prediction } from 'replicate'; + +const replicate = new Replicate(); +const model = "black-forest-labs/flux-schnell"; +const prompt = "a 19th century portrait of a raccoon gentleman wearing a suit"; +const onProgress = (prediction: Prediction) => { + console.log({ prediction }); +}; + +const output = await replicate.run(model, { input: { prompt } }, onProgress) +console.log({ output }) +``` + +See the full list of exported types in [index.d.ts](./index.d.ts). + ### Webhooks Webhooks provide real-time updates about your prediction. Specify an endpoint when you create a prediction, and Replicate will send HTTP POST requests to that URL when the prediction is created, updated, and finished. From d5af4d0b19f527541ca249e21d5b2f7ad684a9e9 Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Tue, 24 Sep 2024 15:20:29 -0700 Subject: [PATCH 34/68] Update README.md Co-authored-by: F --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f42b4265..247f7410 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ const output = await replicate.run(model, { input }); This library exports TypeScript definitions. You can import them like this: ```ts -import Replicate, { Prediction } from 'replicate'; +import Replicate, { type Prediction } from 'replicate'; ``` Here's an example that uses the `Prediction` type with a custom `onProgress` callback: From 13f1bf35bc7b8942028211ac8a105e20c4d1209a Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Tue, 24 Sep 2024 15:20:38 -0700 Subject: [PATCH 35/68] Update README.md Co-authored-by: F --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 247f7410..b6b8f489 100644 --- a/README.md +++ b/README.md @@ -128,9 +128,9 @@ import Replicate, { Prediction } from 'replicate'; const replicate = new Replicate(); const model = "black-forest-labs/flux-schnell"; const prompt = "a 19th century portrait of a raccoon gentleman wearing a suit"; -const onProgress = (prediction: Prediction) => { +function onProgress(prediction: Prediction) { console.log({ prediction }); -}; +} const output = await replicate.run(model, { input: { prompt } }, onProgress) console.log({ output }) From af9d6e2320821468aa8dade0adb1d68be134ba96 Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Tue, 24 Sep 2024 15:20:45 -0700 Subject: [PATCH 36/68] Update README.md Co-authored-by: F --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b6b8f489..d65fe06d 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ import Replicate, { type Prediction } from 'replicate'; Here's an example that uses the `Prediction` type with a custom `onProgress` callback: ```ts -import Replicate, { Prediction } from 'replicate'; +import Replicate, { type Prediction } from 'replicate'; const replicate = new Replicate(); const model = "black-forest-labs/flux-schnell"; From 7f02a64598fe374e16753a3ce2d743ae806bbbc3 Mon Sep 17 00:00:00 2001 From: Mattt Date: Wed, 25 Sep 2024 11:29:47 -0700 Subject: [PATCH 37/68] Extend support for `useFileOutput` to `stream` (#309) * Break stream on done event without enqueuing it * Fix doc comment for fetch parameter * Transform URLs in streaming responses when useFileOutput is enabled * Revert 'Break stream on done event without enqueuing it' * Default to useFileOutput = true for readable streams --- lib/stream.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/stream.js b/lib/stream.js index 2f72e2c4..2c899bd9 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -48,15 +48,18 @@ class ServerSentEvent { * @param {string} config.url The URL to connect to. * @param {typeof fetch} [config.fetch] The URL to connect to. * @param {object} [config.options] The EventSource options. + * @param {boolean} [config.options.useFileOutput] Whether to use the file output stream. * @returns {ReadableStream & AsyncIterable} */ function createReadableStream({ url, fetch, options = {} }) { + const { useFileOutput = true, headers = {}, ...initOptions } = options; + return new ReadableStream({ async start(controller) { const init = { - ...options, + ...initOptions, headers: { - ...options.headers, + ...headers, Accept: "text/event-stream", }, }; @@ -84,9 +87,15 @@ function createReadableStream({ url, fetch, options = {} }) { break; } - controller.enqueue( - new ServerSentEvent(event.event, event.data, event.id) - ); + let data = event.data; + if ( + useFileOutput && + typeof data === "string" && + (data.startsWith("https:") || data.startsWith("data:")) + ) { + data = createFileOutput({ data, fetch }); + } + controller.enqueue(new ServerSentEvent(event.event, data, event.id)); if (event.event === "done") { break; @@ -104,7 +113,7 @@ function createReadableStream({ url, fetch, options = {} }) { * * @param {object} config * @param {string} config.url The URL to connect to. - * @param {typeof fetch} [config.fetch] The URL to connect to. + * @param {typeof fetch} [config.fetch] The fetch function. * @returns {ReadableStream} */ function createFileOutput({ url, fetch }) { From 173b31da148096e3ddfd7252564c68ac21c1bd99 Mon Sep 17 00:00:00 2001 From: Mattt Date: Wed, 25 Sep 2024 11:35:40 -0700 Subject: [PATCH 38/68] Add `wait` parameter to prediction creation methods (#308) * Add block parameter to prediction creation methods * Add support for block: or wait: true to run * Remove unused stream parameter * Apply suggestions from code review Co-authored-by: Aron Carroll * Replace X-Sync header with Prefer: wait * Rename block to wait Update type definitions to capture expectations of wait parameter in run * Normalize wait value to nonzero integer --------- Co-authored-by: Aron Carroll --- index.d.ts | 6 +++++- index.js | 4 +++- lib/deployments.js | 11 ++++++++--- lib/predictions.js | 16 ++++++++++++++-- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index 78457e08..eabcc9b3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -156,7 +156,7 @@ declare module "replicate" { identifier: `${string}/${string}` | `${string}/${string}:${string}`, options: { input: object; - wait?: { interval?: number }; + wait?: boolean | number | { mode?: "poll"; interval?: number }; webhook?: string; webhook_events_filter?: WebhookEventType[]; signal?: AbortSignal; @@ -189,6 +189,7 @@ declare module "replicate" { wait( prediction: Prediction, options?: { + mode?: "poll"; interval?: number; }, stop?: (prediction: Prediction) => Promise @@ -210,9 +211,11 @@ declare module "replicate" { deployment_name: string, options: { input: object; + /** @deprecated */ stream?: boolean; webhook?: string; webhook_events_filter?: WebhookEventType[]; + block?: boolean; } ): Promise; }; @@ -301,6 +304,7 @@ declare module "replicate" { stream?: boolean; webhook?: string; webhook_events_filter?: WebhookEventType[]; + block?: boolean; } & ({ version: string } | { model: string }) ): Promise; get(prediction_id: string): Promise; diff --git a/index.js b/index.js index f0d3e757..712bc59b 100644 --- a/index.js +++ b/index.js @@ -133,7 +133,7 @@ class Replicate { * @param {string} ref - Required. The model version identifier in the format "owner/name" or "owner/name:version" * @param {object} options * @param {object} options.input - Required. An object with the model inputs - * @param {object} [options.wait] - Options for waiting for the prediction to finish + * @param {object} [options.wait] - Options for waiting for the prediction to finish. If `wait` is explicitly true, the function will block and wait for the prediction to finish. * @param {number} [options.wait.interval] - Polling interval in milliseconds. Defaults to 500 * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the prediction has new output * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) @@ -153,11 +153,13 @@ class Replicate { prediction = await this.predictions.create({ ...data, version: identifier.version, + wait: wait, }); } else if (identifier.owner && identifier.name) { prediction = await this.predictions.create({ ...data, model: `${identifier.owner}/${identifier.name}`, + wait: wait, }); } else { throw new Error("Invalid model version identifier"); diff --git a/lib/deployments.js b/lib/deployments.js index 56ed2406..6cab2613 100644 --- a/lib/deployments.js +++ b/lib/deployments.js @@ -7,13 +7,13 @@ const { transformFileInputs } = require("./util"); * @param {string} deployment_name - Required. The name of the deployment * @param {object} options * @param {object} options.input - Required. An object with the model inputs - * @param {boolean} [options.stream] - Whether to stream the prediction output. Defaults to false * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the prediction has new output * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) + * @param {boolean} [options.block] - Whether to wait until the prediction is completed before returning. Defaults to false * @returns {Promise} Resolves with the created prediction data */ async function createPrediction(deployment_owner, deployment_name, options) { - const { stream, input, ...data } = options; + const { input, block, ...data } = options; if (data.webhook) { try { @@ -24,10 +24,16 @@ async function createPrediction(deployment_owner, deployment_name, options) { } } + const headers = {}; + if (block) { + headers["Prefer"] = "wait"; + } + const response = await this.request( `/deployments/${deployment_owner}/${deployment_name}/predictions`, { method: "POST", + headers, data: { ...data, input: await transformFileInputs( @@ -35,7 +41,6 @@ async function createPrediction(deployment_owner, deployment_name, options) { input, this.fileEncodingStrategy ), - stream, }, } ); diff --git a/lib/predictions.js b/lib/predictions.js index 88bed328..f8e1c5ac 100644 --- a/lib/predictions.js +++ b/lib/predictions.js @@ -9,11 +9,11 @@ const { transformFileInputs } = require("./util"); * @param {object} options.input - Required. An object with the model inputs * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the prediction has new output * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) - * @param {boolean} [options.stream] - Whether to stream the prediction output. Defaults to false. Streaming is now enabled by default for all predictions. For more information, see https://replicate.com/changelog/2024-07-15-streams-always-available-stream-parameter-deprecated + * @param {boolean|integer} [options.wait] - Whether to wait until the prediction is completed before returning. If an integer is provided, it will wait for that many seconds. Defaults to false * @returns {Promise} Resolves with the created prediction */ async function createPrediction(options) { - const { model, version, input, ...data } = options; + const { model, version, input, wait, ...data } = options; if (data.webhook) { try { @@ -24,10 +24,21 @@ async function createPrediction(options) { } } + const headers = {}; + if (wait) { + if (typeof wait === "number") { + const n = Math.max(1, Math.ceil(Number(wait)) || 1); + headers["Prefer"] = `wait=${n}`; + } else { + headers["Prefer"] = "wait"; + } + } + let response; if (version) { response = await this.request("/predictions", { method: "POST", + headers, data: { ...data, input: await transformFileInputs( @@ -41,6 +52,7 @@ async function createPrediction(options) { } else if (model) { response = await this.request(`/models/${model}/predictions`, { method: "POST", + headers, data: { ...data, input: await transformFileInputs( From 4cba156dc063a7ab66eff5db484c4244116dd83c Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Wed, 25 Sep 2024 11:36:11 -0700 Subject: [PATCH 39/68] 0.34.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e70966f..2c3d24e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "0.33.0", + "version": "0.34.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "0.33.0", + "version": "0.34.0", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index 77ae3d42..f6c18183 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "0.33.0", + "version": "0.34.0", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From be0f32318a64edbca77351d864b67a24045d99fe Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Fri, 27 Sep 2024 10:06:43 -0700 Subject: [PATCH 40/68] docs: create predictions for official models (#312) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d65fe06d..c644e75e 100644 --- a/README.md +++ b/README.md @@ -615,8 +615,9 @@ const response = await replicate.predictions.create(options); | name | type | description | | ------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `options.version` | string | **Required**. The model version | | `options.input` | object | **Required**. An object with the model's inputs | +| `options.model` | string | The name of the model, e.g. `black-forest-labs/flux-schnell`. This is required if you're running an [official model](https://replicate.com/explore#official-models). | +| `options.version` | string | The 64-character [model version id](https://replicate.com/docs/topics/models/versions), e.g. `80537f9eead1a5bfa72d5ac6ea6414379be41d4d4f6679fd776e9535d1eb58bb`. This is required if you're not specifying a `model`. | | `options.stream` | boolean | Requests a URL for streaming output output | | `options.webhook` | string | An HTTPS URL for receiving a webhook when the prediction has new output | | `options.webhook_events_filter` | string[] | You can change which events trigger webhook requests by specifying webhook events (`start` \| `output` \| `logs` \| `completed`) | From c1a12b0f60122442c4115c23cc964fa96f2042fc Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Tue, 1 Oct 2024 23:44:15 +0100 Subject: [PATCH 41/68] Bug fixes for the `wait` option in `replicate.run` (#315) There were a couple of small bugs in the current implementation: 1. We would pass non-boolean, non-integer values through to `predictions.create` when it was an object with an interval, resulting in the blocking mode being used accidentally. 2. We would pass the boolean/integer values through to `wait` which would create a runtime error when the `wait` function expects an object. 3. We continued to poll for the prediction despite the blocking response returning the output data. This PR addresses these three issues by checking if the run should be blocking and passing the correct arguments in the correct places. We also assume that if the returned prediction is not in `starting` state then it is completed. This isn't ideal but works for the moment. Lastly, in the case where the blocking request times out the client will fall back to polling at the default interval. --- index.d.ts | 10 +++++----- index.js | 40 ++++++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/index.d.ts b/index.d.ts index eabcc9b3..45a24301 100644 --- a/index.d.ts +++ b/index.d.ts @@ -93,9 +93,9 @@ declare module "replicate" { model: string; version: string; input: object; - output?: any; + output?: any; // TODO: this should be `unknown` source: "api" | "web"; - error?: any; + error?: unknown; logs?: string; metrics?: { predict_time?: number; @@ -156,7 +156,7 @@ declare module "replicate" { identifier: `${string}/${string}` | `${string}/${string}:${string}`, options: { input: object; - wait?: boolean | number | { mode?: "poll"; interval?: number }; + wait?: boolean | number | { interval?: number }; webhook?: string; webhook_events_filter?: WebhookEventType[]; signal?: AbortSignal; @@ -215,7 +215,7 @@ declare module "replicate" { stream?: boolean; webhook?: string; webhook_events_filter?: WebhookEventType[]; - block?: boolean; + wait?: boolean | number | { mode?: "poll"; interval?: number }; } ): Promise; }; @@ -304,7 +304,7 @@ declare module "replicate" { stream?: boolean; webhook?: string; webhook_events_filter?: WebhookEventType[]; - block?: boolean; + wait?: boolean | number | { mode?: "poll"; interval?: number }; } & ({ version: string } | { model: string }) ): Promise; get(prediction_id: string): Promise; diff --git a/index.js b/index.js index 712bc59b..ac4b815d 100644 --- a/index.js +++ b/index.js @@ -147,19 +147,20 @@ class Replicate { const { wait, signal, ...data } = options; const identifier = ModelVersionIdentifier.parse(ref); + const isBlocking = typeof wait === "boolean" || typeof wait === "number"; let prediction; if (identifier.version) { prediction = await this.predictions.create({ ...data, version: identifier.version, - wait: wait, + wait: isBlocking ? wait : false, }); } else if (identifier.owner && identifier.name) { prediction = await this.predictions.create({ ...data, model: `${identifier.owner}/${identifier.name}`, - wait: wait, + wait: isBlocking ? wait : false, }); } else { throw new Error("Invalid model version identifier"); @@ -170,23 +171,26 @@ class Replicate { progress(prediction); } - prediction = await this.wait( - prediction, - wait || {}, - async (updatedPrediction) => { - // Call progress callback with the updated prediction object - if (progress) { - progress(updatedPrediction); + const isDone = isBlocking && prediction.status !== "starting"; + if (!isDone) { + prediction = await this.wait( + prediction, + isBlocking ? {} : wait, + async (updatedPrediction) => { + // Call progress callback with the updated prediction object + if (progress) { + progress(updatedPrediction); + } + + // We handle the cancel later in the function. + if (signal && signal.aborted) { + return true; // stop polling + } + + return false; // continue polling } - - // We handle the cancel later in the function. - if (signal && signal.aborted) { - return true; // stop polling - } - - return false; // continue polling - } - ); + ); + } if (signal && signal.aborted) { prediction = await this.predictions.cancel(prediction.id); From a7dfbfdc774e935444e47f371757f2b3120501b0 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Tue, 1 Oct 2024 16:48:14 -0700 Subject: [PATCH 42/68] 0.34.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2c3d24e6..f4c62d7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "0.34.0", + "version": "0.34.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "0.34.0", + "version": "0.34.1", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index f6c18183..1bc8a045 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "0.34.0", + "version": "0.34.1", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From abe1029d65094d199a4e62a18fa7fa7cb96df7a5 Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Wed, 9 Oct 2024 15:09:41 -0700 Subject: [PATCH 43/68] document `wait` option for *.prediction.create() methods (#319) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c644e75e..9671a6d2 100644 --- a/README.md +++ b/README.md @@ -618,6 +618,7 @@ const response = await replicate.predictions.create(options); | `options.input` | object | **Required**. An object with the model's inputs | | `options.model` | string | The name of the model, e.g. `black-forest-labs/flux-schnell`. This is required if you're running an [official model](https://replicate.com/explore#official-models). | | `options.version` | string | The 64-character [model version id](https://replicate.com/docs/topics/models/versions), e.g. `80537f9eead1a5bfa72d5ac6ea6414379be41d4d4f6679fd776e9535d1eb58bb`. This is required if you're not specifying a `model`. | +| `options.wait` | number | Wait up to 60s for the prediction to finish before returning. Disabled by default. See [Synchronous predictions](https://replicate.com/docs/topics/predictions/create-a-prediction#sync-mode) for more information. | | `options.stream` | boolean | Requests a URL for streaming output output | | `options.webhook` | string | An HTTPS URL for receiving a webhook when the prediction has new output | | `options.webhook_events_filter` | string[] | You can change which events trigger webhook requests by specifying webhook events (`start` \| `output` \| `logs` \| `completed`) | From 6ad79c8793eab5d20666fdf82ffb64c365194c07 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Wed, 9 Oct 2024 15:10:04 -0700 Subject: [PATCH 44/68] Enable FileObject and blocking mode by default (#316) * Enable FileObject and blocking mode by default * 1.0.0-beta.1 --- index.d.ts | 9 +++++---- index.js | 18 ++++++++---------- index.test.ts | 24 ++++++++++++++++++------ package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 34 insertions(+), 23 deletions(-) diff --git a/index.d.ts b/index.d.ts index 45a24301..1e417ae7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -156,7 +156,9 @@ declare module "replicate" { identifier: `${string}/${string}` | `${string}/${string}:${string}`, options: { input: object; - wait?: boolean | number | { interval?: number }; + wait?: + | { mode: "block"; interval?: number; timeout?: number } + | { mode: "poll"; interval?: number }; webhook?: string; webhook_events_filter?: WebhookEventType[]; signal?: AbortSignal; @@ -189,7 +191,6 @@ declare module "replicate" { wait( prediction: Prediction, options?: { - mode?: "poll"; interval?: number; }, stop?: (prediction: Prediction) => Promise @@ -215,7 +216,7 @@ declare module "replicate" { stream?: boolean; webhook?: string; webhook_events_filter?: WebhookEventType[]; - wait?: boolean | number | { mode?: "poll"; interval?: number }; + wait?: number | boolean; } ): Promise; }; @@ -304,7 +305,7 @@ declare module "replicate" { stream?: boolean; webhook?: string; webhook_events_filter?: WebhookEventType[]; - wait?: boolean | number | { mode?: "poll"; interval?: number }; + wait?: boolean | number; } & ({ version: string } | { model: string }) ): Promise; get(prediction_id: string): Promise; diff --git a/index.js b/index.js index ac4b815d..a5755d91 100644 --- a/index.js +++ b/index.js @@ -48,7 +48,7 @@ class Replicate { * @param {string} options.userAgent - Identifier of your app * @param {string} [options.baseUrl] - Defaults to https://api.replicate.com/v1 * @param {Function} [options.fetch] - Fetch function to use. Defaults to `globalThis.fetch` - * @param {boolean} [options.useFileOutput] - Set to `true` to return `FileOutput` objects from `run` instead of URLs, defaults to false. + * @param {boolean} [options.useFileOutput] - Set to `false` to disable `FileOutput` objects from `run` instead of URLs, defaults to true. * @param {"default" | "upload" | "data-uri"} [options.fileEncodingStrategy] - Determines the file encoding strategy to use */ constructor(options = {}) { @@ -60,7 +60,7 @@ class Replicate { this.baseUrl = options.baseUrl || "https://api.replicate.com/v1"; this.fetch = options.fetch || globalThis.fetch; this.fileEncodingStrategy = options.fileEncodingStrategy || "default"; - this.useFileOutput = options.useFileOutput || false; + this.useFileOutput = options.useFileOutput === false ? false : true; this.accounts = { current: accounts.current.bind(this), @@ -133,8 +133,7 @@ class Replicate { * @param {string} ref - Required. The model version identifier in the format "owner/name" or "owner/name:version" * @param {object} options * @param {object} options.input - Required. An object with the model inputs - * @param {object} [options.wait] - Options for waiting for the prediction to finish. If `wait` is explicitly true, the function will block and wait for the prediction to finish. - * @param {number} [options.wait.interval] - Polling interval in milliseconds. Defaults to 500 + * @param {{mode: "block", timeout?: number, interval?: number} | {mode: "poll", interval?: number }} [options.wait] - Options for waiting for the prediction to finish. If `wait` is explicitly true, the function will block and wait for the prediction to finish. * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the prediction has new output * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) * @param {AbortSignal} [options.signal] - AbortSignal to cancel the prediction @@ -144,23 +143,22 @@ class Replicate { * @returns {Promise} - Resolves with the output of running the model */ async run(ref, options, progress) { - const { wait, signal, ...data } = options; + const { wait = { mode: "block" }, signal, ...data } = options; const identifier = ModelVersionIdentifier.parse(ref); - const isBlocking = typeof wait === "boolean" || typeof wait === "number"; let prediction; if (identifier.version) { prediction = await this.predictions.create({ ...data, version: identifier.version, - wait: isBlocking ? wait : false, + wait: wait.mode === "block" ? wait.timeout ?? true : false, }); } else if (identifier.owner && identifier.name) { prediction = await this.predictions.create({ ...data, model: `${identifier.owner}/${identifier.name}`, - wait: isBlocking ? wait : false, + wait: wait.mode === "block" ? wait.timeout ?? true : false, }); } else { throw new Error("Invalid model version identifier"); @@ -171,11 +169,11 @@ class Replicate { progress(prediction); } - const isDone = isBlocking && prediction.status !== "starting"; + const isDone = wait.mode === "block" && prediction.status !== "starting"; if (!isDone) { prediction = await this.wait( prediction, - isBlocking ? {} : wait, + { interval: wait.mode === "poll" ? wait.interval : undefined }, async (updatedPrediction) => { // Call progress callback with the updated prediction object if (progress) { diff --git a/index.test.ts b/index.test.ts index 08fbf7c8..c67035a3 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1310,7 +1310,7 @@ describe("Replicate client", () => { "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", { input: { text: "Hello, world!" }, - wait: { interval: 1 }, + wait: { mode: "poll", interval: 1 }, }, (prediction) => { const progress = parseProgressFromLogs(prediction); @@ -1402,7 +1402,7 @@ describe("Replicate client", () => { "replicate/hello-world", { input: { text: "Hello, world!" }, - wait: { interval: 1 }, + wait: { mode: "poll", interval: 1 }, }, progress ); @@ -1448,12 +1448,18 @@ describe("Replicate client", () => { }); await expect( - client.run("a/b-1.0:abc123", { input: { text: "Hello, world!" } }) + client.run("a/b-1.0:abc123", { + wait: { mode: "poll" }, + input: { text: "Hello, world!" }, + }) ).resolves.not.toThrow(); }); test("Throws an error for invalid identifiers", async () => { - const options = { input: { text: "Hello, world!" } }; + const options = { + wait: { mode: "poll" } as { mode: "poll" }, + input: { text: "Hello, world!" }, + }; // @ts-expect-error await expect(client.run("owner:abc123", options)).rejects.toThrow(); @@ -1469,6 +1475,7 @@ describe("Replicate client", () => { await client.run( "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", { + wait: { mode: "poll" }, input: { text: "Alice", }, @@ -1492,7 +1499,7 @@ describe("Replicate client", () => { }) .reply(201, { id: "ufawqhfynnddngldkgtslldrkq", - status: "processing", + status: "starting", }) .persist() .get("/predictions/ufawqhfynnddngldkgtslldrkq") @@ -1510,6 +1517,7 @@ describe("Replicate client", () => { const output = await client.run( "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", { + wait: { mode: "poll" }, input: { text: "Hello, world!" }, signal, }, @@ -1524,7 +1532,7 @@ describe("Replicate client", () => { expect(onProgress).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - status: "processing", + status: "starting", }) ); expect(onProgress).toHaveBeenNthCalledWith( @@ -1580,6 +1588,7 @@ describe("Replicate client", () => { const output = (await client.run( "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", { + wait: { mode: "poll" }, input: { text: "Hello, world!" }, } )) as FileOutput; @@ -1631,6 +1640,7 @@ describe("Replicate client", () => { const output = (await client.run( "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", { + wait: { mode: "poll" }, input: { text: "Hello, world!" }, } )) as unknown as string; @@ -1677,6 +1687,7 @@ describe("Replicate client", () => { const [output] = (await client.run( "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", { + wait: { mode: "poll" }, input: { text: "Hello, world!" }, } )) as FileOutput[]; @@ -1724,6 +1735,7 @@ describe("Replicate client", () => { const output = (await client.run( "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", { + wait: { mode: "poll" }, input: { text: "Hello, world!" }, } )) as FileOutput; diff --git a/package-lock.json b/package-lock.json index f4c62d7c..dd0188a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "0.34.1", + "version": "1.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "0.34.1", + "version": "1.0.0-beta.1", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index 1bc8a045..2572d526 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "0.34.1", + "version": "1.0.0-beta.1", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From 2b55fcb87a75f8417e942289c1b8c59d527077e1 Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Wed, 9 Oct 2024 15:10:39 -0700 Subject: [PATCH 45/68] 1.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index dd0188a6..300e5382 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "1.0.0-beta.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "1.0.0-beta.1", + "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index 2572d526..fcd47e8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "1.0.0-beta.1", + "version": "1.0.0", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From ec31f40cc3d297e6d4a17bd0dca9d656ba55174c Mon Sep 17 00:00:00 2001 From: Dominic Baggott Date: Tue, 15 Oct 2024 11:25:23 +0100 Subject: [PATCH 46/68] Update deployments.createPrediction to use wait A first draft of this interface used `block`, but we ended up going with `wait` (as either a boolean or a number) for the predictions.createPrediction method. This commit brings the two implementations inline, removing the undocumented and unintended `block` parameter. --- lib/deployments.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/deployments.js b/lib/deployments.js index 6cab2613..716c8e19 100644 --- a/lib/deployments.js +++ b/lib/deployments.js @@ -9,11 +9,11 @@ const { transformFileInputs } = require("./util"); * @param {object} options.input - Required. An object with the model inputs * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the prediction has new output * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) - * @param {boolean} [options.block] - Whether to wait until the prediction is completed before returning. Defaults to false + * @param {boolean|integer} [options.wait] - Whether to wait until the prediction is completed before returning. If an integer is provided, it will wait for that many seconds. Defaults to false * @returns {Promise} Resolves with the created prediction data */ async function createPrediction(deployment_owner, deployment_name, options) { - const { input, block, ...data } = options; + const { input, wait, ...data } = options; if (data.webhook) { try { @@ -25,8 +25,13 @@ async function createPrediction(deployment_owner, deployment_name, options) { } const headers = {}; - if (block) { - headers["Prefer"] = "wait"; + if (wait) { + if (typeof wait === "number") { + const n = Math.max(1, Math.ceil(Number(wait)) || 1); + headers["Prefer"] = `wait=${n}`; + } else { + headers["Prefer"] = "wait"; + } } const response = await this.request( From 8b75b39ee8b122f1f85487890f91fd7b52b09d5b Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Mon, 21 Oct 2024 10:26:12 -0700 Subject: [PATCH 47/68] Update README.md to fix examples using FileOutput (#324) --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9671a6d2..8afd43f6 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,11 @@ const model = "stability-ai/stable-diffusion:27b93a2413e7f36cd83da926f3656280b29 const input = { prompt: "a 19th century portrait of a raccoon gentleman wearing a suit", }; -const output = await replicate.run(model, { input }); -// ['https://replicate.delivery/pbxt/GtQb3Sgve42ZZyVnt8xjquFk9EX5LP0fF68NTIWlgBMUpguQA/out-0.png'] +const [output] = await replicate.run(model, { input }); +// FileOutput('https://replicate.delivery/pbxt/GtQb3Sgve42ZZyVnt8xjquFk9EX5LP0fF68NTIWlgBMUpguQA/out-0.png') + +console.log(output.url()); // 'https://replicate.delivery/pbxt/GtQb3Sgve42ZZyVnt8xjquFk9EX5LP0fF68NTIWlgBMUpguQA/out-0.png' +console.log(output.blob()); // Blob ``` You can also run a model in the background: @@ -100,8 +103,8 @@ const model = "nightmareai/real-esrgan:42fed1c4974146d4d2414e2be2c5277c7fcf05fcc const input = { image: await fs.readFile("path/to/image.png"), }; -const output = await replicate.run(model, { input }); -// ['https://replicate.delivery/mgxm/e7b0e122-9daa-410e-8cde-006c7308ff4d/output.png'] +const [output] = await replicate.run(model, { input }); +// FileOutput('https://replicate.delivery/mgxm/e7b0e122-9daa-410e-8cde-006c7308ff4d/output.png') ``` > [!NOTE] From d0316950e0c74eecdd81b3d3ae9c2b1cac519796 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Thu, 24 Oct 2024 13:28:42 -0700 Subject: [PATCH 48/68] Document the `FileOutput` and `wait` parameters (#326) This commit takes a first pass at documenting the `FileOutput` and `wait` parameters introduced in 1.0.0 as well as calling out various potential gotchas with the API around URLs and data-uris based on feedback from the issues. --- README.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8afd43f6..08b6e5c8 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,15 @@ console.log(output.url()); // 'https://replicate.delivery/pbxt/GtQb3Sgve42ZZyVnt console.log(output.blob()); // Blob ``` +> [!NOTE] +> A model that outputs file data returns a `FileOutput` object by default. This is an implementation +> of `ReadableStream` that returns the file contents. It has a `.blob()` method for accessing a +> `Blob` representation and a `.url()` method that will return the underlying data-source. +> +> **This data source can be either a remote URL or a data-uri with base64 encoded data. Check +> out the documentation on [creating a prediction](https://replicate.com/docs/topics/predictions/create-a-prediction) +> for more information.** + You can also run a model in the background: ```js @@ -277,6 +286,7 @@ const replicate = new Replicate(options); | `options.baseUrl` | string | Defaults to https://api.replicate.com/v1 | | `options.fetch` | function | Fetch function to use. Defaults to `globalThis.fetch` | | `options.fileEncodingStrategy` | string | Determines the file encoding strategy to use. Possible values: `"default"`, `"upload"`, or `"data-uri"`. Defaults to `"default"` | +| `options.useFileOutput` | boolean | Determines if the `replicate.run()` method should convert file output into `FileOutput` objects | The client makes requests to Replicate's API using @@ -333,8 +343,10 @@ const output = await replicate.run(identifier, options, progress); | ------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `identifier` | string | **Required**. The model version identifier in the format `{owner}/{name}:{version}`, for example `stability-ai/sdxl:8beff3369e81422112d93b89ca01426147de542cd4684c244b673b105188fe5f` | | `options.input` | object | **Required**. An object with the model inputs. | -| `options.wait` | object | Options for waiting for the prediction to finish | -| `options.wait.interval` | number | Polling interval in milliseconds. Defaults to 500 | +| `options.wait` | object | Options for waiting for the prediction to finish | +| `options.wait.type` | `"poll" \| "block"` | `"block"` holds the request open, `"poll"` makes repeated requests to fetch the prediction. Defaults to `"block"` | +| `options.wait.interval` | number | Polling interval in milliseconds. Defaults to 500 | +| `options.wait.timeout` | number | In `"block"` mode determines how long the request will be held open until falling back to polling. Defaults to 60 | | `options.webhook` | string | An HTTPS URL for receiving a webhook when the prediction has new output | | `options.webhook_events_filter` | string[] | An array of events which should trigger [webhooks](https://replicate.com/docs/webhooks). Allowable values are `start`, `output`, `logs`, and `completed` | | `options.signal` | object | An [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) to cancel the prediction | @@ -342,7 +354,12 @@ const output = await replicate.run(identifier, options, progress); Throws `Error` if the prediction failed. -Returns `Promise` which resolves with the output of running the model. +Returns `Promise` which resolves with the output of running the model. + +> [!NOTE] +> Currently the TypeScript return type of `replicate.run()` is `Promise` this is +> misleading as a model can return array types as well as primative types like strings, +> numbers and booleans. Example: @@ -364,6 +381,26 @@ const onProgress = (prediction) => { const output = await replicate.run(model, { input }, onProgress) ``` +#### Sync vs. Async API (`"poll"` vs. `"block"`) + +The `replicate.run()` API takes advantage of the [Replicate sync API](https://replicate.com/docs/topics/predictions/create-a-prediction) +which is optimized for low latency requests to file models like `black-forest-labs/flux-dev` and +`black-forest-labs/flux-schnell`. When creating a prediction this will hold a connection open to the +server and return a `FileObject` containing the generated file as quickly as possible. + +> [!NOTE] +> In this mode the `url()` method on the `FileObject` may refer to either a remote URL or +> base64 encoded data-uri. The latter is an optimization we make on certain models to deliver +> the files faster to the client. +> +> If you need the prediction URLs for whatever reason you can opt out of the sync mode by +> passing `wait: { "type": "poll" }` to the `run()` method. +> +> ```js +> const output = await replicate.run(model, { input, wait: { type: "poll" } }); +> output.url() // URL +> ``` + ### `replicate.stream` Run a model and stream its output. Unlike [`replicate.prediction.create`](#replicatepredictionscreate), this method returns only the prediction output rather than the entire prediction object. @@ -1192,6 +1229,41 @@ The `replicate.request()` method is used by the other methods to interact with the Replicate API. You can call this method directly to make other requests to the API. +### `FileOutput` + +`FileOutput` is a `ReadableStream` instance that represents a model file output. It can be used to stream file data to disk or as a `Response` body to an HTTP request. + +```javascript +const [output] = await replicate.run("black-forest-labs/flux-schnell", { + input: { prompt: "astronaut riding a rocket like a horse" } +}); + +// To access the file URL (or data-uri): +console.log(output.url()); //=> "http://example.com" + +// To write the file to disk: +fs.writeFile("my-image.png", output); + +// To stream the file back to a browser: +return new Response(output); + +// To read the file in chunks: +for await (const chunk of output) { + console.log(chunk); // UInt8Array +} +``` + +You can opt out of FileOutput by passing `useFileOutput: false` to the `Replicate` constructor: + +```javascript +const replicate = new Replicate({ useFileOutput: false }); +``` + +| method | returns | description | +| -------------------- | ------ | ------------------------------------------------------------ | +| `url()` | string | A `URL` object representing the HTTP URL or data-uri | +| `blob()` | object | A `Blob` instance containing the binary file | + ## Troubleshooting ### Predictions hanging in Next.js From 6a338aa4b4ac79faae48c0d7081694c3e8f42129 Mon Sep 17 00:00:00 2001 From: Radovenchyk Date: Mon, 28 Oct 2024 18:25:33 +0200 Subject: [PATCH 49/68] Update README.md (#329) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 08b6e5c8..9f882472 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ To validate webhooks: 1. Check out the [webhooks guide](https://replicate.com/docs/webhooks) to get started. 1. [Retrieve your webhook signing secret](https://replicate.com/docs/webhooks#retrieving-the-webhook-signing-key) and store it in your enviroment. -1. Update your webhook handler to call `validateWebhook(request, secret)`, where `request` is an instance of a [web-standard `Request` object](https://developer.mozilla.org/en-US/docs/Web/API/object, and `secret` is the signing secret for your environment. +1. Update your webhook handler to call `validateWebhook(request, secret)`, where `request` is an instance of a [web-standard `Request` object](https://developer.mozilla.org/en-US/docs/Web/API/object), and `secret` is the signing secret for your environment. Here's an example of how to validate webhooks using Next.js: From 5a98ca7c9dcd33e30aa0be3a95ce990630546284 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Wed, 30 Oct 2024 04:20:23 -0700 Subject: [PATCH 50/68] Remove mention of returning data URLs from sync API (#330) This change has now been reverted while we figure out how to provide a more consistent implementation. --- README.md | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9f882472..2fa9f0f6 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,8 @@ console.log(output.blob()); // Blob > of `ReadableStream` that returns the file contents. It has a `.blob()` method for accessing a > `Blob` representation and a `.url()` method that will return the underlying data-source. > -> **This data source can be either a remote URL or a data-uri with base64 encoded data. Check -> out the documentation on [creating a prediction](https://replicate.com/docs/topics/predictions/create-a-prediction) -> for more information.** +> We recommend accessing file data directly either as readable stream or via `.blob()` as the +> `.url()` property may not always return a HTTP URL in future. You can also run a model in the background: @@ -388,19 +387,6 @@ which is optimized for low latency requests to file models like `black-forest-la `black-forest-labs/flux-schnell`. When creating a prediction this will hold a connection open to the server and return a `FileObject` containing the generated file as quickly as possible. -> [!NOTE] -> In this mode the `url()` method on the `FileObject` may refer to either a remote URL or -> base64 encoded data-uri. The latter is an optimization we make on certain models to deliver -> the files faster to the client. -> -> If you need the prediction URLs for whatever reason you can opt out of the sync mode by -> passing `wait: { "type": "poll" }` to the `run()` method. -> -> ```js -> const output = await replicate.run(model, { input, wait: { type: "poll" } }); -> output.url() // URL -> ``` - ### `replicate.stream` Run a model and stream its output. Unlike [`replicate.prediction.create`](#replicatepredictionscreate), this method returns only the prediction output rather than the entire prediction object. @@ -1238,7 +1224,7 @@ const [output] = await replicate.run("black-forest-labs/flux-schnell", { input: { prompt: "astronaut riding a rocket like a horse" } }); -// To access the file URL (or data-uri): +// To access the file URL: console.log(output.url()); //=> "http://example.com" // To write the file to disk: @@ -1261,8 +1247,8 @@ const replicate = new Replicate({ useFileOutput: false }); | method | returns | description | | -------------------- | ------ | ------------------------------------------------------------ | -| `url()` | string | A `URL` object representing the HTTP URL or data-uri | | `blob()` | object | A `Blob` instance containing the binary file | +| `url()` | string | A `URL` object pointing to the underlying data source. Please note that this may not always be an HTTP URL in future. | ## Troubleshooting From 0b7c8ebd9adb30f84ab420d1288e2ce1dac349cf Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Fri, 8 Nov 2024 13:46:58 -0800 Subject: [PATCH 51/68] Fix typo on README (#334) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2fa9f0f6..1e14ec24 100644 --- a/README.md +++ b/README.md @@ -596,7 +596,7 @@ const response = await replicate.models.versions.list(model_owner, model_name); ### `replicate.models.versions.get` -Get metatadata for a specific version of a model. +Get metadata for a specific version of a model. ```js const response = await replicate.models.versions.get(model_owner, model_name, version_id); From 720f786b4c6f15576ba1bf331e8c31683204f4bd Mon Sep 17 00:00:00 2001 From: Marius Kleidl <1375043+Acconut@users.noreply.github.com> Date: Wed, 26 Mar 2025 14:23:13 +0100 Subject: [PATCH 52/68] Correct documentaiton for wait mode (#337) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e14ec24..7d2d61e0 100644 --- a/README.md +++ b/README.md @@ -343,7 +343,7 @@ const output = await replicate.run(identifier, options, progress); | `identifier` | string | **Required**. The model version identifier in the format `{owner}/{name}:{version}`, for example `stability-ai/sdxl:8beff3369e81422112d93b89ca01426147de542cd4684c244b673b105188fe5f` | | `options.input` | object | **Required**. An object with the model inputs. | | `options.wait` | object | Options for waiting for the prediction to finish | -| `options.wait.type` | `"poll" \| "block"` | `"block"` holds the request open, `"poll"` makes repeated requests to fetch the prediction. Defaults to `"block"` | +| `options.wait.mode` | `"poll" \| "block"` | `"block"` holds the request open, `"poll"` makes repeated requests to fetch the prediction. Defaults to `"block"` | | `options.wait.interval` | number | Polling interval in milliseconds. Defaults to 500 | | `options.wait.timeout` | number | In `"block"` mode determines how long the request will be held open until falling back to polling. Defaults to 60 | | `options.webhook` | string | An HTTPS URL for receiving a webhook when the prediction has new output | From 01c4e09b1ac3408beb99589b464d2b81077e4e4b Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Wed, 26 Mar 2025 12:16:12 +0000 Subject: [PATCH 53/68] Remove usage of webcrypto from `node:crypto` in Node 18 The original code was intended to shim in support for the webcrypto interface in Node 18, which was included indirectly as `webcrypto` in the "node:crypto" module or globally via the `--no-experimental-global-webcrypto` flag. This change has caused many issues with bundlers and static analyzers which do not like the obfuscated call to `require()`. Node 18 will no longer receive security support as of 30 April 2025[1] and as such it feels like we can now drop this workaround in favor of documenting alternative approaches. [1]: https://endoflife.date/nodejs --- lib/util.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/util.js b/lib/util.js index bd3c31e9..8dc06c8f 100644 --- a/lib/util.js +++ b/lib/util.js @@ -97,24 +97,6 @@ async function validateWebhook(requestData, secret) { */ async function createHMACSHA256(secret, data) { const encoder = new TextEncoder(); - let crypto = globalThis.crypto; - - // In Node 18 the `crypto` global is behind a --no-experimental-global-webcrypto flag - if (typeof crypto === "undefined" && typeof require === "function") { - // NOTE: Webpack (primarily as it's used by Next.js) and perhaps some - // other bundlers do not currently support the `node` protocol and will - // error if it's found in the source. Other platforms like CloudFlare - // will only support requires when using the node protocol. - // - // As this line is purely to support Node 18.x we make an indirect request - // to the require function which fools Webpack... - // - // We may be able to remove this in future as it looks like Webpack is getting - // support for requiring using the `node:` protocol. - // See: https://github.com/webpack/webpack/issues/18277 - crypto = require.call(null, "node:crypto").webcrypto; - } - const key = await crypto.subtle.importKey( "raw", base64ToBytes(secret), From 2d42001bf1c5c49b78b92914c62e56f99adb4997 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Wed, 26 Mar 2025 13:04:05 +0000 Subject: [PATCH 54/68] Fix validateWebhook interfaces and update tests --- index.d.ts | 24 ++++++++++++++---------- index.test.ts | 36 ++++++++++++++++++++++++++++++++++-- lib/util.js | 41 ++++++++++++++++++++++++++++++++++------- 3 files changed, 82 insertions(+), 19 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1e417ae7..5b35bf2d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -340,16 +340,20 @@ declare module "replicate" { } export function validateWebhook( - requestData: - | Request - | { - id?: string; - timestamp?: string; - body: string; - secret?: string; - signature?: string; - }, - secret: string + request: Request, + secret: string, + crypto?: Crypto + ): Promise; + + export function validateWebhook( + requestData: { + id: string; + timestamp: string; + signature: string; + body: string; + secret: string; + }, + crypto?: Crypto ): Promise; export function parseProgressFromLogs(logs: Prediction | string): { diff --git a/index.test.ts b/index.test.ts index c67035a3..f5bd6093 100644 --- a/index.test.ts +++ b/index.test.ts @@ -9,6 +9,7 @@ import Replicate, { } from "replicate"; import nock from "nock"; import { createReadableStream } from "./lib/stream"; +import { webcrypto } from "node:crypto"; let client: Replicate; const BASE_URL = "https://api.replicate.com/v1"; @@ -1779,9 +1780,40 @@ describe("Replicate client", () => { // This is a test secret and should not be used in production const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"; + if (globalThis.crypto) { + const isValid = await validateWebhook(request, secret); + expect(isValid).toBe(true); + } else { + const isValid = await validateWebhook( + request, + secret, + webcrypto as Crypto // Node 18 requires this to be passed manually + ); + expect(isValid).toBe(true); + } + }); - const isValid = await validateWebhook(request, secret); - expect(isValid).toBe(true); + test("Can be used to validate webhook", async () => { + // Test case from https://github.com/svix/svix-webhooks/blob/b41728cd98a7e7004a6407a623f43977b82fcba4/javascript/src/webhook.test.ts#L190-L200 + const requestData = { + id: "msg_p5jXN8AQM9LWM0D4loKWxJek", + timestamp: "1614265330", + signature: "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=", + body: `{"test": 2432232314}`, + secret: "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw", + }; + + // This is a test secret and should not be used in production + if (globalThis.crypto) { + const isValid = await validateWebhook(requestData); + expect(isValid).toBe(true); + } else { + const isValid = await validateWebhook( + requestData, + webcrypto as Crypto // Node 18 requires this to be passed manually + ); + expect(isValid).toBe(true); + } }); // Add more tests for error handling, edge cases, etc. diff --git a/lib/util.js b/lib/util.js index 8dc06c8f..8ef59ac8 100644 --- a/lib/util.js +++ b/lib/util.js @@ -10,6 +10,7 @@ const { create: createFile } = require("./files"); * @param {string} requestData.body - The raw body of the incoming webhook request. * @param {string} requestData.secret - The webhook secret, obtained from `replicate.webhooks.defaul.secret` method. * @param {string} requestData.signature - The webhook signature header from the incoming request, comprising one or more space-delimited signatures. + * @param {Crypto} [crypto] - An optional `Crypto` implementation that conforms to the [browser Crypto interface](https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto) */ /** @@ -22,6 +23,7 @@ const { create: createFile } = require("./files"); * @param {string} requestData.headers["webhook-signature"] - The webhook signature header from the incoming request, comprising one or more space-delimited signatures * @param {string} requestData.body - The raw body of the incoming webhook request * @param {string} secret - The webhook secret, obtained from `replicate.webhooks.defaul.secret` method + * @param {Crypto} [crypto] - An optional `Crypto` implementation that conforms to the [browser Crypto interface](https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto) */ /** @@ -30,9 +32,13 @@ const { create: createFile } = require("./files"); * @returns {Promise} - True if the signature is valid * @throws {Error} - If the request is missing required headers, body, or secret */ -async function validateWebhook(requestData, secret) { - let { id, timestamp, body, signature } = requestData; - const signingSecret = secret || requestData.secret; +async function validateWebhook(requestData, secretOrCrypto, customCrypto) { + let id; + let body; + let timestamp; + let signature; + let secret; + let crypto = globalThis.crypto; if (requestData && requestData.headers && requestData.body) { if (typeof requestData.headers.get === "function") { @@ -47,6 +53,19 @@ async function validateWebhook(requestData, secret) { signature = requestData.headers["webhook-signature"]; } body = requestData.body; + secret = secretOrCrypto; + if (customCrypto) { + crypto = customCrypto; + } + } else { + id = requestData.id; + body = requestData.body; + timestamp = requestData.timestamp; + signature = requestData.signature; + secret = requestData.secret; + if (secretOrCrypto) { + crypto = secretOrCrypto; + } } if (body instanceof ReadableStream || body.readable) { @@ -71,15 +90,22 @@ async function validateWebhook(requestData, secret) { throw new Error("Missing required body"); } - if (!signingSecret) { + if (!secret) { throw new Error("Missing required secret"); } + if (!crypto) { + throw new Error( + 'Missing `crypto` implementation. If using Node 18 pass in require("node:crypto").webcrypto' + ); + } + const signedContent = `${id}.${timestamp}.${body}`; const computedSignature = await createHMACSHA256( - signingSecret.split("_").pop(), - signedContent + secret.split("_").pop(), + signedContent, + crypto ); const expectedSignatures = signature @@ -94,8 +120,9 @@ async function validateWebhook(requestData, secret) { /** * @param {string} secret - base64 encoded string * @param {string} data - text body of request + * @param {Crypto} crypto - an implementation of the web Crypto api */ -async function createHMACSHA256(secret, data) { +async function createHMACSHA256(secret, data, crypto) { const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", From 37aa363e05590a738e2e0aef145645f641c5047e Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Wed, 26 Mar 2025 13:04:18 +0000 Subject: [PATCH 55/68] Document the validateWebhook interface on Node 18 --- README.md | 7 +++++++ lib/util.js | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/README.md b/README.md index 7d2d61e0..67c8cab1 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,13 @@ const requestData = { const webhookIsValid = await validateWebhook(requestData); ``` +> [!NOTE] +> The `validateWebhook` function uses the global `crypto` API available in most JavaScript runtimes. Node <= 18 does not provide this global so in this case you need to either call node with the `--no-experimental-global-webcrypto` or provide the `webcrypto` module manually. +> ```js +> const crypto = require("node:crypto").webcrypto; +> const webhookIsValid = await valdiateWebhook(requestData, crypto); +> ``` + ## TypeScript The `Replicate` constructor and all `replicate.*` methods are fully typed. diff --git a/lib/util.js b/lib/util.js index 8ef59ac8..2fa49190 100644 --- a/lib/util.js +++ b/lib/util.js @@ -53,6 +53,12 @@ async function validateWebhook(requestData, secretOrCrypto, customCrypto) { signature = requestData.headers["webhook-signature"]; } body = requestData.body; + if (typeof secretOrCrypto !== "string") { + throw new Error( + "Unexpected value for secret passed to validateWebhook, expected a string" + ); + } + secret = secretOrCrypto; if (customCrypto) { crypto = customCrypto; From ac5caba3fd6c931803923689ae8fb01486b64646 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Wed, 26 Mar 2025 17:30:42 +0000 Subject: [PATCH 56/68] Add support for `AbortSignal` to all API methods (#339) This PR adds support for passing `AbortSignal` to all API methods that make HTTP requests, these are passed directly into the native `fetch()` implementation, so it's up to the user to handle the `AbortError` raised, if any. ```js const controller = new AbortController(); try { const prediction = await replicate.predictions.create({ version: 'xyz', ..., signal: controller.signal, }); } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { ... } } ``` The `paginate` function also checks to see whether the signal was aborted before proceeding to the next iteration. If so it returns immediately to avoid making a redundant fetch call. This allows the client to take advantage of various frameworks that provide an `AbortSignal` instance to tear down any in flight requests. --- README.md | 14 ++-- index.d.ts | 114 +++++++++++++++++++++++--------- index.js | 17 +++-- index.test.ts | 84 +++++++++++++++++++++++ integration/next/pages/index.js | 8 +-- lib/accounts.js | 5 +- lib/collections.js | 10 ++- lib/deployments.js | 40 +++++++++-- lib/files.js | 20 ++++-- lib/hardware.js | 5 +- lib/models.js | 35 ++++++++-- lib/predictions.js | 20 ++++-- lib/trainings.js | 19 ++++-- lib/util.js | 12 ++-- lib/webhooks.js | 5 +- 15 files changed, 326 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 67c8cab1..cf12865c 100644 --- a/README.md +++ b/README.md @@ -1213,15 +1213,21 @@ Low-level method used by the Replicate client to interact with API endpoints. const response = await replicate.request(route, parameters); ``` -| name | type | description | -| -------------------- | ------ | ------------------------------------------------------------ | -| `options.route` | string | Required. REST API endpoint path. | -| `options.parameters` | object | URL, query, and request body parameters for the given route. | +| name | type | description | +| -------------------- | ------------------- | ----------- | +| `options.route` | `string` | Required. REST API endpoint path. +| `options.params` | `object` | URL query parameters for the given route. | +| `options.method` | `string` | HTTP method for the given route. | +| `options.headers` | `object` | Additional HTTP headers for the given route. | +| `options.data` | `object \| FormData` | Request body. | +| `options.signal` | `AbortSignal` | Optional `AbortSignal`. | The `replicate.request()` method is used by the other methods to interact with the Replicate API. You can call this method directly to make other requests to the API. +The method accepts an `AbortSignal` which can be used to cancel the request in flight. + ### `FileOutput` `FileOutput` is a `ReadableStream` instance that represents a model file output. It can be used to stream file data to disk or as a `Response` body to an HTTP request. diff --git a/index.d.ts b/index.d.ts index 5b35bf2d..709f4665 100644 --- a/index.d.ts +++ b/index.d.ts @@ -183,10 +183,14 @@ declare module "replicate" { headers?: object | Headers; params?: object; data?: object; + signal?: AbortSignal; } ): Promise; - paginate(endpoint: () => Promise>): AsyncGenerator<[T]>; + paginate( + endpoint: () => Promise>, + options?: { signal?: AbortSignal } + ): AsyncGenerator; wait( prediction: Prediction, @@ -197,12 +201,15 @@ declare module "replicate" { ): Promise; accounts: { - current(): Promise; + current(options?: { signal?: AbortSignal }): Promise; }; collections: { - list(): Promise>; - get(collection_slug: string): Promise; + list(options?: { signal?: AbortSignal }): Promise>; + get( + collection_slug: string, + options?: { signal?: AbortSignal } + ): Promise; }; deployments: { @@ -217,21 +224,26 @@ declare module "replicate" { webhook?: string; webhook_events_filter?: WebhookEventType[]; wait?: number | boolean; + signal?: AbortSignal; } ): Promise; }; get( deployment_owner: string, - deployment_name: string + deployment_name: string, + options?: { signal?: AbortSignal } + ): Promise; + create( + deployment_config: { + name: string; + model: string; + version: string; + hardware: string; + min_instances: number; + max_instances: number; + }, + options?: { signal?: AbortSignal } ): Promise; - create(deployment_config: { - name: string; - model: string; - version: string; - hardware: string; - min_instances: number; - max_instances: number; - }): Promise; update( deployment_owner: string, deployment_name: string, @@ -245,32 +257,45 @@ declare module "replicate" { | { hardware: string } | { min_instances: number } | { max_instances: number } - ) + ), + options?: { signal?: AbortSignal } ): Promise; delete( deployment_owner: string, - deployment_name: string + deployment_name: string, + options?: { signal?: AbortSignal } ): Promise; - list(): Promise>; + list(options?: { signal?: AbortSignal }): Promise>; }; files: { create( file: Blob | Buffer, - metadata?: Record + metadata?: Record, + options?: { signal?: AbortSignal } ): Promise; - list(): Promise>; - get(file_id: string): Promise; - delete(file_id: string): Promise; + list(options?: { signal?: AbortSignal }): Promise>; + get( + file_id: string, + options?: { signal?: AbortSignal } + ): Promise; + delete( + file_id: string, + options?: { signal?: AbortSignal } + ): Promise; }; hardware: { - list(): Promise; + list(options?: { signal?: AbortSignal }): Promise; }; models: { - get(model_owner: string, model_name: string): Promise; - list(): Promise>; + get( + model_owner: string, + model_name: string, + options?: { signal?: AbortSignal } + ): Promise; + list(options?: { signal?: AbortSignal }): Promise>; create( model_owner: string, model_name: string, @@ -282,17 +307,26 @@ declare module "replicate" { paper_url?: string; license_url?: string; cover_image_url?: string; + signal?: AbortSignal; } ): Promise; versions: { - list(model_owner: string, model_name: string): Promise; + list( + model_owner: string, + model_name: string, + options?: { signal?: AbortSignal } + ): Promise; get( model_owner: string, model_name: string, - version_id: string + version_id: string, + options?: { signal?: AbortSignal } ): Promise; }; - search(query: string): Promise>; + search( + query: string, + options?: { signal?: AbortSignal } + ): Promise>; }; predictions: { @@ -306,11 +340,18 @@ declare module "replicate" { webhook?: string; webhook_events_filter?: WebhookEventType[]; wait?: boolean | number; + signal?: AbortSignal; } & ({ version: string } | { model: string }) ): Promise; - get(prediction_id: string): Promise; - cancel(prediction_id: string): Promise; - list(): Promise>; + get( + prediction_id: string, + options?: { signal?: AbortSignal } + ): Promise; + cancel( + prediction_id: string, + options?: { signal?: AbortSignal } + ): Promise; + list(options?: { signal?: AbortSignal }): Promise>; }; trainings: { @@ -323,17 +364,24 @@ declare module "replicate" { input: object; webhook?: string; webhook_events_filter?: WebhookEventType[]; + signal?: AbortSignal; } ): Promise; - get(training_id: string): Promise; - cancel(training_id: string): Promise; - list(): Promise>; + get( + training_id: string, + options?: { signal?: AbortSignal } + ): Promise; + cancel( + training_id: string, + options?: { signal?: AbortSignal } + ): Promise; + list(options?: { signal?: AbortSignal }): Promise>; }; webhooks: { default: { secret: { - get(): Promise; + get(options?: { signal?: AbortSignal }): Promise; }; }; }; diff --git a/index.js b/index.js index a5755d91..b1248e70 100644 --- a/index.js +++ b/index.js @@ -225,6 +225,7 @@ class Replicate { * @param {object} [options.params] - Query parameters * @param {object|Headers} [options.headers] - HTTP headers * @param {object} [options.data] - Body parameters + * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request * @returns {Promise} - Resolves with the response object * @throws {ApiError} If the request failed */ @@ -241,7 +242,7 @@ class Replicate { ); } - const { method = "GET", params = {}, data } = options; + const { method = "GET", params = {}, data, signal } = options; for (const [key, value] of Object.entries(params)) { url.searchParams.append(key, value); @@ -273,6 +274,7 @@ class Replicate { method, headers, body, + signal, }; const shouldRetry = @@ -354,15 +356,20 @@ class Replicate { * console.log(page); * } * @param {Function} endpoint - Function that returns a promise for the next page of results + * @param {object} [options] + * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request. * @yields {object[]} Each page of results */ - async *paginate(endpoint) { + async *paginate(endpoint, options = {}) { const response = await endpoint(); yield response.results; - if (response.next) { + if (response.next && !(options.signal && options.signal.aborted)) { const nextPage = () => - this.request(response.next, { method: "GET" }).then((r) => r.json()); - yield* this.paginate(nextPage); + this.request(response.next, { + method: "GET", + signal: options.signal, + }).then((r) => r.json()); + yield* this.paginate(nextPage, options); } } diff --git a/index.test.ts b/index.test.ts index f5bd6093..4905908a 100644 --- a/index.test.ts +++ b/index.test.ts @@ -99,6 +99,90 @@ describe("Replicate client", () => { }); }); + describe("paginate", () => { + test("pages through results", async () => { + nock(BASE_URL) + .get("/collections") + .reply(200, { + results: [ + { + name: "Super resolution", + slug: "super-resolution", + description: + "Upscaling models that create high-quality images from low-quality images.", + }, + ], + next: `${BASE_URL}/collections?page=2`, + previous: null, + }); + nock(BASE_URL) + .get("/collections?page=2") + .reply(200, { + results: [ + { + name: "Image classification", + slug: "image-classification", + description: "Models that classify images.", + }, + ], + next: null, + previous: null, + }); + + const iterator = client.paginate(client.collections.list); + + const firstPage = (await iterator.next()).value; + expect(firstPage.length).toBe(1); + + const secondPage = (await iterator.next()).value; + expect(secondPage.length).toBe(1); + }); + + test("accepts an abort signal", async () => { + nock(BASE_URL) + .get("/collections") + .reply(200, { + results: [ + { + name: "Super resolution", + slug: "super-resolution", + description: + "Upscaling models that create high-quality images from low-quality images.", + }, + ], + next: `${BASE_URL}/collections?page=2`, + previous: null, + }); + nock(BASE_URL) + .get("/collections?page=2") + .reply(200, { + results: [ + { + name: "Image classification", + slug: "image-classification", + description: "Models that classify images.", + }, + ], + next: null, + previous: null, + }); + + const controller = new AbortController(); + const iterator = client.paginate(client.collections.list, { + signal: controller.signal, + }); + + const firstIteration = await iterator.next(); + expect(firstIteration.value.length).toBe(1); + + controller.abort(); + + const secondIteration = await iterator.next(); + expect(secondIteration.value).toBeUndefined(); + expect(secondIteration.done).toBe(true); + }); + }); + describe("account.get", () => { test("Calls the correct API route", async () => { nock(BASE_URL).get("/account").reply(200, { diff --git a/integration/next/pages/index.js b/integration/next/pages/index.js index fc9581a7..09124388 100644 --- a/integration/next/pages/index.js +++ b/integration/next/pages/index.js @@ -1,5 +1,5 @@ export default () => ( -
-

Welcome to Next.js

-
-) +
+

Welcome to Next.js

+
+); diff --git a/lib/accounts.js b/lib/accounts.js index b3bbd9fe..72a94afa 100644 --- a/lib/accounts.js +++ b/lib/accounts.js @@ -1,11 +1,14 @@ /** * Get the current account * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the current account */ -async function getCurrentAccount() { +async function getCurrentAccount({ signal } = {}) { const response = await this.request("/account", { method: "GET", + signal, }); return response.json(); diff --git a/lib/collections.js b/lib/collections.js index 9332aaab..7b8e8f11 100644 --- a/lib/collections.js +++ b/lib/collections.js @@ -2,11 +2,14 @@ * Fetch a model collection * * @param {string} collection_slug - Required. The slug of the collection. See http://replicate.com/collections + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with the collection data */ -async function getCollection(collection_slug) { +async function getCollection(collection_slug, { signal } = {}) { const response = await this.request(`/collections/${collection_slug}`, { method: "GET", + signal, }); return response.json(); @@ -15,11 +18,14 @@ async function getCollection(collection_slug) { /** * Fetch a list of model collections * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with the collections data */ -async function listCollections() { +async function listCollections({ signal } = {}) { const response = await this.request("/collections", { method: "GET", + signal, }); return response.json(); diff --git a/lib/deployments.js b/lib/deployments.js index 716c8e19..d45c4f39 100644 --- a/lib/deployments.js +++ b/lib/deployments.js @@ -10,10 +10,11 @@ const { transformFileInputs } = require("./util"); * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the prediction has new output * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) * @param {boolean|integer} [options.wait] - Whether to wait until the prediction is completed before returning. If an integer is provided, it will wait for that many seconds. Defaults to false + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the created prediction data */ async function createPrediction(deployment_owner, deployment_name, options) { - const { input, wait, ...data } = options; + const { input, wait, signal, ...data } = options; if (data.webhook) { try { @@ -47,6 +48,7 @@ async function createPrediction(deployment_owner, deployment_name, options) { this.fileEncodingStrategy ), }, + signal, } ); @@ -58,13 +60,20 @@ async function createPrediction(deployment_owner, deployment_name, options) { * * @param {string} deployment_owner - Required. The username of the user or organization who owns the deployment * @param {string} deployment_name - Required. The name of the deployment + * @param {object] [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the deployment data */ -async function getDeployment(deployment_owner, deployment_name) { +async function getDeployment( + deployment_owner, + deployment_name, + { signal } = {} +) { const response = await this.request( `/deployments/${deployment_owner}/${deployment_name}`, { method: "GET", + signal, } ); @@ -84,13 +93,16 @@ async function getDeployment(deployment_owner, deployment_name) { /** * Create a deployment * - * @param {DeploymentCreateRequest} config - Required. The deployment config. + * @param {DeploymentCreateRequest} deployment_config - Required. The deployment config. + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the deployment data */ -async function createDeployment(deployment_config) { +async function createDeployment(deployment_config, { signal } = {}) { const response = await this.request("/deployments", { method: "POST", data: deployment_config, + signal, }); return response.json(); @@ -110,18 +122,22 @@ async function createDeployment(deployment_config) { * @param {string} deployment_owner - Required. The username of the user or organization who owns the deployment * @param {string} deployment_name - Required. The name of the deployment * @param {DeploymentUpdateRequest} deployment_config - Required. The deployment changes. + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the deployment data */ async function updateDeployment( deployment_owner, deployment_name, - deployment_config + deployment_config, + { signal } = {} ) { const response = await this.request( `/deployments/${deployment_owner}/${deployment_name}`, { method: "PATCH", data: deployment_config, + signal, } ); @@ -133,13 +149,20 @@ async function updateDeployment( * * @param {string} deployment_owner - Required. The username of the user or organization who owns the deployment * @param {string} deployment_name - Required. The name of the deployment + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with true if the deployment was deleted */ -async function deleteDeployment(deployment_owner, deployment_name) { +async function deleteDeployment( + deployment_owner, + deployment_name, + { signal } = {} +) { const response = await this.request( `/deployments/${deployment_owner}/${deployment_name}`, { method: "DELETE", + signal, } ); @@ -149,11 +172,14 @@ async function deleteDeployment(deployment_owner, deployment_name) { /** * List all deployments * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with a page of deployments */ -async function listDeployments() { +async function listDeployments({ signal } = {}) { const response = await this.request("/deployments", { method: "GET", + signal, }); return response.json(); diff --git a/lib/files.js b/lib/files.js index de49c580..c8101398 100644 --- a/lib/files.js +++ b/lib/files.js @@ -3,9 +3,11 @@ * * @param {object} file - Required. The file object. * @param {object} metadata - Optional. User-provided metadata associated with the file. + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with the file data */ -async function createFile(file, metadata = {}) { +async function createFile(file, metadata = {}, { signal } = {}) { const form = new FormData(); let filename; @@ -36,6 +38,7 @@ async function createFile(file, metadata = {}) { headers: { "Content-Type": "multipart/form-data", }, + signal, }); return response.json(); @@ -44,11 +47,14 @@ async function createFile(file, metadata = {}) { /** * List all files * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with the files data */ -async function listFiles() { +async function listFiles({ signal } = {}) { const response = await this.request("/files", { method: "GET", + signal, }); return response.json(); @@ -58,11 +64,14 @@ async function listFiles() { * Get a file * * @param {string} file_id - Required. The ID of the file. + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with the file data */ -async function getFile(file_id) { +async function getFile(file_id, { signal } = {}) { const response = await this.request(`/files/${file_id}`, { method: "GET", + signal, }); return response.json(); @@ -72,11 +81,14 @@ async function getFile(file_id) { * Delete a file * * @param {string} file_id - Required. The ID of the file. + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with true if the file was deleted */ -async function deleteFile(file_id) { +async function deleteFile(file_id, { signal } = {}) { const response = await this.request(`/files/${file_id}`, { method: "DELETE", + signal, }); return response.status === 204; diff --git a/lib/hardware.js b/lib/hardware.js index d717548f..e981b1fa 100644 --- a/lib/hardware.js +++ b/lib/hardware.js @@ -1,11 +1,14 @@ /** * List hardware * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the array of hardware */ -async function listHardware() { +async function listHardware({ signal } = {}) { const response = await this.request("/hardware", { method: "GET", + signal, }); return response.json(); diff --git a/lib/models.js b/lib/models.js index 272d9edb..4a3fcdde 100644 --- a/lib/models.js +++ b/lib/models.js @@ -3,11 +3,14 @@ * * @param {string} model_owner - Required. The name of the user or organization that owns the model * @param {string} model_name - Required. The name of the model + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the model data */ -async function getModel(model_owner, model_name) { +async function getModel(model_owner, model_name, { signal } = {}) { const response = await this.request(`/models/${model_owner}/${model_name}`, { method: "GET", + signal, }); return response.json(); @@ -18,13 +21,16 @@ async function getModel(model_owner, model_name) { * * @param {string} model_owner - Required. The name of the user or organization that owns the model * @param {string} model_name - Required. The name of the model + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the list of model versions */ -async function listModelVersions(model_owner, model_name) { +async function listModelVersions(model_owner, model_name, { signal } = {}) { const response = await this.request( `/models/${model_owner}/${model_name}/versions`, { method: "GET", + signal, } ); @@ -37,13 +43,21 @@ async function listModelVersions(model_owner, model_name) { * @param {string} model_owner - Required. The name of the user or organization that owns the model * @param {string} model_name - Required. The name of the model * @param {string} version_id - Required. The model version + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the model version data */ -async function getModelVersion(model_owner, model_name, version_id) { +async function getModelVersion( + model_owner, + model_name, + version_id, + { signal } = {} +) { const response = await this.request( `/models/${model_owner}/${model_name}/versions/${version_id}`, { method: "GET", + signal, } ); @@ -53,11 +67,14 @@ async function getModelVersion(model_owner, model_name, version_id) { /** * List all public models * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the model version data */ -async function listModels() { +async function listModels({ signal } = {}) { const response = await this.request("/models", { method: "GET", + signal, }); return response.json(); @@ -76,14 +93,17 @@ async function listModels() { * @param {string} options.paper_url - A URL for the model's paper. * @param {string} options.license_url - A URL for the model's license. * @param {string} options.cover_image_url - A URL for the model's cover image. This should be an image file. + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the model version data */ async function createModel(model_owner, model_name, options) { - const data = { owner: model_owner, name: model_name, ...options }; + const { signal, ...rest } = options; + const data = { owner: model_owner, name: model_name, ...rest }; const response = await this.request("/models", { method: "POST", data, + signal, }); return response.json(); @@ -93,15 +113,18 @@ async function createModel(model_owner, model_name, options) { * Search for public models * * @param {string} query - The search query + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with a page of models matching the search query */ -async function search(query) { +async function search(query, { signal } = {}) { const response = await this.request("/models", { method: "QUERY", headers: { "Content-Type": "text/plain", }, data: query, + signal, }); return response.json(); diff --git a/lib/predictions.js b/lib/predictions.js index f8e1c5ac..708d04b3 100644 --- a/lib/predictions.js +++ b/lib/predictions.js @@ -10,10 +10,11 @@ const { transformFileInputs } = require("./util"); * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the prediction has new output * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) * @param {boolean|integer} [options.wait] - Whether to wait until the prediction is completed before returning. If an integer is provided, it will wait for that many seconds. Defaults to false + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the created prediction */ async function createPrediction(options) { - const { model, version, input, wait, ...data } = options; + const { model, version, input, wait, signal, ...data } = options; if (data.webhook) { try { @@ -48,6 +49,7 @@ async function createPrediction(options) { ), version, }, + signal, }); } else if (model) { response = await this.request(`/models/${model}/predictions`, { @@ -61,6 +63,7 @@ async function createPrediction(options) { this.fileEncodingStrategy ), }, + signal, }); } else { throw new Error("Either model or version must be specified"); @@ -73,11 +76,14 @@ async function createPrediction(options) { * Fetch a prediction by ID * * @param {number} prediction_id - Required. The prediction ID + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the prediction data */ -async function getPrediction(prediction_id) { +async function getPrediction(prediction_id, { signal } = {}) { const response = await this.request(`/predictions/${prediction_id}`, { method: "GET", + signal, }); return response.json(); @@ -87,11 +93,14 @@ async function getPrediction(prediction_id) { * Cancel a prediction by ID * * @param {string} prediction_id - Required. The training ID + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the data for the training */ -async function cancelPrediction(prediction_id) { +async function cancelPrediction(prediction_id, { signal } = {}) { const response = await this.request(`/predictions/${prediction_id}/cancel`, { method: "POST", + signal, }); return response.json(); @@ -100,11 +109,14 @@ async function cancelPrediction(prediction_id) { /** * List all predictions * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with a page of predictions */ -async function listPredictions() { +async function listPredictions({ signal } = {}) { const response = await this.request("/predictions", { method: "GET", + signal, }); return response.json(); diff --git a/lib/trainings.js b/lib/trainings.js index 6b13dca2..49640b9e 100644 --- a/lib/trainings.js +++ b/lib/trainings.js @@ -9,10 +9,11 @@ * @param {object} options.input - Required. An object with the model inputs * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the training updates * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the data for the created training */ async function createTraining(model_owner, model_name, version_id, options) { - const { ...data } = options; + const { signal, ...data } = options; if (data.webhook) { try { @@ -28,6 +29,7 @@ async function createTraining(model_owner, model_name, version_id, options) { { method: "POST", data, + signal, } ); @@ -38,11 +40,14 @@ async function createTraining(model_owner, model_name, version_id, options) { * Fetch a training by ID * * @param {string} training_id - Required. The training ID + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the data for the training */ -async function getTraining(training_id) { +async function getTraining(training_id, { signal } = {}) { const response = await this.request(`/trainings/${training_id}`, { method: "GET", + signal, }); return response.json(); @@ -52,11 +57,14 @@ async function getTraining(training_id) { * Cancel a training by ID * * @param {string} training_id - Required. The training ID + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the data for the training */ -async function cancelTraining(training_id) { +async function cancelTraining(training_id, { signal } = {}) { const response = await this.request(`/trainings/${training_id}/cancel`, { method: "POST", + signal, }); return response.json(); @@ -65,11 +73,14 @@ async function cancelTraining(training_id) { /** * List all trainings * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with a page of trainings */ -async function listTrainings() { +async function listTrainings({ signal } = {}) { const response = await this.request("/trainings", { method: "GET", + signal, }); return response.json(); diff --git a/lib/util.js b/lib/util.js index 2fa49190..09665774 100644 --- a/lib/util.js +++ b/lib/util.js @@ -247,7 +247,7 @@ async function withAutomaticRetries(request, options = {}) { * @param {Replicate} client - The client used to upload the file * @param {object} inputs - The inputs to transform * @param {"default" | "upload" | "data-uri"} strategy - Whether to upload files to Replicate, encode as dataURIs or try both. - * @returns {object} - The transformed inputs + * @returns {Promise} - The transformed inputs * @throws {ApiError} If the request to upload the file fails */ async function transformFileInputs(client, inputs, strategy) { @@ -280,7 +280,7 @@ async function transformFileInputs(client, inputs, strategy) { * * @param {Replicate} client - The client used to upload the file * @param {object} inputs - The inputs to transform - * @returns {object} - The transformed inputs + * @returns {Promise} - The transformed inputs * @throws {ApiError} If the request to upload the file fails */ async function transformFileInputsToReplicateFileURLs(client, inputs) { @@ -301,8 +301,8 @@ const MAX_DATA_URI_SIZE = 10_000_000; * base64-encoded data URI. * * @param {object} inputs - The inputs to transform - * @returns {object} - The transformed inputs - * @throws {Error} If the size of inputs exceeds a given threshould set by MAX_DATA_URI_SIZE + * @returns {Promise} - The transformed inputs + * @throws {Error} If the size of inputs exceeds a given threshold set by MAX_DATA_URI_SIZE */ async function transformFileInputsToBase64EncodedDataURIs(inputs) { let totalBytes = 0; @@ -311,10 +311,10 @@ async function transformFileInputsToBase64EncodedDataURIs(inputs) { let mime; if (value instanceof Blob) { - // Currently we use a NodeJS only API for base64 encoding, as + // Currently, we use a NodeJS only API for base64 encoding, as // we move to support the browser we could support either using // btoa (which does string encoding), the FileReader API or - // a JavaScript implenentation like base64-js. + // a JavaScript implementation like base64-js. // See: https://developer.mozilla.org/en-US/docs/Glossary/Base64 // See: https://github.com/beatgammit/base64-js buffer = await value.arrayBuffer(); diff --git a/lib/webhooks.js b/lib/webhooks.js index f1324ec7..8da6fdf3 100644 --- a/lib/webhooks.js +++ b/lib/webhooks.js @@ -1,11 +1,14 @@ /** * Get the default webhook signing secret * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the signing secret for the default webhook */ -async function getDefaultWebhookSecret() { +async function getDefaultWebhookSecret({ signal } = {}) { const response = await this.request("/webhooks/default/secret", { method: "GET", + signal, }); return response.json(); From 5ccf9f3bb9247026d983d2810d2a8b951e8c28ed Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Wed, 26 Mar 2025 17:43:47 +0000 Subject: [PATCH 57/68] Update interface for replicate.models.versions.list() (#349) The endpoint returns a paginated list rather than an array --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 709f4665..2b183d04 100644 --- a/index.d.ts +++ b/index.d.ts @@ -315,7 +315,7 @@ declare module "replicate" { model_owner: string, model_name: string, options?: { signal?: AbortSignal } - ): Promise; + ): Promise>; get( model_owner: string, model_name: string, From f8ab2e8ead2997ac0f2e257652037483b7a3ff52 Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Mon, 12 May 2025 14:26:12 -0400 Subject: [PATCH 58/68] 1.1.0-0 (#352) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 300e5382..c0232d31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "1.0.0", + "version": "1.1.0-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "1.0.0", + "version": "1.1.0-0", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index fcd47e8f..8bf27d6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "1.0.0", + "version": "1.1.0-0", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From 7825a4333257062a1fe51428adb536bfd55e8e77 Mon Sep 17 00:00:00 2001 From: Will Sackfield Date: Wed, 20 Aug 2025 15:36:23 -0400 Subject: [PATCH 59/68] Add aborted to Status types (#355) * Aborted is now a first class status --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 2b183d04..83bf34a7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,5 @@ declare module "replicate" { - type Status = "starting" | "processing" | "succeeded" | "failed" | "canceled"; + type Status = "starting" | "processing" | "succeeded" | "failed" | "canceled" | "aborted"; type Visibility = "public" | "private"; type WebhookEventType = "start" | "output" | "logs" | "completed"; From 80f1c37b97eeffbdb6badb6f054b72ac05de2a7d Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Wed, 20 Aug 2025 15:58:34 -0400 Subject: [PATCH 60/68] 1.1.0 (#356) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c0232d31..0b97bfb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "1.1.0-0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "1.1.0-0", + "version": "1.1.0", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index 8bf27d6b..217f6164 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "1.1.0-0", + "version": "1.1.0", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From e6a83b929ba25a9bdb3a8d62e47d2849841e4fdc Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Thu, 18 Sep 2025 09:59:28 -0400 Subject: [PATCH 61/68] feat: expose is_official boolean on models (#358) --- index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.d.ts b/index.d.ts index 83bf34a7..800a524a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -71,6 +71,7 @@ declare module "replicate" { name: string; description?: string; visibility: "public" | "private"; + is_official: boolean; github_url?: string; paper_url?: string; license_url?: string; From ec80783d7b2e7475568b32dfb17a4e7f506cfe66 Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Thu, 18 Sep 2025 10:12:19 -0400 Subject: [PATCH 62/68] 1.2.0 (#359) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b97bfb4..22b3376a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "1.1.0", + "version": "1.2.0", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index 217f6164..3dfa3500 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "1.1.0", + "version": "1.2.0", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From 0f19335f6bc8292e78a8006d3fd3b0e05e4562ce Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Wed, 8 Oct 2025 09:54:07 -0400 Subject: [PATCH 63/68] feat(collection): add typescript info for full_description field (#361) --- index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.d.ts b/index.d.ts index 800a524a..a0cba1db 100644 --- a/index.d.ts +++ b/index.d.ts @@ -25,6 +25,7 @@ declare module "replicate" { name: string; slug: string; description: string; + full_description: string | null; models?: Model[]; } From 922bf8e4621a2d839070f133c348dd517baef926 Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Wed, 8 Oct 2025 09:59:48 -0400 Subject: [PATCH 64/68] 1.3.0 (#362) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22b3376a..4a3430e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "1.2.0", + "version": "1.3.0", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index 3dfa3500..6e76641e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "1.2.0", + "version": "1.3.0", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From 6124a0eebb4cac468a009ef7896e6cee5a15f843 Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Fri, 24 Oct 2025 09:53:50 -0700 Subject: [PATCH 65/68] feat: update types to match OpenAPI schema (#363) * feat: update types to match OpenAPI schema This PR updates TypeScript types to match the OpenAPI schema from https://api.replicate.com/openapi.json Changes: - Account: add avatar_url field - Status: remove "aborted" status (not in OpenAPI schema) - FileObject: replace name, etag, checksum with checksums object - Prediction: add data_removed, deadline, deployment fields; change version to support "hidden"; update metrics to use total_time; add web URL - Training: convert from type alias to full interface with proper output structure - ModelVersion: make cog_version and openapi_schema nullable * fix: keep predict_time in Prediction metrics for backward compatibility * feat: add "aborted" status to Status type Per the OpenAPI schema, both predictions and trainings can have an "aborted" status, which indicates the task was terminated before it started running (e.g., when a deadline is reached before execution begins). * fix: add web URL to Training.urls Per the OpenAPI schema, Training objects include a web URL in their urls object, matching the Prediction interface. --- index.d.ts | 47 ++++++++++++++++++++++++++++++++++++++++------- index.test.ts | 23 +++++++++++------------ 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/index.d.ts b/index.d.ts index a0cba1db..fee693d0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -19,6 +19,7 @@ declare module "replicate" { username: string; name: string; github_url?: string; + avatar_url?: string; } export interface Collection { @@ -48,11 +49,11 @@ declare module "replicate" { export interface FileObject { id: string; - name: string; content_type: string; size: number; - etag: string; - checksum: string; + checksums: { + sha256: string; + }; metadata: Record; created_at: string; expires_at: string | null; @@ -85,22 +86,26 @@ declare module "replicate" { export interface ModelVersion { id: string; created_at: string; - cog_version: string; - openapi_schema: object; + cog_version: string | null; + openapi_schema: object | null; } export interface Prediction { id: string; status: Status; model: string; - version: string; + version: string | "hidden"; input: object; output?: any; // TODO: this should be `unknown` source: "api" | "web"; error?: unknown; logs?: string; + data_removed: boolean; + deadline?: string; + deployment?: string; metrics?: { predict_time?: number; + total_time?: number; }; webhook?: string; webhook_events_filter?: WebhookEventType[]; @@ -111,10 +116,38 @@ declare module "replicate" { get: string; cancel: string; stream?: string; + web?: string; }; } - export type Training = Prediction; + export interface Training { + id: string; + status: Status; + model: string; + version: string; + input: object; + output?: { + version?: string; + weights?: string; + }; + source: "api" | "web"; + error?: unknown; + logs?: string; + metrics?: { + predict_time?: number; + total_time?: number; + }; + webhook?: string; + webhook_events_filter?: WebhookEventType[]; + created_at: string; + started_at?: string; + completed_at?: string; + urls: { + get: string; + cancel: string; + web?: string; + }; + } export type FileEncodingStrategy = "default" | "upload" | "data-uri"; diff --git a/index.test.ts b/index.test.ts index 4905908a..df277bf7 100644 --- a/index.test.ts +++ b/index.test.ts @@ -4,6 +4,7 @@ import Replicate, { FileOutput, Model, Prediction, + Training, validateWebhook, parseProgressFromLogs, } from "replicate"; @@ -906,7 +907,7 @@ describe("Replicate client", () => { next: null, }); - const results: Prediction[] = []; + const results: Training[] = []; for await (const batch of client.paginate(client.trainings.list)) { results.push(...batch); } @@ -1176,11 +1177,11 @@ describe("Replicate client", () => { .post("/files") .reply(200, { id: "123", - name: "test-file", content_type: "application/octet-stream", size: 1024, - etag: "abc123", - checksum: "sha256:1234567890abcdef", + checksums: { + sha256: "1234567890abcdef", + }, metadata: {}, created_at: "2023-01-01T00:00:00Z", expires_at: null, @@ -1190,7 +1191,6 @@ describe("Replicate client", () => { }); const file = await client.files.create(testCase.value); expect(file.id).toBe("123"); - expect(file.name).toBe("test-file"); } }); }); @@ -1201,11 +1201,11 @@ describe("Replicate client", () => { .get("/files/123") .reply(200, { id: "123", - name: "test-file", content_type: "application/octet-stream", size: 1024, - etag: "abc123", - checksum: "sha256:1234567890abcdef", + checksums: { + sha256: "1234567890abcdef", + }, metadata: {}, created_at: "2023-01-01T00:00:00Z", expires_at: null, @@ -1216,7 +1216,6 @@ describe("Replicate client", () => { const file = await client.files.get("123"); expect(file.id).toBe("123"); - expect(file.name).toBe("test-file"); }); }); @@ -1230,11 +1229,11 @@ describe("Replicate client", () => { results: [ { id: "123", - name: "test-file", content_type: "application/octet-stream", size: 1024, - etag: "abc123", - checksum: "sha256:1234567890abcdef", + checksums: { + sha256: "1234567890abcdef", + }, metadata: {}, created_at: "2023-01-01T00:00:00Z", expires_at: null, From 0eac811aaedef1c4b46ee3c60b2040b26a554eb9 Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Fri, 24 Oct 2025 09:55:48 -0700 Subject: [PATCH 66/68] 1.3.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a3430e8..f27bdbf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "1.3.0", + "version": "1.3.1", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index 6e76641e..f88fa2a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "1.3.0", + "version": "1.3.1", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", From 6e544d017a37962d16013b33a45767ae079f6346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sita=20B=C3=A9r=C3=A9t=C3=A9?= Date: Fri, 14 Nov 2025 15:20:44 +0000 Subject: [PATCH 67/68] Fix url key of createFileOutput options for streaming (#350) Fix bug with broken `FileOutput` object being created as part of the streaming API as the wrong value for URL was passed to the constructor. This also extends the `replicate.stream()` interface to accept a `useFileOutput` configuration object. Lastly, we now use the stream URL itself to detect if we should convert URL objects into `FileOutput`. --------- Co-authored-by: Aron Carroll --- biome.json | 6 +--- index.d.ts | 1 + index.js | 12 +++++-- index.test.ts | 99 +++++++++++++++++++++++++++++++++++++++++++++++++-- lib/stream.js | 16 ++++++--- 5 files changed, 121 insertions(+), 13 deletions(-) diff --git a/biome.json b/biome.json index 094cf0ec..ba1c236b 100644 --- a/biome.json +++ b/biome.json @@ -1,11 +1,7 @@ { "$schema": "https://biomejs.dev/schemas/1.0.0/schema.json", "files": { - "ignore": [ - ".wrangler", - "node_modules", - "vendor/*" - ] + "ignore": [".wrangler", "node_modules", "vendor/*"] }, "formatter": { "indentStyle": "space", diff --git a/index.d.ts b/index.d.ts index fee693d0..d4e07840 100644 --- a/index.d.ts +++ b/index.d.ts @@ -208,6 +208,7 @@ declare module "replicate" { webhook?: string; webhook_events_filter?: WebhookEventType[]; signal?: AbortSignal; + useFileOutput?: boolean; } ): AsyncGenerator; diff --git a/index.js b/index.js index b1248e70..3fc14dd5 100644 --- a/index.js +++ b/index.js @@ -315,7 +315,12 @@ class Replicate { * @yields {ServerSentEvent} Each streamed event from the prediction */ async *stream(ref, options) { - const { wait, signal, ...data } = options; + const { + wait, + signal, + useFileOutput = this.useFileOutput, + ...data + } = options; const identifier = ModelVersionIdentifier.parse(ref); @@ -338,7 +343,10 @@ class Replicate { const stream = createReadableStream({ url: prediction.urls.stream, fetch: this.fetch, - ...(signal ? { options: { signal } } : {}), + options: { + useFileOutput, + ...(signal ? { signal } : {}), + }, }); yield* streamAsyncIterator(stream); diff --git a/index.test.ts b/index.test.ts index df277bf7..96f50db7 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1905,8 +1905,12 @@ describe("Replicate client", () => { // Continue with tests for other methods describe("createReadableStream", () => { - function createStream(body: string | ReadableStream, status = 200) { - const streamEndpoint = "https://stream.replicate.com/fake_stream"; + function createStream( + body: string | ReadableStream, + status = 200, + streamEndpoint = "https://stream.replicate.com/fake_stream", + options: { useFileOutput?: boolean } = {} + ) { const fetch = jest.fn((url) => { if (url !== streamEndpoint) { throw new Error(`Unmocked call to fetch() with url: ${url}`); @@ -1916,6 +1920,7 @@ describe("Replicate client", () => { return createReadableStream({ url: streamEndpoint, fetch: fetch as any, + options, }); } @@ -2192,5 +2197,95 @@ describe("Replicate client", () => { ); expect(await iterator.next()).toEqual({ done: true }); }); + + describe("file streams", () => { + test("emits FileOutput objects", async () => { + const stream = createStream( + ` + event: output + id: EVENT_1 + data: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg== + + event: output + id: EVENT_2 + data: https://delivery.replicate.com/my_file.png + + event: done + id: EVENT_3 + data: {} + + `.replace(/^[ ]+/gm, ""), + 200, + "https://stream.replicate.com/v1/files/abcd" + ); + + const iterator = stream[Symbol.asyncIterator](); + const { value: event1 } = await iterator.next(); + expect(event1.data).toBeInstanceOf(ReadableStream); + expect(event1.data.url().href).toEqual( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + ); + + const { value: event2 } = await iterator.next(); + expect(event2.data).toBeInstanceOf(ReadableStream); + expect(event2.data.url().href).toEqual( + "https://delivery.replicate.com/my_file.png" + ); + + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "done", id: "EVENT_3", data: "{}" }, + }); + + expect(await iterator.next()).toEqual({ done: true }); + }); + + test("emits strings when useFileOutput is false", async () => { + const stream = createStream( + ` + event: output + id: EVENT_1 + data: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg== + + event: output + id: EVENT_2 + data: https://delivery.replicate.com/my_file.png + + event: done + id: EVENT_3 + data: {} + + `.replace(/^[ ]+/gm, ""), + 200, + "https://stream.replicate.com/v1/files/abcd", + { useFileOutput: false } + ); + + const iterator = stream[Symbol.asyncIterator](); + + expect(await iterator.next()).toEqual({ + done: false, + value: { + event: "output", + id: "EVENT_1", + data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + }, + }); + expect(await iterator.next()).toEqual({ + done: false, + value: { + event: "output", + id: "EVENT_2", + data: "https://delivery.replicate.com/my_file.png", + }, + }); + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "done", id: "EVENT_3", data: "{}" }, + }); + + expect(await iterator.next()).toEqual({ done: true }); + }); + }); }); }); diff --git a/lib/stream.js b/lib/stream.js index 2c899bd9..802a98e1 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -53,6 +53,7 @@ class ServerSentEvent { */ function createReadableStream({ url, fetch, options = {} }) { const { useFileOutput = true, headers = {}, ...initOptions } = options; + const shouldProcessFileOutput = useFileOutput && isFileStream(url); return new ReadableStream({ async start(controller) { @@ -89,11 +90,11 @@ function createReadableStream({ url, fetch, options = {} }) { let data = event.data; if ( - useFileOutput && - typeof data === "string" && - (data.startsWith("https:") || data.startsWith("data:")) + event.event === "output" && + shouldProcessFileOutput && + typeof data === "string" ) { - data = createFileOutput({ data, fetch }); + data = createFileOutput({ url: data, fetch }); } controller.enqueue(new ServerSentEvent(event.event, data, event.id)); @@ -169,6 +170,13 @@ function createFileOutput({ url, fetch }) { }); } +function isFileStream(url) { + try { + return new URL(url).pathname.startsWith("/v1/files/"); + } catch {} + return false; +} + module.exports = { createFileOutput, createReadableStream, From 2fd6f3944f5e252f2eb7cd4e9e5416786b9c4861 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Mon, 17 Nov 2025 11:01:57 +0000 Subject: [PATCH 68/68] 1.4.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f27bdbf4..6c869967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "replicate", - "version": "1.3.1", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "1.3.1", + "version": "1.4.0", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", diff --git a/package.json b/package.json index f88fa2a3..90cde175 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "1.3.1", + "version": "1.4.0", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme",