From 0cf57c4ca6de9913abdb6e9ebeef165438d41b5d Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Thu, 29 Aug 2024 06:52:31 -0600 Subject: [PATCH 01/47] 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 02/47] 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 03/47] 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 04/47] 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 05/47] 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 06/47] 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 07/47] 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 08/47] 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 09/47] 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 10/47] 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 11/47] 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 12/47] 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 13/47] 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 14/47] 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 15/47] 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 16/47] 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 17/47] 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 18/47] 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 19/47] 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 20/47] 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 21/47] 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 22/47] 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 23/47] 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 24/47] 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 25/47] 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 26/47] 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 27/47] 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 28/47] 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 29/47] 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 30/47] 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 31/47] 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 32/47] 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 33/47] 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 34/47] 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 35/47] 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 36/47] 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 37/47] 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 38/47] 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 39/47] 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 40/47] 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 41/47] 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 42/47] 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 43/47] 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 44/47] 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 45/47] 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 46/47] 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 47/47] 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",