From 309738c3af05a440864bc6eca125f83a4d78932c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sita=20B=C3=A9r=C3=A9t=C3=A9?= Date: Sat, 29 Mar 2025 08:02:10 +0800 Subject: [PATCH 1/5] Fix url key of createFileOutput options for streaming --- lib/stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stream.js b/lib/stream.js index 2c899bd..7fcee11 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -93,7 +93,7 @@ function createReadableStream({ url, fetch, options = {} }) { typeof data === "string" && (data.startsWith("https:") || data.startsWith("data:")) ) { - data = createFileOutput({ data, fetch }); + data = createFileOutput({ url: data, fetch }); } controller.enqueue(new ServerSentEvent(event.event, data, event.id)); From c2e5811a6577a9ed1c674be836f482ade888ecef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sita=20B=C3=A9r=C3=A9t=C3=A9?= Date: Sat, 29 Mar 2025 08:22:12 +0800 Subject: [PATCH 2/5] Enable user to controle the use of FileOutput for streaming --- index.d.ts | 1 + index.js | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 2b183d0..678d7a0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -173,6 +173,7 @@ declare module "replicate" { webhook?: string; webhook_events_filter?: WebhookEventType[]; signal?: AbortSignal; + useFileOutput?: boolean; } ): AsyncGenerator; diff --git a/index.js b/index.js index b1248e7..5dbfc12 100644 --- a/index.js +++ b/index.js @@ -315,7 +315,7 @@ 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 +338,10 @@ class Replicate { const stream = createReadableStream({ url: prediction.urls.stream, fetch: this.fetch, - ...(signal ? { options: { signal } } : {}), + options: { + useFileOutput, + ...(signal ? { signal } : {}), + }, }); yield* streamAsyncIterator(stream); From fa7efb8dae7012bf9ad8d8348cb7ba86c1835669 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Fri, 14 Nov 2025 14:57:53 +0000 Subject: [PATCH 3/5] Detect file streams by looking at URL path prefix The file streaming API is hosted at https://stream.replicate.com/v1/files so we can use this prefix to determine that the response will contain file entities. This is far cleaner than relying on the event contents. --- lib/stream.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/stream.js b/lib/stream.js index 7fcee11..802a98e 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,9 +90,9 @@ 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({ url: data, fetch }); } @@ -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 ccfff46ed16e01922c357f894fc8485badbdca7e Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Fri, 14 Nov 2025 14:58:18 +0000 Subject: [PATCH 4/5] Add tests for the file streaming interface --- index.js | 7 +++- index.test.ts | 99 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 5dbfc12..3fc14dd 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, useFileOutput = this.useFileOutput, ...data } = options; + const { + wait, + signal, + useFileOutput = this.useFileOutput, + ...data + } = options; const identifier = ModelVersionIdentifier.parse(ref); diff --git a/index.test.ts b/index.test.ts index 4905908..65eb93e 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1906,8 +1906,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}`); @@ -1917,6 +1921,7 @@ describe("Replicate client", () => { return createReadableStream({ url: streamEndpoint, fetch: fetch as any, + options, }); } @@ -2193,5 +2198,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 }); + }); + }); }); }); From 5e03835240d8ab84f7032c5951320cacb3e5dc63 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Fri, 14 Nov 2025 14:58:26 +0000 Subject: [PATCH 5/5] Format biome.json --- biome.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/biome.json b/biome.json index 094cf0e..ba1c236 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",