mirror of
https://github.com/Heretek-AI/opencode.git
synced 2026-11-10 02:11:36 -10:00
1288 lines
53 KiB
TypeScript
1288 lines
53 KiB
TypeScript
import { describe, expect } from "bun:test"
|
|
import { Effect, Fiber, Layer, Stream } from "effect"
|
|
import * as TestClock from "effect/testing/TestClock"
|
|
import { Media, Video, VideoClient, type GenerationEvent, type VideoEvent } from "../src/index.js"
|
|
import { Fal, Google, Runway, XAI } from "../src/providers.js"
|
|
import { it } from "./lib/effect.js"
|
|
import { dynamicResponse, json, observe, settle, type Call, type HandlerInput } from "./lib/http.js"
|
|
|
|
const layer = (handler: Parameters<typeof dynamicResponse>[0]) =>
|
|
VideoClient.layer.pipe(Layer.provideMerge(dynamicResponse(handler)))
|
|
|
|
const DAY = 24 * 60 * 60 * 1000
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Google Veo
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("Video / Google Veo", () => {
|
|
const google = Google.configure({
|
|
apiKey: "test",
|
|
baseURL: "https://google.test/v1beta",
|
|
headers: { "x-deployment": "yes" },
|
|
})
|
|
const model = google.video("veo-3.1-generate-preview")
|
|
const operation = "models/veo-3.1-generate-preview/operations/op_1"
|
|
const fileUri = "https://generativelanguage.googleapis.com/v1beta/files/abc:download?alt=media"
|
|
|
|
it.effect("starts a predictLongRunning operation, polls it, and returns an authenticated download URL", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const program = Effect.gen(function* () {
|
|
const generation = yield* Video.start({
|
|
model,
|
|
prompt: "A calico kitten sleeping in the sunshine",
|
|
frames: {
|
|
first: Media.bytes(Uint8Array.from([1, 2, 3]), "image/png"),
|
|
last: Media.fromDataUrl("data:image/jpeg;base64,BAUG"),
|
|
},
|
|
references: [Media.bytes(Uint8Array.from([7, 8, 9]), "image/png")],
|
|
durationSeconds: 8,
|
|
aspectRatio: "16:9",
|
|
resolution: "1080p",
|
|
negativePrompt: "text, watermark",
|
|
seed: 42,
|
|
audio: true,
|
|
providerOptions: { personGeneration: "allow_adult", futureOption: true },
|
|
http: { body: { parameters: { httpOption: "yes" } } },
|
|
})
|
|
expect(generation.id).toBe(operation)
|
|
expect(generation.status).toBe("running")
|
|
expect(generation.token).toEqual({ operation })
|
|
const response = yield* generation.await({ poll: { interval: "1 second" } })
|
|
// Download credentials stay off the wire model: not in `source`, not in JSON, only on the live instance.
|
|
expect(response.video.source).toEqual({
|
|
type: "url",
|
|
url: fileUri,
|
|
mediaType: "video/mp4",
|
|
expiresAt: 1000 + 2 * DAY,
|
|
})
|
|
// Only what `Auth` added travels with the asset; deployment headers stay on the route.
|
|
expect(response.video.headers).toEqual({ "x-goog-api-key": "test" })
|
|
expect(JSON.stringify(response.video)).not.toContain("x-goog-api-key")
|
|
expect(Media.from(response.video.source).headers).toBeUndefined()
|
|
expect(response.notices).toEqual([
|
|
{
|
|
type: "filtered",
|
|
message: "Google Veo filtered media: audio filtered",
|
|
providerMetadata: { google: { raiMediaFilteredReason: "audio filtered" } },
|
|
},
|
|
])
|
|
expect(response.providerMetadata).toEqual({
|
|
google: { operation, raiMediaFilteredCount: 1, metadata: undefined },
|
|
})
|
|
expect(yield* response.video.bytes()).toEqual(Uint8Array.from([9, 9, 9]))
|
|
})
|
|
yield* settle(program, 1).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call, nth } = yield* observe(calls, input)
|
|
expect(call.headers.get("x-goog-api-key")).toBe("test")
|
|
if (call.url !== fileUri) expect(call.headers.get("x-deployment")).toBe("yes")
|
|
if (call.method === "POST") {
|
|
expect(call.url).toBe("https://google.test/v1beta/models/veo-3.1-generate-preview:predictLongRunning")
|
|
expect(JSON.parse(call.body)).toEqual({
|
|
instances: [
|
|
{
|
|
prompt: "A calico kitten sleeping in the sunshine",
|
|
image: { inlineData: { mimeType: "image/png", data: "AQID" } },
|
|
lastFrame: { inlineData: { mimeType: "image/jpeg", data: "BAUG" } },
|
|
referenceImages: [
|
|
{ image: { inlineData: { mimeType: "image/png", data: "BwgJ" } }, referenceType: "asset" },
|
|
],
|
|
},
|
|
],
|
|
parameters: {
|
|
aspectRatio: "16:9",
|
|
resolution: "1080p",
|
|
durationSeconds: 8,
|
|
negativePrompt: "text, watermark",
|
|
seed: 42,
|
|
personGeneration: "allow_adult",
|
|
futureOption: true,
|
|
httpOption: "yes",
|
|
},
|
|
})
|
|
return json(input, { name: operation })
|
|
}
|
|
if (call.url === fileUri) return input.respond(Uint8Array.from([9, 9, 9]))
|
|
expect(call.url).toBe(`https://google.test/v1beta/${operation}`)
|
|
if (nth === 1) return json(input, { name: operation, done: false })
|
|
return json(input, {
|
|
name: operation,
|
|
done: true,
|
|
response: {
|
|
generateVideoResponse: {
|
|
generatedSamples: [{ video: { uri: fileUri } }],
|
|
raiMediaFilteredCount: 1,
|
|
raiMediaFilteredReasons: ["audio filtered"],
|
|
},
|
|
},
|
|
})
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
|
|
"POST https://google.test/v1beta/models/veo-3.1-generate-preview:predictLongRunning",
|
|
`GET https://google.test/v1beta/${operation}`,
|
|
`GET https://google.test/v1beta/${operation}`,
|
|
`GET https://google.test/v1beta/${operation}`,
|
|
`GET ${fileUri}`,
|
|
])
|
|
}),
|
|
)
|
|
|
|
it.effect("hands the asset the auth header that overwrote a deployment header", () =>
|
|
Effect.gen(function* () {
|
|
const generation = yield* Video.start({
|
|
model: Google.configure({
|
|
apiKey: "test",
|
|
baseURL: "https://google.test/v1beta",
|
|
headers: { "x-goog-api-key": "stale" },
|
|
}).video("veo-3.1-generate-preview"),
|
|
prompt: "A kite",
|
|
})
|
|
const response = yield* generation.result()
|
|
expect(response.video.headers).toEqual({ "x-goog-api-key": "test" })
|
|
}).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
input.request.method === "POST"
|
|
? Effect.succeed(json(input, { name: operation }))
|
|
: Effect.succeed(
|
|
json(input, {
|
|
name: operation,
|
|
done: true,
|
|
response: { generateVideoResponse: { generatedSamples: [{ video: { uri: fileUri } }] } },
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
for (const terminal of [
|
|
{ error: { code: 3, message: "Prompt violates policy", status: "INVALID_ARGUMENT" }, tag: "InvalidRequest" },
|
|
{ error: { code: 9, message: "Unsupported resolution", status: "FAILED_PRECONDITION" }, tag: "InvalidRequest" },
|
|
{ error: { code: 11, message: "Duration out of range", status: "OUT_OF_RANGE" }, tag: "InvalidRequest" },
|
|
{ error: { code: 7, message: "Permission denied", status: "PERMISSION_DENIED" }, tag: "Authentication" },
|
|
{ error: { code: 16, message: "Invalid credentials", status: "UNAUTHENTICATED" }, tag: "Authentication" },
|
|
{ error: { code: 8, message: "Quota exceeded", status: "RESOURCE_EXHAUSTED" }, tag: "RateLimit" },
|
|
{ error: { code: 13, message: "Internal error", status: "INTERNAL" }, tag: "ProviderInternal" },
|
|
{ error: { code: 14, message: "Service unavailable", status: "UNAVAILABLE" }, tag: "ProviderInternal" },
|
|
{ error: { message: "Something broke" }, tag: "ProviderInternal" },
|
|
]) {
|
|
it.effect(
|
|
`surfaces ${terminal.error.status ?? "an uncoded"} operation error as ${terminal.tag} with the provider body`,
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const failure = { name: operation, done: true, error: terminal.error }
|
|
const error = yield* Video.generate({ model, prompt: "nope" }).pipe(
|
|
Effect.flip,
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.succeed(
|
|
input.request.method === "POST" ? json(input, { name: operation }) : json(input, failure),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
expect(error.reason._tag).toBe(terminal.tag)
|
|
expect(error.message).toBe(`Google Veo operation failed: ${terminal.error.message}`)
|
|
expect(error.reason.body).toBe(JSON.stringify(failure))
|
|
expect(error.reason.http?.status).toBe(200)
|
|
}),
|
|
)
|
|
}
|
|
|
|
it.effect("reports fully filtered output as a content policy failure", () =>
|
|
Effect.gen(function* () {
|
|
const error = yield* Video.generate({ model, prompt: "nope" }).pipe(
|
|
Effect.flip,
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.succeed(
|
|
input.request.method === "POST"
|
|
? json(input, { name: operation })
|
|
: json(input, {
|
|
done: true,
|
|
response: {
|
|
generateVideoResponse: { raiMediaFilteredCount: 1, raiMediaFilteredReasons: ["safety"] },
|
|
},
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
expect(error.reason._tag).toBe("ContentPolicy")
|
|
expect(error.message).toContain("safety")
|
|
}),
|
|
)
|
|
|
|
it.effect("rejects unsupported inputs before any network call", () =>
|
|
Effect.gen(function* () {
|
|
const cases = [
|
|
Video.generate({ model, prompt: "x", n: 2 }),
|
|
Video.generate({ model, prompt: "x", audio: false }),
|
|
Video.generate({ model, prompt: "x", frames: { last: Media.bytes(Uint8Array.from([1]), "image/png") } }),
|
|
Video.generate({ model, prompt: "x", frames: { first: Media.url("https://example.test/first.png") } }),
|
|
]
|
|
const tags = yield* Effect.forEach(cases, (program) =>
|
|
program.pipe(
|
|
Effect.flip,
|
|
Effect.map((error) => error.reason._tag),
|
|
),
|
|
)
|
|
expect(tags).toEqual(["UnsupportedOperation", "UnsupportedOperation", "InvalidRequest", "InvalidRequest"])
|
|
}).pipe(Effect.provide(layer(() => Effect.die("unsupported input reached the network")))),
|
|
)
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// xAI
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("Video / xAI", () => {
|
|
const xai = XAI.configure({ apiKey: "test", baseURL: "https://xai.test/v1" })
|
|
const model = xai.video("grok-imagine-video-1.5")
|
|
|
|
it.effect("submits a generation, reports pending progress, and returns the temporary URL", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const events: Array<GenerationEvent> = []
|
|
const program = Effect.gen(function* () {
|
|
const generation = yield* Video.start({
|
|
model,
|
|
prompt: "Make the water crash down",
|
|
frames: {
|
|
first: Media.fromDataUrl("data:image/png;base64,AQID"),
|
|
last: Media.url("https://example.test/last.png"),
|
|
},
|
|
references: [Media.ref("xai", "file_1")],
|
|
durationSeconds: 10,
|
|
aspectRatio: "16:9",
|
|
resolution: "720p",
|
|
audio: false,
|
|
providerOptions: { reference_audios: [{ voice_id: "eve" }], future_option: true },
|
|
http: { headers: { "x-request": "yes" }, query: { trace: "1" } },
|
|
})
|
|
expect(generation.token).toEqual({ requestID: "req_1" })
|
|
events.push(...(yield* generation.events({ poll: { interval: "1 second" } }).pipe(Stream.runCollect)))
|
|
return yield* generation.await()
|
|
})
|
|
const response = yield* settle(program, 2).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call, nth } = yield* observe(calls, input)
|
|
expect(call.headers.get("authorization")).toBe("Bearer test")
|
|
// The request's own `http` overlay follows the generation into every poll started from it.
|
|
expect(call.headers.get("x-request")).toBe("yes")
|
|
if (call.method === "POST") {
|
|
expect(call.url).toBe("https://xai.test/v1/videos/generations?trace=1")
|
|
expect(JSON.parse(call.body)).toEqual({
|
|
model: "grok-imagine-video-1.5",
|
|
prompt: "Make the water crash down",
|
|
image: { url: "data:image/png;base64,AQID" },
|
|
last_frame: { url: "https://example.test/last.png" },
|
|
reference_images: [{ file_id: "file_1" }],
|
|
duration: 10,
|
|
aspect_ratio: "16:9",
|
|
resolution: "720p",
|
|
generate_audio: false,
|
|
reference_audios: [{ voice_id: "eve" }],
|
|
future_option: true,
|
|
})
|
|
return json(input, { request_id: "req_1" })
|
|
}
|
|
expect(call.url).toBe("https://xai.test/v1/videos/req_1?trace=1")
|
|
if (nth === 1) return json(input, { status: "pending", progress: 40 })
|
|
return json(input, {
|
|
status: "done",
|
|
video: { url: "https://vidgen.x.ai/out.mp4", duration: 10, respect_moderation: true },
|
|
model: "grok-imagine-video-1.5",
|
|
})
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(events).toEqual([
|
|
{ type: "generation-progress", id: "req_1", progress: 0.4 },
|
|
{ type: "generation-finished", id: "req_1", status: "completed" },
|
|
])
|
|
expect(response.video.source).toEqual({ type: "url", url: "https://vidgen.x.ai/out.mp4", mediaType: "video/mp4" })
|
|
expect(response.video.info).toEqual({ durationSeconds: 10 })
|
|
expect(response.notices).toBeUndefined()
|
|
expect(response.providerMetadata).toEqual({ xai: { requestId: "req_1", model: "grok-imagine-video-1.5" } })
|
|
}),
|
|
)
|
|
|
|
it.effect("routes a source video to edits by default and to extensions on request", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const source = Media.url("https://example.test/in.mp4")
|
|
yield* Effect.gen(function* () {
|
|
yield* Video.start({ model, prompt: "brighter", video: source })
|
|
yield* Video.start({ model, prompt: "keep going", video: source, providerOptions: { mode: "extend" } })
|
|
}).pipe(
|
|
Effect.provide(
|
|
layer((input) => observe(calls, input).pipe(Effect.map(() => json(input, { request_id: "req_2" })))),
|
|
),
|
|
)
|
|
expect(calls.map((call) => call.url)).toEqual([
|
|
"https://xai.test/v1/videos/edits",
|
|
"https://xai.test/v1/videos/extensions",
|
|
])
|
|
expect(calls.map((call) => JSON.parse(call.body))).toEqual([
|
|
{ model: "grok-imagine-video-1.5", prompt: "brighter", video: { url: "https://example.test/in.mp4" } },
|
|
{ model: "grok-imagine-video-1.5", prompt: "keep going", video: { url: "https://example.test/in.mp4" } },
|
|
])
|
|
}),
|
|
)
|
|
|
|
for (const terminal of [
|
|
{
|
|
body: { status: "failed", error: { code: "invalid_argument", message: "Prompt cannot be empty." } },
|
|
tag: "InvalidRequest",
|
|
message: "xAI Video generation failed (invalid_argument): Prompt cannot be empty.",
|
|
},
|
|
{
|
|
body: { status: "failed", error: { code: "failed_precondition", message: "Extension is not supported." } },
|
|
tag: "InvalidRequest",
|
|
message: "xAI Video generation failed (failed_precondition): Extension is not supported.",
|
|
},
|
|
{
|
|
body: { status: "failed", error: { code: "permission_denied", message: "Team lacks access." } },
|
|
tag: "Authentication",
|
|
message: "xAI Video generation failed (permission_denied): Team lacks access.",
|
|
},
|
|
{
|
|
body: { status: "failed", error: { code: "service_unavailable", message: "Overloaded." } },
|
|
tag: "ProviderInternal",
|
|
message: "xAI Video generation failed (service_unavailable): Overloaded.",
|
|
},
|
|
{
|
|
body: { status: "failed", error: { code: "internal_error", message: "Generation failed." } },
|
|
tag: "ProviderInternal",
|
|
message: "xAI Video generation failed (internal_error): Generation failed.",
|
|
},
|
|
{
|
|
body: { status: "failed", error: { code: "constructor", message: "Future code." } },
|
|
tag: "ProviderInternal",
|
|
message: "xAI Video generation failed (constructor): Future code.",
|
|
},
|
|
{ body: { status: "expired" }, tag: "InvalidRequest", message: "xAI Video request req_1 expired" },
|
|
]) {
|
|
it.effect(`surfaces ${terminal.body.error?.code ?? terminal.body.status} generations with the provider body`, () =>
|
|
Effect.gen(function* () {
|
|
const error = yield* Video.generate({ model, prompt: "x" }).pipe(Effect.flip)
|
|
expect(error.reason._tag).toBe(terminal.tag)
|
|
expect(error.message).toBe(terminal.message)
|
|
expect(error.reason.body).toBe(JSON.stringify(terminal.body))
|
|
}).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.succeed(
|
|
input.request.method === "POST" ? json(input, { request_id: "req_1" }) : json(input, terminal.body),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
it.effect("flags moderated results as a notice and withheld videos as a content policy failure", () =>
|
|
Effect.gen(function* () {
|
|
const flagged = yield* Video.generate({ model, prompt: "x" }).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.succeed(
|
|
input.request.method === "POST"
|
|
? json(input, { request_id: "req_1" })
|
|
: json(input, {
|
|
status: "done",
|
|
video: { url: "https://vidgen.x.ai/o.mp4", respect_moderation: false },
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
expect(flagged.notices).toEqual([
|
|
{ type: "moderated", message: "xAI Video flagged the generated video for moderation" },
|
|
])
|
|
const withheld = yield* Video.generate({ model, prompt: "x" }).pipe(
|
|
Effect.flip,
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.succeed(
|
|
input.request.method === "POST"
|
|
? json(input, { request_id: "req_1" })
|
|
: json(input, { status: "done", video: { respect_moderation: false } }),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
expect(withheld.reason._tag).toBe("ContentPolicy")
|
|
}),
|
|
)
|
|
|
|
it.effect("rejects seed, negativePrompt, and n before sending", () =>
|
|
Effect.gen(function* () {
|
|
const tags = yield* Effect.forEach(
|
|
[
|
|
Video.generate({ model, prompt: "x", seed: 1 }),
|
|
Video.generate({ model, prompt: "x", negativePrompt: "blur" }),
|
|
Video.generate({ model, prompt: "x", n: 2 }),
|
|
],
|
|
(program) =>
|
|
program.pipe(
|
|
Effect.flip,
|
|
Effect.map((error) => error.reason._tag),
|
|
),
|
|
)
|
|
expect(tags).toEqual(["UnsupportedOperation", "UnsupportedOperation", "UnsupportedOperation"])
|
|
}).pipe(Effect.provide(layer(() => Effect.die("unsupported input reached the network")))),
|
|
)
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// fal
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("Video / fal", () => {
|
|
const fal = Fal.configure({ apiKey: "test", baseURL: "https://queue.fal.test" })
|
|
const model = fal.video("fal-ai/veo3.1")
|
|
const urls = {
|
|
status: "https://queue.fal.test/fal-ai/veo3.1/requests/r1/status",
|
|
response: "https://queue.fal.test/fal-ai/veo3.1/requests/r1",
|
|
cancel: "https://queue.fal.test/fal-ai/veo3.1/requests/r1/cancel",
|
|
}
|
|
const submitted = {
|
|
request_id: "r1",
|
|
status_url: urls.status,
|
|
response_url: urls.response,
|
|
cancel_url: urls.cancel,
|
|
queue_position: 2,
|
|
}
|
|
|
|
it.effect("submits to the queue and follows the provider's status, response, and cancel URLs", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const program = Effect.gen(function* () {
|
|
const generation = yield* Video.start({
|
|
model,
|
|
prompt: "Two person street interview",
|
|
frames: { first: Media.url("https://example.test/first.png") },
|
|
negativePrompt: "blur",
|
|
seed: 7,
|
|
aspectRatio: "9:16",
|
|
resolution: "1080p",
|
|
audio: true,
|
|
providerOptions: { duration: "8s", safety_tolerance: "4" },
|
|
})
|
|
expect(generation.status).toBe("queued")
|
|
expect(generation.position).toBe(2)
|
|
expect(generation.token).toEqual({
|
|
requestID: "r1",
|
|
statusURL: urls.status,
|
|
responseURL: urls.response,
|
|
cancelURL: urls.cancel,
|
|
})
|
|
const queued = yield* generation.refresh()
|
|
expect(queued.status).toBe("queued")
|
|
expect(queued.position).toBe(1)
|
|
const response = yield* generation.await({ poll: { interval: "1 second" } })
|
|
yield* generation.cancel()
|
|
return response
|
|
})
|
|
const response = yield* settle(program, 3).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call, nth } = yield* observe(calls, input)
|
|
expect(call.headers.get("authorization")).toBe("Key test")
|
|
if (call.method === "POST") {
|
|
expect(call.url).toBe("https://queue.fal.test/fal-ai/veo3.1")
|
|
expect(JSON.parse(call.body)).toEqual({
|
|
prompt: "Two person street interview",
|
|
negative_prompt: "blur",
|
|
seed: 7,
|
|
aspect_ratio: "9:16",
|
|
resolution: "1080p",
|
|
generate_audio: true,
|
|
image_url: "https://example.test/first.png",
|
|
duration: "8s",
|
|
safety_tolerance: "4",
|
|
})
|
|
return json(input, submitted)
|
|
}
|
|
if (call.method === "PUT") {
|
|
expect(call.url).toBe(urls.cancel)
|
|
return json(input, { status: "CANCELLATION_REQUESTED" }, { status: 202 })
|
|
}
|
|
if (call.url === urls.response)
|
|
return json(input, {
|
|
video: {
|
|
url: "https://v3.fal.media/out.mp4",
|
|
content_type: "video/mp4",
|
|
file_name: "out.mp4",
|
|
file_size: 10,
|
|
},
|
|
seed: 7,
|
|
has_nsfw_concepts: [false],
|
|
})
|
|
expect(call.url).toBe(urls.status)
|
|
if (nth === 1) return json(input, { status: "IN_QUEUE", queue_position: 1 })
|
|
if (nth === 2) return json(input, { status: "IN_QUEUE", queue_position: 0 })
|
|
if (nth === 3) return json(input, { status: "IN_PROGRESS", logs: [{ message: "Generating..." }] })
|
|
return json(input, { status: "COMPLETED", metrics: { inference_time: 3.2 } })
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(response.video.source).toEqual({
|
|
type: "url",
|
|
url: "https://v3.fal.media/out.mp4",
|
|
mediaType: "video/mp4",
|
|
})
|
|
expect(response.providerMetadata).toEqual({
|
|
fal: { requestId: "r1", seed: 7, fileName: "out.mp4", fileSize: 10, has_nsfw_concepts: [false] },
|
|
})
|
|
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
|
|
"POST https://queue.fal.test/fal-ai/veo3.1",
|
|
`GET ${urls.status}`,
|
|
`GET ${urls.status}`,
|
|
`GET ${urls.status}`,
|
|
`GET ${urls.status}`,
|
|
`GET ${urls.response}`,
|
|
`PUT ${urls.cancel}`,
|
|
])
|
|
}),
|
|
)
|
|
|
|
it.effect("treats a COMPLETED status carrying an error as failed", () =>
|
|
Effect.gen(function* () {
|
|
const generation = yield* Video.start({ model, prompt: "x" })
|
|
const failed = yield* generation.refresh()
|
|
expect(failed.status).toBe("failed")
|
|
}).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.succeed(
|
|
input.request.method === "POST"
|
|
? json(input, submitted)
|
|
: json(input, { status: "COMPLETED", error: "Invalid input", error_type: "ValidationError" }),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
for (const failure of [
|
|
{
|
|
name: "a COMPLETED status carrying an error",
|
|
status: { status: "COMPLETED", error: "Invalid input", error_type: "ValidationError" },
|
|
result: { status: 422, body: { detail: [{ loc: ["body", "prompt"], msg: "Invalid input" }] } },
|
|
tag: "InvalidRequest",
|
|
},
|
|
{
|
|
name: "a failing response_url",
|
|
status: { status: "COMPLETED" },
|
|
result: { status: 500, body: { detail: "Internal error" } },
|
|
tag: "ProviderInternal",
|
|
},
|
|
]) {
|
|
it.effect(`fails await for ${failure.name} with the response_url body and HTTP context`, () =>
|
|
Effect.gen(function* () {
|
|
// A transient 500 on the result fetch is retried first; the body and HTTP context survive the final failure.
|
|
const fiber = yield* Effect.forkChild(Video.generate({ model, prompt: "x" }).pipe(Effect.flip))
|
|
yield* TestClock.adjust("5 minutes")
|
|
const error = yield* Fiber.join(fiber)
|
|
expect(error.reason._tag).toBe(failure.tag)
|
|
expect(error.reason.body).toBe(JSON.stringify(failure.result.body))
|
|
expect(error.reason.http).toMatchObject({ url: urls.response, status: failure.result.status })
|
|
}).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.succeed(
|
|
input.request.method === "POST"
|
|
? json(input, submitted)
|
|
: input.request.url === urls.response
|
|
? json(input, failure.result.body, { status: failure.result.status })
|
|
: json(input, failure.status),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
it.effect("rejects model-specific common fields and points at providerOptions", () =>
|
|
Effect.gen(function* () {
|
|
const errors = yield* Effect.forEach(
|
|
[
|
|
Video.generate({ model, prompt: "x", durationSeconds: 8 }),
|
|
Video.generate({ model, prompt: "x", frames: { last: Media.url("https://example.test/last.png") } }),
|
|
Video.generate({ model, prompt: "x", references: [Media.url("https://example.test/ref.png")] }),
|
|
Video.generate({ model, prompt: "x", n: 2 }),
|
|
Video.generate({ model, prompt: "x", frames: { first: Media.ref("fal", "handle") } }),
|
|
],
|
|
(program) => program.pipe(Effect.flip),
|
|
)
|
|
expect(errors.map((error) => error.reason._tag)).toEqual([
|
|
"UnsupportedOperation",
|
|
"UnsupportedOperation",
|
|
"UnsupportedOperation",
|
|
"UnsupportedOperation",
|
|
"InvalidRequest",
|
|
])
|
|
expect(errors[1].message).toContain("end_image_url")
|
|
expect(errors[4].message).toContain("; got fal:handle")
|
|
}).pipe(Effect.provide(layer(() => Effect.die("unsupported input reached the network")))),
|
|
)
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Runway
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("Video / Runway", () => {
|
|
const runway = Runway.configure({ apiKey: "test", baseURL: "https://runway.test/v1" })
|
|
const model = runway.video("gen4.5")
|
|
const taskUrl = "https://runway.test/v1/tasks/task_1"
|
|
|
|
it.effect("submits image_to_video, polls the task, reports credits, and keeps the finished task on cancel", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const program = Effect.gen(function* () {
|
|
const generation = yield* Video.start({
|
|
model,
|
|
prompt: "The kite lifts off",
|
|
frames: {
|
|
first: Media.url("https://example.test/first.png"),
|
|
last: Media.ref("runway", "runway://upload-token"),
|
|
},
|
|
aspectRatio: "1280:720",
|
|
durationSeconds: 5,
|
|
seed: 3,
|
|
audio: true,
|
|
providerOptions: { contentModeration: { publicFigureThreshold: "low" } },
|
|
})
|
|
expect(generation.status).toBe("queued")
|
|
expect(generation.token).toEqual({ taskID: "task_1" })
|
|
const response = yield* generation.await({ poll: { interval: "1 second" } })
|
|
yield* generation.cancel()
|
|
return response
|
|
})
|
|
const response = yield* settle(program, 3).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call, nth } = yield* observe(calls, input)
|
|
expect(call.headers.get("authorization")).toBe("Bearer test")
|
|
expect(call.headers.get("x-runway-version")).toBe("2024-11-06")
|
|
if (call.method === "POST") {
|
|
expect(call.url).toBe("https://runway.test/v1/image_to_video")
|
|
expect(JSON.parse(call.body)).toEqual({
|
|
model: "gen4.5",
|
|
promptText: "The kite lifts off",
|
|
promptImage: [
|
|
{ uri: "https://example.test/first.png", position: "first" },
|
|
{ uri: "runway://upload-token", position: "last" },
|
|
],
|
|
ratio: "1280:720",
|
|
duration: 5,
|
|
seed: 3,
|
|
audio: true,
|
|
contentModeration: { publicFigureThreshold: "low" },
|
|
})
|
|
return json(input, { id: "task_1", estimatedCost: { credits: 25 } })
|
|
}
|
|
expect(call.url).toBe(taskUrl)
|
|
if (call.method === "DELETE") return yield* Effect.die("cancel deleted a finished Runway task")
|
|
if (nth === 1) return json(input, { id: "task_1", status: "PENDING", estimatedCost: { credits: 25 } })
|
|
if (nth === 2) return json(input, { id: "task_1", status: "THROTTLED", estimatedCost: { credits: 25 } })
|
|
if (nth === 3) return json(input, { id: "task_1", status: "RUNNING", progress: 0.5 })
|
|
return json(input, {
|
|
id: "task_1",
|
|
status: "SUCCEEDED",
|
|
output: ["https://dnznrvs05pmza.cloudfront.net/out.mp4"],
|
|
cost: { credits: 20 },
|
|
})
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(response.video.source).toEqual({
|
|
type: "url",
|
|
url: "https://dnznrvs05pmza.cloudfront.net/out.mp4",
|
|
mediaType: "video/mp4",
|
|
expiresAt: 3000 + DAY,
|
|
})
|
|
expect(response.usage).toEqual({ type: "credits", credits: 20 })
|
|
expect(response.providerMetadata).toEqual({ runway: { taskId: "task_1", estimatedCredits: undefined } })
|
|
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
|
|
"POST https://runway.test/v1/image_to_video",
|
|
`GET ${taskUrl}`,
|
|
`GET ${taskUrl}`,
|
|
`GET ${taskUrl}`,
|
|
`GET ${taskUrl}`,
|
|
`GET ${taskUrl}`,
|
|
`GET ${taskUrl}`,
|
|
])
|
|
}),
|
|
)
|
|
|
|
it.effect("cancels a task that is still running", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
yield* Effect.gen(function* () {
|
|
const generation = yield* Video.start({ model, prompt: "x" })
|
|
yield* generation.cancel()
|
|
}).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call } = yield* observe(calls, input)
|
|
if (call.method === "POST") return json(input, { id: "task_1" })
|
|
if (call.method === "DELETE") return input.respond(null, { status: 204 })
|
|
return json(input, { id: "task_1", status: "RUNNING", progress: 0.2 })
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
|
|
"POST https://runway.test/v1/text_to_video",
|
|
`GET ${taskUrl}`,
|
|
`DELETE ${taskUrl}`,
|
|
])
|
|
}),
|
|
)
|
|
|
|
it.effect("selects text_to_video with references and video_to_video for a source video", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
yield* Effect.gen(function* () {
|
|
yield* Video.start({
|
|
model,
|
|
prompt: "A robot",
|
|
references: [Media.bytes(Uint8Array.from([1, 2, 3]), "image/png")],
|
|
negativePrompt: "blur",
|
|
resolution: "720p",
|
|
})
|
|
yield* Video.start({ model, prompt: "Restyle", video: Media.url("https://example.test/in.mp4") })
|
|
}).pipe(
|
|
Effect.provide(layer((input) => observe(calls, input).pipe(Effect.map(() => json(input, { id: "task_2" }))))),
|
|
)
|
|
expect(calls.map((call) => call.url)).toEqual([
|
|
"https://runway.test/v1/text_to_video",
|
|
"https://runway.test/v1/video_to_video",
|
|
])
|
|
expect(JSON.parse(calls[0].body)).toEqual({
|
|
model: "gen4.5",
|
|
promptText: "A robot",
|
|
references: [{ uri: "data:image/png;base64,AQID" }],
|
|
resolution: "720p",
|
|
negativePrompt: "blur",
|
|
})
|
|
expect(JSON.parse(calls[1].body)).toEqual({
|
|
model: "gen4.5",
|
|
promptText: "Restyle",
|
|
videoUri: "https://example.test/in.mp4",
|
|
})
|
|
}),
|
|
)
|
|
|
|
for (const terminal of [
|
|
{
|
|
body: { status: "FAILED", failure: "Input image flagged", failureCode: "SAFETY.INPUT.IMAGE" },
|
|
tag: "ContentPolicy",
|
|
message: "Runway task failed (SAFETY.INPUT.IMAGE): Input image flagged",
|
|
},
|
|
{
|
|
body: { status: "FAILED", failure: "Something broke", failureCode: "INTERNAL.BAD_OUTPUT.CODE01" },
|
|
tag: "ProviderInternal",
|
|
message: "Runway task failed (INTERNAL.BAD_OUTPUT.CODE01): Something broke",
|
|
},
|
|
{
|
|
body: { status: "FAILED", failure: "Unsupported dimensions", failureCode: "ASSET.INVALID" },
|
|
tag: "InvalidRequest",
|
|
message: "Runway task failed (ASSET.INVALID): Unsupported dimensions",
|
|
},
|
|
{ body: { status: "CANCELLED" }, tag: "InvalidRequest", message: "Runway task task_1 was cancelled" },
|
|
]) {
|
|
it.effect(`surfaces ${terminal.body.failureCode ?? terminal.body.status} with the task body`, () =>
|
|
Effect.gen(function* () {
|
|
const error = yield* Video.generate({ model, prompt: "x" }).pipe(Effect.flip)
|
|
expect(error.reason._tag).toBe(terminal.tag)
|
|
expect(error.message).toBe(terminal.message)
|
|
expect(error.reason.body).toBe(JSON.stringify(terminal.body))
|
|
}).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.succeed(
|
|
input.request.method === "POST" ? json(input, { id: "task_1" }) : json(input, terminal.body),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
it.effect("rejects n before sending", () =>
|
|
Video.generate({ model, prompt: "x", n: 2 }).pipe(
|
|
Effect.flip,
|
|
Effect.tap((error) => Effect.sync(() => expect(error.reason._tag).toBe("UnsupportedOperation"))),
|
|
Effect.provide(layer(() => Effect.die("unsupported input reached the network"))),
|
|
),
|
|
)
|
|
|
|
it.effect("resumes from a JSON round-tripped token and rejects foreign tokens", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const program = Effect.gen(function* () {
|
|
const started = yield* Video.start({ model, prompt: "x" })
|
|
const token: unknown = JSON.parse(JSON.stringify(started.token))
|
|
const resumed = yield* Video.resume(model, token)
|
|
expect(resumed.status).toBe("running")
|
|
expect(resumed.progress).toBe(0.25)
|
|
expect(resumed.token).toEqual({ taskID: "task_1" })
|
|
const response = yield* resumed.await({ poll: { interval: "1 second" } })
|
|
expect(response.videos).toHaveLength(1)
|
|
const foreign = yield* Video.resume(model, { operation: "models/x/operations/y" }).pipe(Effect.flip)
|
|
expect(foreign.reason._tag).toBe("InvalidRequest")
|
|
expect(foreign.message).toContain("cannot resume")
|
|
})
|
|
yield* settle(program, 2).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call, nth } = yield* observe(calls, input)
|
|
if (call.method === "POST") return json(input, { id: "task_1" })
|
|
if (nth <= 2) return json(input, { status: "RUNNING", progress: 0.25 })
|
|
return json(input, { status: "SUCCEEDED", output: ["https://runway.test/out.mp4"] })
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(calls.filter((call) => call.method === "GET")).toHaveLength(4)
|
|
}),
|
|
)
|
|
|
|
it.effect("streams queue and progress observations followed by the video and finish events", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const program = Video.stream({ model, prompt: "x" }, { poll: { interval: "1 second" } }).pipe(Stream.runCollect)
|
|
const events = Array.from(
|
|
yield* settle(program, 3).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call, nth } = yield* observe(calls, input)
|
|
if (call.method === "POST") return json(input, { id: "task_1" })
|
|
if (nth === 1) return json(input, { status: "PENDING" })
|
|
if (nth === 2) return json(input, { status: "RUNNING", progress: 0.5 })
|
|
return json(input, {
|
|
status: "SUCCEEDED",
|
|
output: ["https://runway.test/out.mp4"],
|
|
cost: { credits: 5 },
|
|
})
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
expect(events.map((event) => event.type)).toEqual(["generation-queued", "generation-progress", "video", "finish"])
|
|
expect(events[1]).toEqual({ type: "generation-progress", id: "task_1", progress: 0.5 })
|
|
expect(events[3]).toMatchObject({ type: "finish", usage: { type: "credits", credits: 5 } })
|
|
}),
|
|
)
|
|
|
|
it.effect("streams the observations of a failed task and then fails with the task body", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const events: Array<VideoEvent> = []
|
|
const failed = { status: "FAILED", failure: "Something broke", failureCode: "INTERNAL.BAD_OUTPUT.CODE01" }
|
|
const program = Video.stream({ model, prompt: "x" }, { poll: { interval: "1 second" } }).pipe(
|
|
Stream.runForEach((event) => Effect.sync(() => events.push(event))),
|
|
Effect.flip,
|
|
)
|
|
const error = yield* settle(program, 3).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call, nth } = yield* observe(calls, input)
|
|
if (call.method === "POST") return json(input, { id: "task_1" })
|
|
if (nth === 1) return json(input, { status: "PENDING" })
|
|
if (nth === 2) return json(input, { status: "RUNNING", progress: 0.5 })
|
|
return json(input, failed)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(events).toEqual([
|
|
{ type: "generation-queued", id: "task_1", position: undefined },
|
|
{ type: "generation-progress", id: "task_1", progress: 0.5 },
|
|
])
|
|
expect(error.reason._tag).toBe("ProviderInternal")
|
|
expect(error.message).toBe("Runway task failed (INTERNAL.BAD_OUTPUT.CODE01): Something broke")
|
|
expect(error.reason.body).toBe(JSON.stringify(failed))
|
|
expect(error.reason.http?.status).toBe(200)
|
|
}),
|
|
)
|
|
|
|
it.effect("fails a stream with a Timeout reason once polling passes the poll deadline", () =>
|
|
Effect.gen(function* () {
|
|
const program = Video.stream(
|
|
{ model, prompt: "x" },
|
|
{ poll: { interval: "1 second", timeout: "2 seconds" } },
|
|
).pipe(Stream.runCollect, Effect.flip)
|
|
const error = yield* settle(program, 3).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.succeed(
|
|
input.request.method === "POST" ? json(input, { id: "task_1" }) : json(input, { status: "RUNNING" }),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
expect(error.reason._tag).toBe("Timeout")
|
|
expect(error.message).toContain("task_1")
|
|
}),
|
|
)
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Transient read failures
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("Video / transient read failures", () => {
|
|
const model = Runway.configure({ apiKey: "test", baseURL: "https://runway.test/v1" }).video("gen4.5")
|
|
const succeeded = { id: "task_1", status: "SUCCEEDED", output: ["https://runway.test/out.mp4"] }
|
|
const failure = (input: HandlerInput, status: number, headers?: Record<string, string>) =>
|
|
json(input, { error: `HTTP ${status}` }, { status, headers })
|
|
const methods = (calls: ReadonlyArray<Call>) => calls.map((call) => call.method)
|
|
|
|
it.effect("retries a 503 status poll and a 503 result read, then returns the result", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const response = yield* settle(
|
|
Video.generate({ model, prompt: "x" }, { poll: { interval: "1 second" } }),
|
|
5,
|
|
).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call, nth } = yield* observe(calls, input)
|
|
if (call.method === "POST") return json(input, { id: "task_1" })
|
|
// 1: status fails, 2: status succeeds, 3: result fails, 4: result succeeds.
|
|
if (nth === 1 || nth === 3) return failure(input, 503)
|
|
return json(input, succeeded)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(response.video.source).toMatchObject({ type: "url", url: "https://runway.test/out.mp4" })
|
|
expect(methods(calls)).toEqual(["POST", "GET", "GET", "GET", "GET"])
|
|
}),
|
|
)
|
|
|
|
it.effect("waits for a 429 retry-after before polling again", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const fiber = yield* Effect.forkChild(
|
|
Video.generate({ model, prompt: "x" }, { poll: { interval: "1 second" } }).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call, nth } = yield* observe(calls, input)
|
|
if (call.method === "POST") return json(input, { id: "task_1" })
|
|
if (nth === 1) return failure(input, 429, { "retry-after": "10" })
|
|
return json(input, succeeded)
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
yield* TestClock.adjust("9 seconds")
|
|
expect(methods(calls)).toEqual(["POST", "GET"])
|
|
yield* TestClock.adjust("1 second")
|
|
yield* Fiber.join(fiber)
|
|
expect(methods(calls)).toEqual(["POST", "GET", "GET", "GET"])
|
|
}),
|
|
)
|
|
|
|
it.effect("fails a 400 status poll without retrying", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const error = yield* Video.generate({ model, prompt: "x" }).pipe(
|
|
Effect.flip,
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call } = yield* observe(calls, input)
|
|
return call.method === "POST" ? json(input, { id: "task_1" }) : failure(input, 400)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(error.reason._tag).toBe("InvalidRequest")
|
|
expect(methods(calls)).toEqual(["POST", "GET"])
|
|
}),
|
|
)
|
|
|
|
it.effect("stops retrying at poll.timeout with a Timeout reason", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const error = yield* settle(
|
|
Video.generate({ model, prompt: "x" }, { poll: { interval: "1 second", timeout: "5 seconds" } }).pipe(
|
|
Effect.flip,
|
|
),
|
|
6,
|
|
).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call } = yield* observe(calls, input)
|
|
return call.method === "POST" ? json(input, { id: "task_1" }) : failure(input, 503)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(error.reason._tag).toBe("Timeout")
|
|
expect(calls.filter((call) => call.method === "GET").length).toBeGreaterThan(1)
|
|
}),
|
|
)
|
|
|
|
it.effect("bounds a streamed result read's retries by poll.timeout", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const error = yield* settle(
|
|
Video.stream({ model, prompt: "x" }, { poll: { interval: "1 second", timeout: "5 seconds" } }).pipe(
|
|
Stream.runCollect,
|
|
Effect.flip,
|
|
),
|
|
6,
|
|
).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call, nth } = yield* observe(calls, input)
|
|
if (call.method === "POST") return json(input, { id: "task_1" })
|
|
return nth === 1 ? json(input, succeeded) : failure(input, 503)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(error.reason._tag).toBe("Timeout")
|
|
expect(calls.filter((call) => call.method === "GET").length).toBeGreaterThan(2)
|
|
}),
|
|
)
|
|
|
|
it.effect("never retries a failed submit", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const error = yield* Video.generate({ model, prompt: "x" }).pipe(
|
|
Effect.flip,
|
|
Effect.provide(layer((input) => observe(calls, input).pipe(Effect.map(() => failure(input, 503))))),
|
|
)
|
|
expect(error.reason._tag).toBe("ProviderInternal")
|
|
expect(methods(calls)).toEqual(["POST"])
|
|
}),
|
|
)
|
|
|
|
it.effect("never retries a failed cancel", () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const error = yield* Effect.gen(function* () {
|
|
const generation = yield* Video.start({ model, prompt: "x" })
|
|
return yield* generation.cancel().pipe(Effect.flip)
|
|
}).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call } = yield* observe(calls, input)
|
|
if (call.method === "POST") return json(input, { id: "task_1" })
|
|
if (call.method === "DELETE") return failure(input, 503)
|
|
return json(input, { id: "task_1", status: "RUNNING" })
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(error.reason._tag).toBe("ProviderInternal")
|
|
expect(methods(calls)).toEqual(["POST", "GET", "DELETE"])
|
|
}),
|
|
)
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared queued behavior
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("Video / queued result", () => {
|
|
for (const pending of [
|
|
{
|
|
model: Google.configure({ apiKey: "test", baseURL: "https://google.test/v1beta" }).video("veo-3.1"),
|
|
token: { operation: "models/veo-3.1/operations/op_1" },
|
|
body: { name: "models/veo-3.1/operations/op_1", done: false },
|
|
name: "Google Veo",
|
|
},
|
|
{
|
|
model: XAI.configure({ apiKey: "test", baseURL: "https://xai.test/v1" }).video("grok-imagine-video-1.5"),
|
|
token: { requestID: "req_1" },
|
|
body: { status: "pending", progress: 40 },
|
|
name: "xAI Video",
|
|
},
|
|
{
|
|
model: Runway.configure({ apiKey: "test", baseURL: "https://runway.test/v1" }).video("gen4.5"),
|
|
token: { taskID: "task_1" },
|
|
body: { status: "RUNNING", progress: 0.5 },
|
|
name: "Runway",
|
|
},
|
|
]) {
|
|
it.effect(`rejects reading a ${pending.model.provider} result before the generation finishes`, () =>
|
|
Effect.gen(function* () {
|
|
const generation = yield* Video.resume(pending.model, pending.token)
|
|
const error = yield* generation.result().pipe(Effect.flip)
|
|
expect(error.reason._tag).toBe("InvalidRequest")
|
|
expect(error.message).toBe(
|
|
`${pending.name} generation ${generation.id} has not finished; await it before reading the result`,
|
|
)
|
|
expect(error.reason.body).toBe(JSON.stringify(pending.body))
|
|
expect(error.reason.http?.status).toBe(200)
|
|
}).pipe(Effect.provide(layer((input) => Effect.succeed(json(input, pending.body))))),
|
|
)
|
|
}
|
|
|
|
const veoOperation = "models/veo-3.1/operations/op_1"
|
|
const falURLs = {
|
|
status: "https://queue.fal.test/fal-ai/veo3.1/requests/r1/status",
|
|
response: "https://queue.fal.test/fal-ai/veo3.1/requests/r1",
|
|
cancel: "https://queue.fal.test/fal-ai/veo3.1/requests/r1/cancel",
|
|
}
|
|
for (const queued of [
|
|
{
|
|
model: Google.configure({ apiKey: "test", baseURL: "https://google.test/v1beta" }).video("veo-3.1"),
|
|
submitted: { name: veoOperation },
|
|
token: { operation: veoOperation },
|
|
submitURL: "https://google.test/v1beta/models/veo-3.1:predictLongRunning",
|
|
statusURL: `https://google.test/v1beta/${veoOperation}`,
|
|
resultURL: `https://google.test/v1beta/${veoOperation}`,
|
|
running: { name: veoOperation, done: false },
|
|
done: {
|
|
name: veoOperation,
|
|
done: true,
|
|
response: { generateVideoResponse: { generatedSamples: [{ video: { uri: "https://google.test/out.mp4" } }] } },
|
|
},
|
|
result: undefined,
|
|
url: "https://google.test/out.mp4",
|
|
},
|
|
{
|
|
model: XAI.configure({ apiKey: "test", baseURL: "https://xai.test/v1" }).video("grok-imagine-video-1.5"),
|
|
submitted: { request_id: "req_1" },
|
|
token: { requestID: "req_1" },
|
|
submitURL: "https://xai.test/v1/videos/generations",
|
|
statusURL: "https://xai.test/v1/videos/req_1",
|
|
resultURL: "https://xai.test/v1/videos/req_1",
|
|
running: { status: "pending", progress: 40 },
|
|
done: { status: "done", video: { url: "https://vidgen.x.ai/out.mp4", respect_moderation: true } },
|
|
result: undefined,
|
|
url: "https://vidgen.x.ai/out.mp4",
|
|
},
|
|
{
|
|
model: Fal.configure({ apiKey: "test", baseURL: "https://queue.fal.test" }).video("fal-ai/veo3.1"),
|
|
submitted: {
|
|
request_id: "r1",
|
|
status_url: falURLs.status,
|
|
response_url: falURLs.response,
|
|
cancel_url: falURLs.cancel,
|
|
},
|
|
token: { requestID: "r1", statusURL: falURLs.status, responseURL: falURLs.response, cancelURL: falURLs.cancel },
|
|
submitURL: "https://queue.fal.test/fal-ai/veo3.1",
|
|
statusURL: falURLs.status,
|
|
resultURL: falURLs.response,
|
|
running: { status: "IN_PROGRESS" },
|
|
done: { status: "COMPLETED" },
|
|
result: { video: { url: "https://v3.fal.media/out.mp4" } },
|
|
url: "https://v3.fal.media/out.mp4",
|
|
},
|
|
]) {
|
|
it.effect(`resumes a ${queued.model.provider} generation from a JSON round-tripped token`, () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
const response = yield* Effect.gen(function* () {
|
|
const started = yield* Video.start({ model: queued.model, prompt: "x" })
|
|
const resumed = yield* Video.resume(queued.model, JSON.parse(JSON.stringify(started.token)))
|
|
expect(resumed.status).toBe("running")
|
|
expect(resumed.token).toEqual(queued.token)
|
|
return yield* resumed.await()
|
|
}).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call, nth } = yield* observe(calls, input)
|
|
if (call.method === "POST") return json(input, queued.submitted)
|
|
if (call.url === queued.resultURL && queued.result !== undefined) return json(input, queued.result)
|
|
return json(input, nth === 1 ? queued.running : queued.done)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(response.video.source).toEqual(expect.objectContaining({ type: "url", url: queued.url }))
|
|
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
|
|
`POST ${queued.submitURL}`,
|
|
`GET ${queued.statusURL}`,
|
|
`GET ${queued.statusURL}`,
|
|
`GET ${queued.resultURL}`,
|
|
])
|
|
}),
|
|
)
|
|
}
|
|
|
|
for (const queued of [
|
|
{
|
|
model: Google.configure({ apiKey: "test", baseURL: "https://google.test/v1beta" }).video("veo-3.1"),
|
|
submitted: { name: veoOperation },
|
|
},
|
|
{
|
|
model: XAI.configure({ apiKey: "test", baseURL: "https://xai.test/v1" }).video("grok-imagine-video-1.5"),
|
|
submitted: { request_id: "req_1" },
|
|
},
|
|
]) {
|
|
it.effect(`cancels a ${queued.model.provider} generation without sending a request`, () =>
|
|
Effect.gen(function* () {
|
|
const calls: Array<Call> = []
|
|
yield* Effect.gen(function* () {
|
|
const generation = yield* Video.start({ model: queued.model, prompt: "x" })
|
|
yield* generation.cancel()
|
|
}).pipe(
|
|
Effect.provide(
|
|
layer((input) =>
|
|
Effect.gen(function* () {
|
|
const { call } = yield* observe(calls, input)
|
|
if (call.method !== "POST") return yield* Effect.die(`cancel sent ${call.method} ${call.url}`)
|
|
return json(input, queued.submitted)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
expect(calls.map((call) => call.method)).toEqual(["POST"])
|
|
}),
|
|
)
|
|
}
|
|
|
|
it.effect("rejects a status that only matches an inherited property", () =>
|
|
Effect.gen(function* () {
|
|
const error = yield* Video.resume(
|
|
XAI.configure({ apiKey: "test", baseURL: "https://xai.test/v1" }).video("grok-imagine-video-1.5"),
|
|
{ requestID: "req_1" },
|
|
).pipe(Effect.flip)
|
|
expect(error.reason._tag).toBe("InvalidProviderOutput")
|
|
expect(error.message).toBe('Unknown generation status "constructor"')
|
|
expect(error.reason.body).toBe(JSON.stringify({ status: "constructor" }))
|
|
}).pipe(Effect.provide(layer((input) => Effect.succeed(json(input, { status: "constructor" }))))),
|
|
)
|
|
})
|