diff --git a/packages/ai/src/protocols/assemblyai-transcription.ts b/packages/ai/src/protocols/assemblyai-transcription.ts index 423868c15d7..46c07413225 100644 --- a/packages/ai/src/protocols/assemblyai-transcription.ts +++ b/packages/ai/src/protocols/assemblyai-transcription.ts @@ -158,8 +158,7 @@ const decodeResult = Effect.fn("AssemblyAITranscription.decodeResult")(function* const error = transcript.error ?? undefined if (status === "failed") return yield* output.ended("failed", `${route.name} transcription failed${error === undefined ? "" : `: ${error}`}`) - if (status !== "completed") - return yield* output.invalid(`${route.name} transcript ${context.token.transcriptID} has not finished`) + if (status !== "completed") return yield* output.pending(context.token.transcriptID) const duration = transcript.audio_duration ?? undefined return new TranscriptionResponse({ text: transcript.text ?? "", diff --git a/packages/ai/src/protocols/bfl-images.ts b/packages/ai/src/protocols/bfl-images.ts index 37518f5f1bd..b5f7b532bf0 100644 --- a/packages/ai/src/protocols/bfl-images.ts +++ b/packages/ai/src/protocols/bfl-images.ts @@ -181,7 +181,8 @@ const decodeResult = Effect.fn("BlackForestLabsImages.decodeResult")(function* ( if (isModerated(document.status)) return yield* output.contentPolicy(`${route.name} moderated the generation`) if (status === "failed" || status === "expired") return yield* output.ended(status, `${route.name} generation ${context.token.id} ended with ${document.status}`) - if (status !== "completed" || document.result === undefined || document.result === null) + if (status !== "completed") return yield* output.pending(context.token.id) + if (document.result === undefined || document.result === null) return yield* output.invalid(`${route.name} generation ${context.token.id} has no result`) const { sample, seed, prompt, ...rest } = document.result // A settled `cost` on the result supersedes the submit-time cost carried on the token. diff --git a/packages/ai/src/protocols/google-images.ts b/packages/ai/src/protocols/google-images.ts index 1c93fbf6815..b8f7ff1584c 100644 --- a/packages/ai/src/protocols/google-images.ts +++ b/packages/ai/src/protocols/google-images.ts @@ -101,7 +101,7 @@ const generationConfig = (request: Request) => { const fromRequest = Effect.fn("GoogleImages.fromRequest")(function* (request: Request) { if (request.n !== undefined && request.n > 1) return yield* route.unsupported( - "image.n", + "media.n", `${route.name} generates one image per request; call it once per image instead of n=${request.n}`, ) const parts = yield* Effect.forEach(request.images ?? [], (image) => diff --git a/packages/ai/src/protocols/google-video.ts b/packages/ai/src/protocols/google-video.ts index b877d472c75..a00c4609027 100644 --- a/packages/ai/src/protocols/google-video.ts +++ b/packages/ai/src/protocols/google-video.ts @@ -149,8 +149,7 @@ const decodeResult = Effect.fn("GoogleVideo.decodeResult")(function* ( const output = yield* decodeOperation(response) const operation = output.value const status = statusOf(operation) - if (status === "running") - return yield* output.invalid(`${route.name} operation ${context.token.operation} has not finished`) + if (status === "running") return yield* output.pending(context.token.operation) if (status === "failed") return yield* output.ended( "failed", diff --git a/packages/ai/src/protocols/replicate-images.ts b/packages/ai/src/protocols/replicate-images.ts index 6204267c9e0..087bd57df9e 100644 --- a/packages/ai/src/protocols/replicate-images.ts +++ b/packages/ai/src/protocols/replicate-images.ts @@ -132,8 +132,7 @@ const decodeResult = Effect.fn("ReplicateImages.decodeResult")(function* ( status, `${route.name} prediction ${context.token.id} ${prediction.status}${typeof prediction.error === "string" ? `: ${prediction.error}` : ""}`, ) - if (status !== "completed") - return yield* output.invalid(`${route.name} prediction ${context.token.id} has not finished`) + if (status !== "completed") return yield* output.pending(context.token.id) if (prediction.data_removed === true) return yield* output.ended("expired", `${route.name} removed the output of prediction ${context.token.id}`) if (!isOutput(prediction.output)) diff --git a/packages/ai/src/protocols/runway-video.ts b/packages/ai/src/protocols/runway-video.ts index e81b7c62bbc..c9159c15b3c 100644 --- a/packages/ai/src/protocols/runway-video.ts +++ b/packages/ai/src/protocols/runway-video.ts @@ -141,8 +141,7 @@ const decodeResult = Effect.fn("RunwayVideo.decodeResult")(function* ( } if (status === "cancelled") return yield* output.ended("cancelled", `${route.name} task ${context.token.taskID} was cancelled`) - if (status !== "completed") - return yield* output.invalid(`${route.name} task ${context.token.taskID} has not finished`) + if (status !== "completed") return yield* output.pending(context.token.taskID) const urls = task.output ?? [] if (urls.length === 0) return yield* output.invalid(`${route.name} task succeeded without any output`) return new VideoResponse({ diff --git a/packages/ai/src/protocols/stability-images.ts b/packages/ai/src/protocols/stability-images.ts index 9d61fa5db65..2dcc538df8d 100644 --- a/packages/ai/src/protocols/stability-images.ts +++ b/packages/ai/src/protocols/stability-images.ts @@ -175,7 +175,7 @@ const decodeUpscaleResult = Effect.fn("StabilityImages.decodeUpscaleResult")(fun ) { if (response.status === 202) { const output = yield* upscaleRoute.text(response) - return yield* output.invalid(`${upscaleRoute.name} upscale ${context.token.id} has not finished`) + return yield* output.pending(context.token.id) } return yield* decodeUpscaleImage(response) }) diff --git a/packages/ai/src/protocols/xai-video.ts b/packages/ai/src/protocols/xai-video.ts index a71750987c7..d9e9f2d9edd 100644 --- a/packages/ai/src/protocols/xai-video.ts +++ b/packages/ai/src/protocols/xai-video.ts @@ -136,8 +136,7 @@ const decodeResult = Effect.fn("XAIVideo.decodeResult")(function* ( const output = yield* decodeVideoStatus(response) const decoded = output.value const status = yield* MediaProtocol.status(STATUS, decoded.status, output) - if (status === "running") - return yield* output.invalid(`${route.name} request ${context.token.requestID} has not finished`) + if (status === "running") return yield* output.pending(context.token.requestID) if (status === "failed") { const code = decoded.error?.code ?? undefined const message = decoded.error?.message ?? undefined diff --git a/packages/ai/src/route/media-protocol.ts b/packages/ai/src/route/media-protocol.ts index 0876cf0f89d..89fb70013e5 100644 --- a/packages/ai/src/route/media-protocol.ts +++ b/packages/ai/src/route/media-protocol.ts @@ -200,7 +200,8 @@ export const identity = (input: { readonly id: string; readonly name: string; re /** * Read a text body while retaining the original payload and HTTP context on every downstream error. `invalid` is a * malformed provider document; `ended` is a generation that reached a terminal status without output (`failed` is - * provider-side, `cancelled`/`expired` mean the result will never exist); `contentPolicy` is a moderated result. + * provider-side, `cancelled`/`expired` mean the result will never exist); `pending` is a `result()` read before the + * generation finished, which is caller misuse; `contentPolicy` is a moderated result. */ const text = Effect.fn("MediaProtocol.text")(function* (response: HttpClientResponse.HttpClientResponse) { const http = context(response) @@ -229,6 +230,14 @@ export const identity = (input: { readonly id: string; readonly name: string; re ? new ProviderInternalError({ message, body, http }) : new InvalidRequestError({ message, body, http }), }), + pending: (id: string) => + new AIError({ + reason: new InvalidRequestError({ + message: `${input.name} generation ${id} has not finished; await it before reading the result`, + body, + http, + }), + }), contentPolicy: (message: string) => new AIError({ reason: new ContentPolicyError({ message, body, http }) }), } }) @@ -290,9 +299,8 @@ export const status =