Files
opencode/packages/ai/test/transcription.test.ts
T

713 lines
29 KiB
TypeScript

import { describe, expect } from "bun:test"
import { Effect, Fiber, Layer, Stream } from "effect"
import * as TestClock from "effect/testing/TestClock"
import { HttpClientRequest } from "effect/unstable/http"
import { Media, Transcription, TranscriptionClient, type TranscriptionEvent } from "../src/index.js"
import { AssemblyAI, Deepgram, ElevenLabs, Google, OpenAI } from "../src/providers.js"
import { it } from "./lib/effect.js"
import { dynamicResponse, json, observe, type Call } from "./lib/http.js"
import { sseEvents } from "./lib/sse.js"
const layer = (handler: Parameters<typeof dynamicResponse>[0]) =>
TranscriptionClient.layer.pipe(Layer.provideMerge(dynamicResponse(handler)))
const audio = Media.bytes(Uint8Array.from([0x49, 0x44, 0x33, 1, 2, 3]), "audio/mpeg")
const openai = OpenAI.configure({ apiKey: "test", baseURL: "https://openai.test/v1" })
const deepgram = Deepgram.configure({ apiKey: "test", baseURL: "https://deepgram.test" }).transcription("nova-3")
const google = Google.configure({ apiKey: "test", baseURL: "https://google.test/v1beta" }).transcription(
"gemini-3.5-transcribe",
)
/**
* Multipart fields of a recorded request, with repeated names collected in order. The boundary comes from the body:
* each conversion of a FormData request to a web request picks a fresh one, so the recorded headers may not match.
*/
const formFields = (call: Call) =>
Effect.promise(() =>
new Response(call.body, {
headers: { "content-type": `multipart/form-data; boundary=${call.body.slice(2, call.body.indexOf("\r\n"))}` },
}).formData(),
).pipe(
Effect.map((form) =>
Object.fromEntries([...new Set(form.keys())].map((key) => [key, form.getAll(key).map((value) => String(value))])),
),
)
const assemblyai = AssemblyAI.configure({ apiKey: "aai-key", baseURL: "https://assemblyai.test" }).transcription(
"universal-3-5-pro",
)
const elevenlabs = ElevenLabs.configure({ apiKey: "test", baseURL: "https://elevenlabs.test" }).transcription(
"scribe_v2",
)
describe("Transcription", () => {
it.effect("rejects what a route cannot honor before sending anything", () =>
Effect.gen(function* () {
const errors = yield* Effect.all(
[
Transcription.generate({ model: openai.transcription("gpt-4o-mini-transcribe"), audio, diarize: true }),
Transcription.generate({ model: openai.transcription("gpt-4o-mini-transcribe"), audio, timestamps: "word" }),
Transcription.generate({ model: openai.transcription("gpt-4o-transcribe-diarize"), audio, prompt: "Names" }),
Transcription.generate({ model: deepgram, audio, prompt: "OpenCode" }),
Transcription.generate({ model: google, audio, speakers: 2 }),
Transcription.start({ model: deepgram, audio }),
Transcription.generate({ model: deepgram, audio, http: { body: { callback: "https://hook.test" } } }),
Transcription.generate({
model: openai.transcription("gpt-transcribe"),
audio: Media.url("https://a.test/x.mp3"),
}),
Transcription.start({
model: assemblyai,
audio: Media.ref("file_1", { provider: "openai", mediaType: "audio/mpeg" }),
}),
Transcription.generate({
model: openai.transcription("gpt-transcribe"),
audio: Media.bytes(Uint8Array.from([1, 2, 3]), "audio/x-unknown"),
}),
Transcription.generate({
model: Google.configure({ apiKey: "test" }).transcription("gemini-3.6-flash"),
audio,
}),
].map((effect) => Effect.flip(effect)),
)
expect(errors.map((error) => [error.reason._tag, "operation" in error.reason && error.reason.operation])).toEqual(
[
["UnsupportedOperation", "media.diarize"],
["UnsupportedOperation", "media.timestamps"],
["UnsupportedOperation", "media.prompt"],
["UnsupportedOperation", "media.prompt"],
["UnsupportedOperation", "media.speakers"],
["UnsupportedOperation", "transcription.start"],
["InvalidRequest", false],
["InvalidRequest", false],
["InvalidRequest", false],
["InvalidRequest", false],
["UnsupportedOperation", "transcription.model"],
],
)
}).pipe(Effect.provide(layer(() => Effect.die("an unsupported request reached the network")))),
)
it.effect("ignores unknown OpenAI stream events and fails on an error event with the frame", () =>
Effect.gen(function* () {
const sse = (...frames: ReadonlyArray<string>) => frames.map((frame) => `data: ${frame}\n\n`).join("")
const failure = `{"type":"error","error":{"type":"server_error","code":"server_error","message":"The server had an error"}}`
const bodies = [
sse(
`{"type":"transcript.text.delta","delta":"Hi"}`,
`{"type":"transcript.text.future","payload":1}`,
`{"type":"transcript.text.done","text":"Hi"}`,
"[DONE]",
),
sse(`{"type":"transcript.text.delta","delta":"Hi"}`, failure),
]
const model = openai.transcription("gpt-4o-mini-transcribe")
const program = Effect.gen(function* () {
const events = Array.from(yield* Stream.runCollect(Transcription.stream({ model, audio })))
const error = yield* Stream.runCollect(Transcription.stream({ model, audio })).pipe(Effect.flip)
return { events, error }
})
const { events, error } = yield* program.pipe(
Effect.provide(
layer((input) =>
Effect.sync(() =>
input.respond(bodies.shift() ?? "", { headers: { "content-type": "text/event-stream" } }),
),
),
),
)
expect(events.map((event) => event.type)).toEqual(["text-delta", "finish"])
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", body: failure })
expect(error.message).toContain("The server had an error")
}),
)
it.effect("streams whisper-1 as a single finish from a plain request", () =>
Effect.gen(function* () {
const bodies: Array<string> = []
const events = Array.from(
yield* Stream.runCollect(Transcription.stream({ model: openai.transcription("whisper-1"), audio })).pipe(
Effect.provide(
layer((input) =>
Effect.sync(() => {
bodies.push(input.text)
return input.respond(
JSON.stringify({ text: "Hello there.", usage: { type: "duration", seconds: 2 } }),
{ headers: { "content-type": "application/json" } },
)
}),
),
),
),
)
expect(bodies[0]).not.toContain('name="stream"')
expect(events).toEqual([
expect.objectContaining({ type: "finish", text: "Hello there.", usage: { type: "seconds", seconds: 2 } }),
])
}),
)
it.effect("streams diarized segments and finishes with the accumulated segments", () =>
Effect.gen(function* () {
const calls: Array<Call> = []
const body = sseEvents(
{ type: "transcript.text.segment", id: "seg_0", text: " Hello", start: 0.25, end: 0.7, speaker: "A" },
{ type: "transcript.text.segment", id: "seg_1", text: " there.", start: 0.7, end: 1.25, speaker: "B" },
{ type: "transcript.text.done", text: "Hello there.", usage: { type: "duration", seconds: 2 } },
)
const events = Array.from(
yield* Stream.runCollect(
Transcription.stream({ model: openai.transcription("gpt-4o-transcribe-diarize"), audio, diarize: true }),
).pipe(
Effect.provide(
layer((input) =>
observe(calls, input).pipe(
Effect.as(input.respond(body, { headers: { "content-type": "text/event-stream" } })),
),
),
),
),
)
const form = yield* formFields(calls[0])
expect(form).toMatchObject({
model: ["gpt-4o-transcribe-diarize"],
response_format: ["diarized_json"],
chunking_strategy: ["auto"],
stream: ["true"],
})
const segments = [
{ text: "Hello", startSeconds: 0.25, endSeconds: 0.7, speaker: "A" },
{ text: "there.", startSeconds: 0.7, endSeconds: 1.25, speaker: "B" },
]
expect(events).toEqual([
{ type: "segment", segment: segments[0] },
{ type: "segment", segment: segments[1] },
expect.objectContaining({
type: "finish",
text: "Hello there.",
segments,
usage: { type: "seconds", seconds: 2 },
}),
])
}),
)
it.effect("requests whisper-1 segment timestamps as verbose_json", () =>
Effect.gen(function* () {
const calls: Array<Call> = []
const response = yield* Transcription.generate({
model: openai.transcription("whisper-1"),
audio,
timestamps: "segment",
}).pipe(
Effect.provide(
layer((input) =>
observe(calls, input).pipe(
Effect.as(
json(input, {
text: "Hello there.",
language: "English",
duration: 1.25,
segments: [
{ id: 0, text: " Hello", start: 0.25, end: 0.7 },
{ id: 1, text: " there.", start: 0.7, end: 1.25 },
],
usage: { type: "duration", seconds: 2 },
}),
),
),
),
),
)
const form = yield* formFields(calls[0])
expect(form).toMatchObject({
model: ["whisper-1"],
response_format: ["verbose_json"],
"timestamp_granularities[]": ["segment"],
})
expect(form.stream).toBeUndefined()
expect(response).toMatchObject({
text: "Hello there.",
segments: [
{ text: "Hello", startSeconds: 0.25, endSeconds: 0.7 },
{ text: "there.", startSeconds: 0.7, endSeconds: 1.25 },
],
language: "english",
durationSeconds: 1.25,
usage: { type: "seconds", seconds: 2 },
})
}),
)
it.effect("fails an OpenAI stream that ends without transcript.text.done as incomplete", () =>
Effect.gen(function* () {
const events: Array<TranscriptionEvent> = []
const error = yield* Transcription.stream({ model: openai.transcription("gpt-4o-mini-transcribe"), audio }).pipe(
Stream.runForEach((event) => Effect.sync(() => events.push(event))),
Effect.flip,
Effect.provide(
layer((input) =>
Effect.succeed(
input.respond(sseEvents({ type: "transcript.text.delta", delta: "Hel" }), {
headers: { "content-type": "text/event-stream" },
}),
),
),
),
)
expect(events).toEqual([{ type: "text-delta", delta: "Hel" }])
expect(error.reason).toMatchObject({ _tag: "InvalidProviderOutput", classification: "incomplete-stream" })
expect(error.reason.http?.status).toBe(200)
}),
)
it.effect("sends a Deepgram URL source as a JSON body and repeats array query parameters", () =>
Effect.gen(function* () {
const calls: Array<Call> = []
const response = yield* Transcription.generate({
model: deepgram,
audio: Media.url("https://a.test/call.mp3", { mediaType: "audio/mpeg" }),
language: "en",
providerOptions: { keyterm: ["OpenCode", "Effect"] },
}).pipe(
Effect.provide(
layer((input) =>
observe(calls, input).pipe(
Effect.as(
json(input, {
metadata: { request_id: "dg_1", duration: 2 },
results: { channels: [{ alternatives: [{ transcript: "Hello there." }] }] },
}),
),
),
),
),
)
expect(calls).toHaveLength(1)
const url = new URL(calls[0].url)
expect(url.origin + url.pathname).toBe("https://deepgram.test/v1/listen")
expect([...url.searchParams]).toEqual([
["model", "nova-3"],
["smart_format", "true"],
["language", "en"],
["keyterm", "OpenCode"],
["keyterm", "Effect"],
])
expect(calls[0].headers.get("content-type")).toBe("application/json")
expect(JSON.parse(calls[0].body)).toEqual({ url: "https://a.test/call.mp3" })
expect(response).toMatchObject({
text: "Hello there.",
usage: { type: "seconds", seconds: 2 },
providerMetadata: { deepgram: { requestId: "dg_1" } },
})
}),
)
it.effect("transcribes an AssemblyAI URL source without uploading it first", () =>
Effect.gen(function* () {
const calls: Array<Call> = []
const response = yield* Transcription.generate({
model: assemblyai,
audio: Media.url("https://a.test/call.mp3", { mediaType: "audio/mpeg" }),
}).pipe(
Effect.provide(
layer((input) =>
Effect.gen(function* () {
const { call } = yield* observe(calls, input)
if (call.method === "POST") return json(input, { id: "tr_1", status: "queued" })
return json(input, { id: "tr_1", status: "completed", text: "Hello there.", audio_duration: 2 })
}),
),
),
)
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
"POST https://assemblyai.test/v2/transcript",
"GET https://assemblyai.test/v2/transcript/tr_1",
"GET https://assemblyai.test/v2/transcript/tr_1",
])
expect(JSON.parse(calls[0].body)).toEqual({
audio_url: "https://a.test/call.mp3",
speech_models: ["universal-3-5-pro"],
language_detection: true,
})
expect(response).toMatchObject({ text: "Hello there.", usage: { type: "seconds", seconds: 2 } })
}),
)
it.effect(
"uploads inline audio to AssemblyAI, resumes polling from a persisted token, and surfaces failed transcripts",
() =>
Effect.gen(function* () {
const calls: Array<{
readonly method: string
readonly url: string
readonly auth: string | null
readonly body: string
}> = []
const json = (value: unknown) => ({
body: JSON.stringify(value),
init: { headers: { "content-type": "application/json" } },
})
const polls = () => calls.filter((call) => call.url.endsWith("/tr_1")).length
const failed = JSON.stringify({ id: "tr_2", status: "error", error: "Audio file could not be decoded" })
const program = Effect.gen(function* () {
const started = yield* Transcription.start({ model: assemblyai, audio, diarize: true, speakers: 2 })
expect(started.status).toBe("queued")
const resumed = yield* Transcription.resume(assemblyai, JSON.parse(JSON.stringify(started.token)))
const response = yield* resumed.await({ poll: { interval: "1 second" } })
const failure = yield* Transcription.resume(assemblyai, { transcriptID: "tr_2" }).pipe(
Effect.flatMap((generation) => generation.await()),
Effect.flip,
)
return { response, failure }
}).pipe(
Effect.provide(
layer((input) =>
Effect.gen(function* () {
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
calls.push({
method: web.method,
url: web.url,
auth: web.headers.get("authorization"),
body: input.text,
})
if (web.url.endsWith("/tr_2"))
return input.respond(failed, { headers: { "content-type": "application/json" } })
const reply = web.url.endsWith("/v2/upload")
? json({ upload_url: "https://cdn.assemblyai.test/upload/1" })
: web.method === "POST"
? json({ id: "tr_1", status: "queued" })
: polls() < 3
? json({ id: "tr_1", status: "processing" })
: json({
id: "tr_1",
status: "completed",
text: "Hello there.",
words: [
{ text: "Hello", start: 250, end: 700, confidence: 0.9, speaker: "A" },
{ text: "there.", start: 700, end: 1250, confidence: 0.8, speaker: "B" },
],
utterances: [
{ text: "Hello", start: 250, end: 700, speaker: "A" },
{ text: "there.", start: 700, end: 1250, speaker: "B" },
],
language_code: "en_us",
audio_duration: 2,
})
return input.respond(reply.body, reply.init)
}),
),
),
)
const fiber = yield* Effect.forkChild(program)
yield* TestClock.adjust("5 seconds")
const { response, failure } = yield* Fiber.join(fiber)
expect(calls.slice(0, 2).map((call) => [call.method, call.url, call.auth])).toEqual([
["POST", "https://assemblyai.test/v2/upload", "aai-key"],
["POST", "https://assemblyai.test/v2/transcript", "aai-key"],
])
expect(JSON.parse(calls[1].body)).toEqual({
audio_url: "https://cdn.assemblyai.test/upload/1",
speech_models: ["universal-3-5-pro"],
language_detection: true,
speaker_labels: true,
speakers_expected: 2,
})
expect(calls[2].url).toBe("https://assemblyai.test/v2/transcript/tr_1")
expect(response.segments).toEqual([
{ text: "Hello", startSeconds: 0.25, endSeconds: 0.7, speaker: "A" },
{ text: "there.", startSeconds: 0.7, endSeconds: 1.25, speaker: "B" },
])
expect(response.words?.[1]).toEqual({
text: "there.",
startSeconds: 0.7,
endSeconds: 1.25,
speaker: "B",
confidence: 0.8,
})
expect(response).toMatchObject({
text: "Hello there.",
language: "en_us",
usage: { type: "seconds", seconds: 2 },
})
expect(failure.reason).toMatchObject({ _tag: "ProviderInternal", body: failed })
}),
)
it.effect("enables AssemblyAI speaker labels when only an expected speaker count is given", () =>
Effect.gen(function* () {
const calls: Array<Call> = []
yield* Transcription.start({ model: assemblyai, audio: Media.url("https://a.test/call.mp3"), speakers: 2 }).pipe(
Effect.provide(
layer((input) => observe(calls, input).pipe(Effect.as(json(input, { id: "tr_1", status: "queued" })))),
),
)
expect(calls.map((call) => JSON.parse(call.body))).toEqual([
{
audio_url: "https://a.test/call.mp3",
speech_models: ["universal-3-5-pro"],
language_detection: true,
speaker_labels: true,
speakers_expected: 2,
},
])
}),
)
it.effect("rejects ElevenLabs prompts, webhooks, per-channel transcripts, and untimed diarization", () =>
Effect.gen(function* () {
const errors = yield* Effect.all(
[
Transcription.generate({ model: elevenlabs, audio, prompt: "OpenCode" }),
Transcription.generate({ model: elevenlabs, audio, providerOptions: { webhook: true } }),
Transcription.generate({ model: elevenlabs, audio, http: { body: { use_multi_channel: true } } }),
Transcription.generate({
model: elevenlabs,
audio,
diarize: true,
providerOptions: { timestamps_granularity: "none" },
}),
Transcription.generate({
model: elevenlabs,
audio: Media.ref("file_1", { provider: "elevenlabs", mediaType: "audio/mpeg" }),
}),
].map((effect) => Effect.flip(effect)),
)
expect(errors.map((error) => [error.reason._tag, "operation" in error.reason && error.reason.operation])).toEqual(
[
["UnsupportedOperation", "media.prompt"],
["UnsupportedOperation", "transcription.webhook"],
["UnsupportedOperation", "transcription.multichannel"],
["UnsupportedOperation", "media.timestamps"],
["InvalidRequest", false],
],
)
}).pipe(Effect.provide(layer(() => Effect.die("an unsupported request reached the network")))),
)
it.effect("sends ElevenLabs URL audio as source_url and groups diarized words into speaker turns", () =>
Effect.gen(function* () {
const calls: Array<Call> = []
const token = (text: string, type: string, start: number, end: number, speaker_id?: string) => ({
text,
type,
start,
end,
speaker_id,
logprob: 0,
})
const response = yield* Transcription.generate({
model: elevenlabs,
audio: Media.url("https://a.test/call.mp3"),
language: "en",
speakers: 2,
providerOptions: { keyterms: ["OpenCode", "Scribe"], tag_audio_events: true, diarize: false },
}).pipe(
Effect.provide(
layer((input) =>
observe(calls, input).pipe(
Effect.as(
json(input, {
language_code: "ENG",
text: "Ready? (laughs) Yes. Go",
words: [
token("Ready?", "word", 0, 0.5, "speaker_0"),
token(" ", "spacing", 0.5, 0.6, "speaker_0"),
token("(laughs)", "audio_event", 0.6, 1, "speaker_0"),
token(" ", "spacing", 1, 1.1, "speaker_0"),
token("Yes.", "word", 1.2, 1.5, "speaker_1"),
token(" ", "spacing", 1.5, 1.6, "speaker_1"),
token("Go", "word", 1.6, 1.9, "speaker_0"),
],
transcription_id: "tr_1",
audio_duration_secs: 2,
}),
),
),
),
),
)
// `observe` re-encodes the FormData with a new boundary, so read the boundary from the sent body.
const boundary = /^--(\S+)/.exec(calls[0].body)?.[1]
const form = yield* Effect.promise(() =>
new Response(calls[0].body, {
headers: { "content-type": `multipart/form-data; boundary=${boundary}` },
}).formData(),
)
expect(calls[0].url).toBe("https://elevenlabs.test/v1/speech-to-text")
expect(calls[0].headers.get("xi-api-key")).toBe("test")
expect(Array.from(form.entries())).toEqual([
["model_id", "scribe_v2"],
["source_url", "https://a.test/call.mp3"],
["language_code", "en"],
["diarize", "true"],
["num_speakers", "2"],
["keyterms", "OpenCode"],
["keyterms", "Scribe"],
["tag_audio_events", "true"],
])
expect(response.segments).toEqual([
{ text: "Ready?", startSeconds: 0, endSeconds: 0.5, speaker: "speaker_0" },
{ text: "Yes.", startSeconds: 1.2, endSeconds: 1.5, speaker: "speaker_1" },
{ text: "Go", startSeconds: 1.6, endSeconds: 1.9, speaker: "speaker_0" },
])
expect(response.words?.map((word) => [word.text, word.speaker, word.confidence])).toEqual([
["Ready?", "speaker_0", 1],
["Yes.", "speaker_1", 1],
["Go", "speaker_0", 1],
])
expect(response.language).toBe("eng")
expect(response.usage).toEqual({ type: "seconds", seconds: 2 })
expect(response.providerMetadata).toEqual({ elevenlabs: { transcriptionId: "tr_1" } })
}),
)
it.effect("keeps ElevenLabs transcript edits separate from the original timed transcript", () =>
Effect.gen(function* () {
const calls: Array<Call> = []
const response = yield* Transcription.generate({
model: elevenlabs,
audio,
providerOptions: { transcript_edit: "Write dates in ISO 8601 format" },
}).pipe(
Effect.provide(
layer((input) =>
observe(calls, input).pipe(
Effect.as(
json(input, {
text: "the twelfth of July",
words: [{ text: "twelfth", type: "word", start: 0.5, end: 0.9 }],
transcription_id: "tr_2",
edited_transcript: { kind: "transcript", text: "2026-07-12" },
}),
),
),
),
),
)
expect((yield* formFields(calls[0])).transcript_edit).toEqual(["Write dates in ISO 8601 format"])
expect(response.text).toBe("the twelfth of July")
expect(response.words).toMatchObject([{ text: "twelfth", startSeconds: 0.5, endSeconds: 0.9 }])
expect(response.providerMetadata).toEqual({
elevenlabs: { transcriptionId: "tr_2", editedTranscript: { kind: "transcript", text: "2026-07-12" } },
})
expect(response.notices).toBeUndefined()
}),
)
it.effect("exposes an ElevenLabs edit failure without losing the successful transcription", () =>
Effect.gen(function* () {
const response = yield* Transcription.generate({
model: elevenlabs,
audio,
providerOptions: { transcript_edit: "Write dates in ISO 8601 format" },
}).pipe(
Effect.provide(
layer((input) =>
Effect.succeed(
json(input, {
text: "Original transcript",
edited_transcript: { kind: "error", error_type: "edit_failed", message: "Please try again" },
}),
),
),
),
)
expect(response.text).toBe("Original transcript")
expect(response.providerMetadata).toEqual({
elevenlabs: { editedTranscript: { kind: "error", errorType: "edit_failed", message: "Please try again" } },
})
expect(response.notices).toEqual([
{
type: "other",
message: "ElevenLabs transcript edit failed: Please try again",
providerMetadata: { elevenlabs: { errorType: "edit_failed" } },
},
])
}),
)
it.effect("rejects incompatible ElevenLabs edit options before sending audio", () =>
Effect.gen(function* () {
const errors = yield* Effect.all(
[
{ transcript_edit: "Edit", entity_detection: ["person"] },
{ transcript_edit: "Edit", entity_redaction: ["person"] },
{ transcript_edit: "Edit", use_multi_channel: true, multichannel_output_style: "combined" },
{ transcript_edit: "x".repeat(2001) },
].map((providerOptions) =>
Transcription.generate({ model: elevenlabs, audio, providerOptions }).pipe(Effect.flip),
),
)
const overlayError = yield* Transcription.generate({
model: elevenlabs,
audio,
providerOptions: { transcript_edit: "Edit" },
http: { body: { entity_redaction: ["person"] } },
}).pipe(Effect.flip)
expect([...errors, overlayError].map((error) => error.reason._tag)).toEqual(Array(5).fill("UnsupportedOperation"))
}).pipe(Effect.provide(layer(() => Effect.die("an unsupported request reached the network")))),
)
it.effect("rejects reading an AssemblyAI result before the transcript finishes", () =>
Effect.gen(function* () {
const generation = yield* Transcription.resume(assemblyai, { transcriptID: "tr_1" })
const error = yield* generation.result().pipe(Effect.flip)
expect(error.reason._tag).toBe("InvalidRequest")
expect(error.message).toBe("AssemblyAI generation tr_1 has not finished; await it before reading the result")
expect(error.reason.body).toBe(JSON.stringify({ id: "tr_1", status: "processing" }))
expect(error.reason.http?.status).toBe(200)
}).pipe(
Effect.provide(
layer((input) =>
Effect.succeed(
input.respond(JSON.stringify({ id: "tr_1", status: "processing" }), {
headers: { "content-type": "application/json" },
}),
),
),
),
),
)
it.effect("surfaces Gemini transcripts that ended without STOP instead of returning them as complete", () =>
Effect.gen(function* () {
const document = (finishReason?: string) =>
JSON.stringify({
candidates: [{ content: { parts: [{ audioTranscription: { text: "Hello" } }] }, finishReason }],
})
const withheld = JSON.stringify({ candidates: [{ finishReason: "SAFETY" }] })
const generate = (body: string) =>
Transcription.generate({ model: google, audio }).pipe(
Effect.provide(
layer((input) => Effect.succeed(input.respond(body, { headers: { "content-type": "application/json" } }))),
),
)
const truncated = yield* generate(document()).pipe(Effect.flip)
const partial = yield* generate(document("MAX_TOKENS"))
const policy = yield* generate(withheld).pipe(Effect.flip)
expect(truncated.reason).toMatchObject({ _tag: "InvalidProviderOutput", classification: "incomplete-stream" })
expect(partial.text).toBe("Hello")
expect(partial.notices).toEqual([
{
type: "other",
message: "Google Transcription finished with MAX_TOKENS",
providerMetadata: { google: { finishReason: "MAX_TOKENS" } },
},
])
expect(policy.reason).toMatchObject({ _tag: "ContentPolicy", body: withheld })
}),
)
})