@effect-uai/mistral 0.17.0 → 0.19.0-dev-20261109183959

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +4 -4
  2. package/dist/Mistral.d.mts +1 -1
  3. package/dist/Mistral.d.mts.map +1 -1
  4. package/dist/Mistral.mjs +4 -3
  5. package/dist/Mistral.mjs.map +1 -1
  6. package/dist/MistralRealtimeTranscriber.d.mts +2 -2
  7. package/dist/MistralRealtimeTranscriber.mjs +2 -2
  8. package/dist/MistralRealtimeTranscriber.mjs.map +1 -1
  9. package/dist/MistralSynthesizer.d.mts +1 -1
  10. package/dist/MistralSynthesizer.d.mts.map +1 -1
  11. package/dist/MistralSynthesizer.mjs +5 -4
  12. package/dist/MistralSynthesizer.mjs.map +1 -1
  13. package/dist/MistralTranscriber.d.mts +1 -1
  14. package/dist/MistralTranscriber.mjs +2 -2
  15. package/dist/MistralTranscriber.mjs.map +1 -1
  16. package/dist/audioCodec.d.mts.map +1 -1
  17. package/dist/audioCodec.mjs +3 -2
  18. package/dist/audioCodec.mjs.map +1 -1
  19. package/dist/codec.d.mts +3 -3
  20. package/dist/codec.d.mts.map +1 -1
  21. package/dist/codec.mjs +8 -6
  22. package/dist/codec.mjs.map +1 -1
  23. package/dist/codec.test.mjs +9 -3
  24. package/dist/codec.test.mjs.map +1 -1
  25. package/dist/dist-DHer1cql.mjs +1533 -0
  26. package/dist/dist-DHer1cql.mjs.map +1 -0
  27. package/dist/http.d.mts +1 -1
  28. package/dist/http.mjs.map +1 -1
  29. package/dist/{test.DNmyFkvJ-N3QqMRwZ.mjs → index.C4tKZ0yW-CUBm3Bmz.mjs} +9446 -5818
  30. package/dist/index.C4tKZ0yW-CUBm3Bmz.mjs.map +1 -0
  31. package/dist/onHalt.test.mjs +2 -2
  32. package/dist/onHalt.test.mjs.map +1 -1
  33. package/dist/{realtimeStt-BcthMc57.d.mts → realtimeStt-CcuQ6EZS.d.mts} +7 -1
  34. package/dist/realtimeStt-CcuQ6EZS.d.mts.map +1 -0
  35. package/dist/realtimeStt.d.mts +1 -1
  36. package/dist/realtimeStt.mjs +27 -30
  37. package/dist/realtimeStt.mjs.map +1 -1
  38. package/dist/realtimeStt.test.d.mts +1 -0
  39. package/dist/realtimeStt.test.mjs +317 -0
  40. package/dist/realtimeStt.test.mjs.map +1 -0
  41. package/dist/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-EAsCxrDo.mjs} +2 -1
  42. package/package.json +6 -6
  43. package/src/Mistral.ts +3 -1
  44. package/src/MistralRealtimeTranscriber.ts +1 -1
  45. package/src/MistralSynthesizer.ts +4 -13
  46. package/src/MistralTranscriber.ts +1 -1
  47. package/src/audioCodec.ts +3 -2
  48. package/src/codec.test.ts +9 -2
  49. package/src/codec.ts +6 -3
  50. package/src/http.ts +1 -1
  51. package/src/onHalt.test.ts +1 -1
  52. package/src/realtimeStt.test.ts +116 -0
  53. package/src/realtimeStt.ts +68 -64
  54. package/dist/magic-string.es-CUAnjKYC.mjs +0 -1014
  55. package/dist/magic-string.es-CUAnjKYC.mjs.map +0 -1
  56. package/dist/realtimeStt-BcthMc57.d.mts.map +0 -1
  57. package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtimeStt.test.mjs","names":["Effect","Exit","Duration","Layer","V.describe","it","V.it","internal.it"],"sources":["../../../../node_modules/.pnpm/@effect+vitest@4.1.2_effect@4.1.2_vitest@5.0.3_@types+node@26.2.0_vite@7.3.2_@types+nod_a2fffe0deec250ef9d6207b86a9a1ab8/node_modules/@effect/vitest/dist/internal/internal.js","../../../../node_modules/.pnpm/@effect+vitest@4.1.2_effect@4.1.2_vitest@5.0.3_@types+node@26.2.0_vite@7.3.2_@types+nod_a2fffe0deec250ef9d6207b86a9a1ab8/node_modules/@effect/vitest/dist/index.js","../src/realtimeStt.test.ts"],"sourcesContent":["/**\n * @since 4.0.0\n */\nimport * as Arbitrary from \"effect/Arbitrary\";\nimport * as Cause from \"effect/Cause\";\nimport * as Duration from \"effect/Duration\";\nimport * as Effect from \"effect/Effect\";\nimport * as Exit from \"effect/Exit\";\nimport * as Fiber from \"effect/Fiber\";\nimport { constVoid, flow, pipe } from \"effect/Function\";\nimport * as Layer from \"effect/Layer\";\nimport * as Schedule from \"effect/Schedule\";\nimport * as Scope from \"effect/Scope\";\nimport * as TestClock from \"effect/testing/TestClock\";\nimport * as TestConsole from \"effect/testing/TestConsole\";\nimport * as V from \"vitest\";\nconst getCurrentSuite = V.TestRunner.getCurrentSuite;\nconst runPromise = (effect, ctx) => Effect.runPromise(\n// Only the test is interruptible: once the signal aborts, the fiber stops\n// at its next interruptible step, which would skip reporting the defects\n// raised by finalizers.\nEffect.uninterruptibleMask(restore => Effect.gen(function* () {\n const exit = yield* Effect.exit(restore(effect));\n if (Exit.isFailure(exit)) {\n const errors = Cause.prettyErrors(exit.cause);\n for (let i = 0; i < errors.length; i++) {\n yield* Effect.logError(errors[i]);\n }\n }\n return yield* exit;\n})), {\n signal: ctx?.signal\n});\nconst settleOnAbort = (ctx, promise) => {\n // Vitest stops awaiting the promise once the signal aborts (timeout or\n // cancellation), so only then add a hook that waits for the finalizers.\n // Registering it unconditionally would change the teardown of every test.\n const onAbort = () => ctx.onTestFinished(() => promise.then(constVoid, constVoid));\n ctx.signal.addEventListener(\"abort\", onAbort, {\n once: true\n });\n const cleanup = () => ctx.signal.removeEventListener(\"abort\", onAbort);\n promise.then(cleanup, cleanup);\n // A retry after a timed-out attempt reuses the aborted signal, so no event fires.\n if (ctx.signal.aborted) onAbort();\n return promise;\n};\n/** @internal */\nconst runTest = ctx => effect => {\n const promise = runPromise(effect, ctx);\n return ctx ? settleOnAbort(ctx, promise) : promise;\n};\nconst TestEnv = /*#__PURE__*/Layer.mergeAll(TestConsole.layer, /*#__PURE__*/TestClock.layer());\n/** @internal */\nexport const addEqualityTesters = () => {\n V.expect.addEqualityTesters([]);\n};\n/** @internal */\nconst testOptions = timeout => typeof timeout === \"number\" ? {\n timeout\n} : timeout ?? {};\nconst hookTimeout = timeout => timeout === undefined ? undefined : Duration.toMillis(Duration.fromInputUnsafe(timeout));\nconst propertyTestOptions = timeout => typeof timeout === \"number\" ? undefined : timeout;\nconst checkOptions = timeout => propertyTestOptions(timeout)?.arbitrary;\nconst compileArbitraryInput = input => Arbitrary.isArbitrary(input) ? input : Arbitrary.schema(input);\nconst makeArbitrary = arbitraries => Arbitrary.all(Array.isArray(arbitraries) ? arbitraries.map(compileArbitraryInput) : Object.fromEntries(Object.entries(arbitraries).map(([key, input]) => [key, compileArbitraryInput(input)])));\nconst normalizeProperty = (property, value) => Effect.catchCause(Effect.suspend(() => {\n const output = property(value);\n return Effect.isEffect(output) ? output : Effect.succeed(output);\n}), cause => Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.fail(cause));\nconst runCheck = (ctx, arbitrary, property, options) => runTest(ctx)(Effect.flatMapEager(Arbitrary.checkEffect(arbitrary, value => normalizeProperty(property, value), options), result => {\n const failure = Arbitrary.formatCheckFailure(result);\n return failure === undefined ? Effect.void : Effect.die(new Error(failure));\n}));\n// Vitest decides which fixtures to set up by parsing the source of a test\n// function's context parameter, and there is no public API to supply the names\n// for a wrapper, so wrappers expose the source of the function they call.\nconst withFixturesOf = (source, f) => Object.defineProperty(f, \"toString\", {\n value: () => source.toString()\n});\nconst runHook = (fn, run) => withFixturesOf(fn, (...args) => {\n const result = fn(...args);\n if (!Effect.isEffect(result)) {\n return result;\n }\n const scope = Scope.makeUnsafe();\n // Everything the hook leaves behind lives in the scope: the resources it\n // acquired and, ahead of them, a cleanup function it returned. On failure\n // the scope closes inside the fiber, so interrupting it awaits the release.\n const effect = Scope.provide(result, scope).pipe(Effect.flatMap(cleanup => typeof cleanup === \"function\" ? Scope.addFinalizer(scope, Effect.promise(async () => await cleanup())) : Effect.void), Effect.onError(cause => Scope.close(scope, Exit.failCause(cause))));\n return run(args, effect, scope);\n});\n// Closing must outlive an already-aborted signal, so it runs without one.\nconst closeHookScope = scope => runPromise(Scope.close(scope, Exit.void));\n/** @internal */\nexport const beforeAll = (fn, timeout) => {\n let dispose = () => Promise.resolve();\n // Vitest drops the cleanup of a setup that outlives its timeout, so the\n // scope is disposed by the cleanup of a hook registered just before this\n // one. It runs in the same cleanup phase, in the slot next to this hook's,\n // with this hook's timeout.\n V.beforeAll(() => () => dispose(), timeout);\n V.beforeAll(runHook(fn, (_, effect, scope) => {\n const fiber = Effect.runFork(effect);\n // Interrupting a setup that Vitest stopped waiting for awaits its release.\n dispose = () => runPromise(Effect.andThen(Fiber.interrupt(fiber), Scope.close(scope, Exit.void)));\n return runPromise(Fiber.join(fiber));\n }), timeout);\n};\n/** @internal */\nexport const beforeEach = (fn, timeout) => V.beforeEach(runHook(fn, ([ctx], effect, scope) =>\n// The scope closes when Vitest runs the hook's cleanup after the test,\n// and the next test waits for it even if that cleanup times out.\nrunTest(ctx)(effect).then(() => () => settleOnAbort(ctx, closeHookScope(scope)))), timeout);\nconst makeItProxy = (it, overrides) => new Proxy(it, {\n apply(target, thisArg, argArray) {\n return Reflect.apply(target, thisArg, argArray);\n },\n get(target, property, receiver) {\n if (Object.hasOwn(overrides, property)) {\n return Reflect.get(overrides, property);\n }\n // do not bind: binding would strip vitest's static helpers (e.g. `describe.each`)\n return Reflect.get(target, property, receiver);\n }\n});\nconst collectTasks = (tasks, acc = []) => {\n for (let i = 0; i < tasks.length; i++) {\n const task = tasks[i];\n if (task.type === \"test\" && task.mode !== \"skip\" && task.mode !== \"todo\") {\n acc.push(task);\n } else if (task.tasks !== undefined) {\n collectTasks(task.tasks, acc);\n }\n }\n return acc;\n};\nconst runsMoreThanOnce = task => {\n const retry = typeof task.retry === \"number\" ? task.retry : task.retry?.count;\n return (retry ?? 0) > 0 || (task.repeats ?? 0) > 0;\n};\nconst registerProp = (it, name, arbitraries, property, timeout) => {\n const arbitrary = makeArbitrary(arbitraries);\n // Property callbacks receive only the base context; the empty source requests no fixtures.\n it(name, testOptions(timeout), withFixturesOf(() => {}, ctx => runCheck(ctx, arbitrary, values => property(values, ctx), checkOptions(timeout))));\n};\nconst makeTester = (mapEffect, it) => {\n const run = (ctx, args, self) => pipe(Effect.suspend(() => self(...args)), mapEffect, runTest(ctx));\n const register = test => (name, self, timeout) => test(name, testOptions(timeout), withFixturesOf(self, ctx => run(ctx, [ctx], self)));\n const each = cases => (name, self, timeout) => it.for(cases)(name, testOptions(timeout), withFixturesOf(self, (args, ctx) => run(ctx, [args, ctx], self).then(constVoid)));\n const prop = (name, arbitraries, self, timeout) => registerProp(it, name, arbitraries, (values, ctx) => Effect.mapEager(mapEffect(Effect.suspend(() => self(values, ctx))), value => value !== false), timeout);\n return Object.assign(register(it), {\n skip: register(it.skip),\n skipIf: condition => register(it.skipIf(condition)),\n runIf: condition => register(it.runIf(condition)),\n only: register(it.only),\n each,\n fails: register(it.fails),\n prop\n });\n};\nconst makeProp = it => (name, arbitraries, self, timeout) => registerProp(it, name, arbitraries, (values, ctx) => {\n const result = self(values, ctx);\n if (result !== undefined && typeof result !== \"boolean\") {\n throw new TypeError(\"it.prop callbacks must return boolean or void. Use it.effect.prop for Effect callbacks.\");\n }\n return result !== false;\n}, timeout);\nconst makeLayer = it => (layer_, options) => (...args) => {\n const excludeTestServices = options?.excludeTestServices ?? false;\n const withTestEnv = excludeTestServices ? layer_ : Layer.provideMerge(layer_, TestEnv);\n const memoMap = options?.memoMap ?? Effect.runSync(Layer.makeMemoMap);\n const buildPerTest = options?.buildPerTest ?? false;\n const suiteOptions = options?.concurrent === undefined ? {} : {\n concurrent: options.concurrent\n };\n const makeBlockMethods = provide => makeItProxy(it, {\n effect: makeTester(effect => provide(Effect.scoped(effect)), it),\n prop: makeProp(it),\n flakyTest,\n layer(nestedLayer, nestedOptions) {\n return makeLayer(it)(Layer.provideMerge(nestedLayer, withTestEnv), {\n ...nestedOptions,\n memoMap: Layer.forkMemoMapUnsafe(memoMap),\n excludeTestServices,\n // The services of a per-test block only exist during a test, so\n // blocks nested in it are built per test as well.\n buildPerTest: buildPerTest || nestedOptions?.buildPerTest === true\n });\n }\n });\n if (buildPerTest) {\n // Each test builds into its own fork of the memo map, so it reuses the\n // services of enclosing blocks and releases the rest when it finishes.\n const methods = makeBlockMethods(effect => Effect.scopedWith(scope => Layer.buildWithMemoMap(withTestEnv, Layer.forkMemoMapUnsafe(memoMap), scope).pipe(Effect.orDie, Effect.flatMap(context => Effect.provide(effect, context)))));\n if (args.length === 1) {\n return args[0](methods);\n }\n return V.describe(args[0], suiteOptions, () => args[1](methods));\n }\n const scope = Effect.runSync(Scope.make());\n const contextEffect = Layer.buildWithMemoMap(withTestEnv, memoMap, scope).pipe(Effect.orDie, Effect.cached, Effect.runSync);\n let closed = false;\n const closeScope = () => {\n if (closed) {\n return Promise.resolve();\n }\n closed = true;\n // Layer cleanup must outlive the last test's already-aborted signal.\n return runPromise(Scope.close(scope, Exit.void));\n };\n const methods = makeBlockMethods(effect => Effect.flatMap(contextEffect, context => Effect.provide(effect, context)));\n if (args.length === 1) {\n const currentSuite = getCurrentSuite();\n const previousTasks = new Set(currentSuite.tasks);\n args[0](methods);\n const blockTasks = collectTasks(currentSuite.tasks.filter(task => !previousTasks.has(task)));\n if (blockTasks.length === 0) {\n V.afterAll(() => closeScope(), hookTimeout(options?.timeout));\n return;\n }\n const blockTaskSet = new Set(blockTasks);\n // Vitest runs hooks once per retry and repeat attempt, so a test's last\n // attempt cannot be counted. Blocks with such tests are released by `afterAll`.\n const releaseAfterLastTest = !blockTasks.some(runsMoreThanOnce);\n let remaining = blockTasks.length;\n V.beforeEach(\n // Destructured so Vitest can parse the hook once the suite defines fixtures.\n ({\n onTestFinished,\n signal,\n task\n }) => {\n if (!blockTaskSet.has(task)) {\n return;\n }\n if (releaseAfterLastTest) {\n onTestFinished(() => {\n remaining--;\n if (remaining === 0) {\n return closeScope();\n }\n });\n }\n return runPromise(Effect.asVoid(contextEffect), {\n signal\n });\n }, hookTimeout(options?.timeout));\n V.afterAll(() => closeScope(), hookTimeout(options?.timeout));\n return;\n }\n return V.describe(args[0], suiteOptions, () => {\n V.beforeAll(() => runPromise(Effect.asVoid(contextEffect)), hookTimeout(options?.timeout));\n V.afterAll(() => closeScope(), hookTimeout(options?.timeout));\n return args[1](methods);\n });\n};\n/** @internal */\nexport const flakyTest = (self, timeout = Duration.seconds(30)) => pipe(self, Effect.scoped, Effect.sandbox, Effect.retry(pipe(Schedule.recurs(10), Schedule.while(_ => Effect.succeed(Duration.isLessThanOrEqualTo(Duration.fromInputUnsafe(_.elapsed), Duration.fromInputUnsafe(timeout)))))), Effect.orDie);\n/** @internal */\nexport const makeMethods = it => makeItProxy(it, {\n effect: makeTester(flow(Effect.scoped, Effect.provide(TestEnv)), it),\n live: makeTester(Effect.scoped, it),\n flakyTest,\n layer: makeLayer(it),\n prop: makeProp(it)\n});\n/** @internal */\nexport const it = /*#__PURE__*/makeMethods(V.it);\n/** @internal */\nexport const {\n /** @internal */\n effect,\n /** @internal */\n layer,\n /** @internal */\n live,\n /** @internal */\n prop\n} = it;\n/** @internal */\nexport const describeWrapped = (name, f) => V.describe(name, () => f(it));\n//# sourceMappingURL=internal.js.map","import * as internal from \"./internal/internal.js\";\n/**\n * Re-exports the `vitest` API unchanged.\n *\n * @stability unstable\n * @since 4.0.0\n */\nexport * from \"vitest\";\n/**\n * Invokes the Effect Vitest equality-tester registration hook.\n *\n * @stability unstable\n * @category utils\n * @since 4.0.0\n */\nexport const addEqualityTesters = internal.addEqualityTesters;\n/**\n * Registers a callback to run once before all tests in the current suite. If\n * the callback returns an `Effect`, it is run and awaited with a `Scope` that\n * closes in a cleanup after the suite's `afterAll` hooks, within the hook's\n * timeout. The `Effect` must not require any other services.\n *\n * **Details**\n *\n * If Vitest stops waiting for the setup, the setup is interrupted before the\n * `Scope` closes. As with any `beforeAll` cleanup, Vitest reports a closing\n * that exceeds the hook's timeout and does not wait for it.\n *\n * @stability unstable\n * @category hooks\n * @since 4.0.0\n */\nexport const beforeAll = internal.beforeAll;\n/**\n * Registers a callback to run before each test in the current suite. If the\n * callback returns an `Effect`, it is run and awaited with a `Scope` that\n * closes when the test finishes, and interrupted if the hook times out. The\n * `Effect` must not require any other services.\n *\n * @stability unstable\n * @category hooks\n * @since 4.0.0\n */\nexport const beforeEach = internal.beforeEach;\n/**\n * Runs a scoped Effect test with test clock and console services.\n *\n * @stability unstable\n * @category testing\n * @since 4.0.0\n */\nexport const effect = internal.effect;\n/**\n * Runs a scoped Effect test with live services.\n *\n * @stability unstable\n * @category testing\n * @since 4.0.0\n */\nexport const live = internal.live;\n/**\n * Share a `Layer` between multiple tests, optionally wrapping\n * the tests in a `describe` block if a name is provided.\n *\n * **Details**\n *\n * Named layers accept `concurrent` to override inherited suite concurrency.\n * Anonymous layers always inherit the enclosing suite's concurrency.\n * Use `ctx.expect` in concurrent tests for test-local snapshots and assertion counts.\n *\n * Pass `buildPerTest: true` to build the layer once per test instead of once\n * per block. Each test gets new services, released when the test finishes,\n * while services from an enclosing `layer` block stay shared. A build failure\n * fails only that test. Blocks nested inside a per-test block are built per\n * test as well. `timeout` has no effect on a per-test block, since the test's\n * own timeout covers the build.\n *\n * **Example** (Sharing layers between tests)\n *\n * ```ts\n * import { assert, layer } from \"@effect/vitest\"\n * import { Effect, Layer, Context } from \"effect\"\n *\n * class Foo extends Context.Service<Foo, \"foo\">()(\"Foo\") {\n * static layer = Layer.succeed(Foo, \"foo\")\n * }\n *\n * class Bar extends Context.Service<Bar, \"bar\">()(\"Bar\") {\n * static layer = Layer.effect(\n * Bar,\n * Effect.map(Foo, () => \"bar\" as const)\n * )\n * }\n *\n * layer(Foo.layer)(\"layer\", (it) => {\n * it.effect(\"adds context\", () =>\n * Effect.gen(function*() {\n * const foo = yield* Foo\n * assert.strictEqual(foo, \"foo\")\n * }))\n *\n * it.layer(Bar.layer)(\"nested\", (it) => {\n * it.effect(\"adds context\", () =>\n * Effect.gen(function*() {\n * const foo = yield* Foo\n * const bar = yield* Bar\n * assert.strictEqual(foo, \"foo\")\n * assert.strictEqual(bar, \"bar\")\n * }))\n * })\n * })\n * ```\n *\n * **Example** (Building a layer for each test)\n *\n * ```ts\n * import { assert, layer } from \"@effect/vitest\"\n * import { Effect, Layer, Context, Ref } from \"effect\"\n *\n * class Database extends Context.Service<Database, \"database\">()(\"Database\") {\n * static layer = Layer.succeed(Database, \"database\")\n * }\n *\n * class Cache extends Context.Service<Cache, Ref.Ref<Array<string>>>()(\"Cache\") {\n * static layer = Layer.effect(\n * Cache,\n * Effect.andThen(Database, Ref.make<Array<string>>([]))\n * )\n * }\n *\n * layer(Database.layer)(\"repository\", (it) => {\n * // Database is shared, Cache is built for each test\n * it.layer(Cache.layer, { buildPerTest: true })((it) => {\n * it.effect(\"writes to the cache\", () =>\n * Effect.gen(function*() {\n * const cache = yield* Cache\n * yield* Ref.update(cache, (keys) => [...keys, \"a\"])\n * assert.deepStrictEqual(yield* Ref.get(cache), [\"a\"])\n * }))\n *\n * it.effect(\"starts with an empty cache\", () =>\n * Effect.gen(function*() {\n * const cache = yield* Cache\n * assert.deepStrictEqual(yield* Ref.get(cache), [])\n * }))\n * })\n * })\n * ```\n *\n * @stability unstable\n * @category testing\n * @since 4.0.0\n */\nexport const layer = internal.layer;\n/**\n * Retries a scoped Effect to tolerate intermittent test failures, dying on exhausted retries.\n *\n * @stability unstable\n * @category testing\n * @since 4.0.0\n */\nexport const flakyTest = internal.flakyTest;\n/**\n * Runs a synchronous property test using schemas or arbitraries.\n *\n * @stability unstable\n * @category testing\n * @since 4.0.0\n */\nexport const prop = internal.prop;\n/**\n * Vitest test API extended with Effect test helpers.\n *\n * @stability unstable\n * @category testing\n * @since 4.0.0\n */\nexport const it = internal.it;\n/**\n * Creates the Effect test helpers for a Vitest test API, such as one extended with fixtures.\n *\n * **Details**\n *\n * Tests receive the fixtures they destructure from their context. Vitest sets them up before the test and tears\n * them down after the test's scope closes, as it does for its own tests. `it.effect.each` passes the context\n * after the test case, and named and anonymous `it.layer` blocks keep the fixtures.\n *\n * **Gotchas**\n *\n * Vitest reads the destructured names to decide which fixtures to set up. Once any fixture is defined, a test that\n * takes the whole context as a plain parameter, such as `(ctx) =>`, fails with a `FixtureParseError`. Property\n * tests receive only the base test context and cannot request fixtures; auto fixtures still run.\n *\n * **Example** (Using a Vitest fixture in an Effect test)\n *\n * ```ts\n * import { assert, makeMethods, test } from \"@effect/vitest\"\n * import { Effect } from \"effect\"\n *\n * const it = makeMethods(\n * test.extend(\"config\", { scope: \"file\" }, () => ({ port: 3000 }))\n * )\n *\n * it.effect(\"reads the config fixture\", ({ config }) =>\n * Effect.sync(() => {\n * assert.strictEqual(config.port, 3000)\n * }))\n * ```\n *\n * @stability unstable\n * @category constructors\n * @since 4.0.0\n */\nexport const makeMethods = internal.makeMethods;\n/**\n * Creates a named Vitest suite whose callback receives Effect test helpers.\n *\n * @stability unstable\n * @category testing\n * @since 4.0.0\n */\nexport const describeWrapped = internal.describeWrapped;\n//# sourceMappingURL=index.js.map","import { describe, it } from \"@effect/vitest\"\nimport { Effect, Exit, Fiber, Redacted, Stream } from \"effect\"\nimport { TestClock } from \"effect/testing\"\nimport { expect } from \"vitest\"\nimport * as FakeWebSocket from \"@effect-uai/core/testing/FakeWebSocket\"\nimport { streamTranscription } from \"./realtimeStt.js\"\n\nconst pcm = { container: \"raw\", encoding: \"pcm_s16le\", sampleRate: 16000, channels: 1 } as const\n\nconst request = { model: \"voxtral-mini-transcribe-realtime-2602\", inputFormat: pcm }\n\nconst sessionCreated = {\n type: \"session.created\",\n session: { request_id: \"req_1\", model: request.model },\n}\n\nconst delta = (text: string) => ({ type: \"transcription.text.delta\", text })\n\nconst start = <E>(\n server: FakeWebSocket.FakeWebSocketServer,\n audioIn: Stream.Stream<Uint8Array, E>,\n) =>\n streamTranscription({ apiKey: Redacted.make(\"test-key\"), webSocket: server.connect })(\n audioIn,\n request,\n ).pipe(Stream.runCollect, Effect.exit, Effect.forkChild)\n\n/** Run a transcription against `server`, closing it, if asked, once the frames are in. */\nconst transcribe = <E>(\n server: FakeWebSocket.FakeWebSocketServer,\n audioIn: Stream.Stream<Uint8Array, E>,\n close?: { readonly code: number; readonly afterSent?: number },\n) =>\n Effect.gen(function* () {\n const collector = yield* start(server, audioIn)\n if (close !== undefined) {\n yield* server.greeted\n yield* server.awaitSent(close.afterSent ?? 0)\n yield* server.close(close.code)\n }\n return yield* Fiber.join(collector)\n })\n\ndescribe(\"Mistral Realtime STT socket\", () => {\n it.live(\"configures the session before any audio, and ends on a clean close\", () =>\n Effect.gen(function* () {\n const server = yield* FakeWebSocket.make({\n greeting: [\n sessionCreated,\n delta(\"hel\"),\n delta(\"lo\"),\n { type: \"transcription.done\", text: \"hello\", model: request.model, segments: [] },\n { type: \"error\", error: { message: \"slow down\", code: 3001 } },\n ],\n })\n const exit = yield* transcribe(server, Stream.make(new Uint8Array([1, 2])), {\n code: 1000,\n afterSent: 3,\n })\n\n expect(exit).toEqual(\n Exit.succeed([\n { _tag: \"partial\", text: \"hel\" },\n { _tag: \"partial\", text: \"hello\" },\n { _tag: \"final\", text: \"hello\" },\n { _tag: \"error\", code: \"3001\", message: \"slow down\" },\n ]),\n )\n const sent = (yield* server.sent) as ReadonlyArray<{ type: string }>\n expect(sent.map((f) => f.type)).toEqual([\n \"session.update\",\n \"input_audio.append\",\n \"input_audio.end\",\n ])\n }).pipe(Effect.scoped),\n )\n\n it.live(\"fails rather than ending when the connection drops\", () =>\n Effect.gen(function* () {\n const server = yield* FakeWebSocket.make({ greeting: [sessionCreated] })\n const exit = yield* transcribe(server, Stream.never, { code: 1011 })\n\n expect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain(\"Unavailable\")\n }).pipe(Effect.scoped),\n )\n\n it.live(\"fails with the audio input's own error\", () =>\n Effect.gen(function* () {\n const server = yield* FakeWebSocket.make({ greeting: [sessionCreated] })\n const exit = yield* transcribe(server, Stream.fail(\"microphone unplugged\"))\n\n expect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain(\"microphone unplugged\")\n }).pipe(Effect.scoped),\n )\n\n it.effect(\"commits a synthetic final once the deltas go quiet\", () =>\n Effect.gen(function* () {\n const server = yield* FakeWebSocket.make({\n greeting: [sessionCreated, delta(\"hel\"), delta(\"lo \")],\n })\n const collector = yield* start(server, Stream.never)\n yield* server.greeted\n yield* TestClock.withLive(Effect.sleep(\"5 millis\"))\n yield* TestClock.adjust(\"1 second\")\n yield* server.close(1000)\n\n expect(yield* Fiber.join(collector)).toEqual(\n Exit.succeed([\n { _tag: \"partial\", text: \"hel\" },\n { _tag: \"partial\", text: \"hello \" },\n { _tag: \"final\", text: \"hello\" },\n ]),\n )\n }).pipe(Effect.scoped),\n )\n})\n"],"x_google_ignoreList":[0,1],"mappings":";;;;;;;;;;;;;;;;;;;;;AAgBA,MAAM,kBAAA,WAA+B;AACrC,MAAM,cAAc,QAAQ,QAAQA,SAAO,WAI3CA,SAAO,qBAAoB,YAAWA,SAAO,IAAI,aAAa;CAC5D,MAAM,OAAO,OAAOA,SAAO,KAAK,QAAQ,MAAM,CAAC;CAC/C,IAAIC,OAAK,UAAU,IAAI,GAAG;EACxB,MAAM,SAAS,MAAM,aAAa,KAAK,KAAK;EAC5C,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KACjC,OAAOD,SAAO,SAAS,OAAO,EAAE;CAEpC;CACA,OAAO,OAAO;AAChB,CAAC,CAAC,GAAG,EACH,QAAQ,KAAK,OACf,CAAC;AACD,MAAM,iBAAiB,KAAK,YAAY;CAItC,MAAM,gBAAgB,IAAI,qBAAqB,QAAQ,KAAK,WAAW,SAAS,CAAC;CACjF,IAAI,OAAO,iBAAiB,SAAS,SAAS,EAC5C,MAAM,KACR,CAAC;CACD,MAAM,gBAAgB,IAAI,OAAO,oBAAoB,SAAS,OAAO;CACrE,QAAQ,KAAK,SAAS,OAAO;CAE7B,IAAI,IAAI,OAAO,SAAS,QAAQ;CAChC,OAAO;AACT;;AAEA,MAAM,WAAU,SAAO,WAAU;CAC/B,MAAM,UAAU,WAAW,QAAQ,GAAG;CACtC,OAAO,MAAM,cAAc,KAAK,OAAO,IAAI;AAC7C;AACA,MAAM,UAAuB,sBAAM,SAAS,YAAY,OAAoB,0BAAU,MAAM,CAAC;;AAM7F,MAAM,eAAc,YAAW,OAAO,YAAY,WAAW,EAC3D,QACF,IAAI,WAAW,CAAC;AAChB,MAAM,eAAc,YAAW,YAAY,KAAA,IAAY,KAAA,IAAYE,WAAS,SAASA,WAAS,gBAAgB,OAAO,CAAC;AACtH,MAAM,uBAAsB,YAAW,OAAO,YAAY,WAAW,KAAA,IAAY;AACjF,MAAM,gBAAe,YAAW,oBAAoB,OAAO,CAAC,EAAE;AAC9D,MAAM,yBAAwB,UAAS,UAAU,YAAY,KAAK,IAAI,QAAQ,UAAU,OAAO,KAAK;AACpG,MAAM,iBAAgB,gBAAe,UAAU,IAAI,MAAM,QAAQ,WAAW,IAAI,YAAY,IAAI,qBAAqB,IAAI,OAAO,YAAY,OAAO,QAAQ,WAAW,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,KAAK,sBAAsB,KAAK,CAAC,CAAC,CAAC,CAAC;AACnO,MAAM,qBAAqB,UAAU,UAAUF,SAAO,WAAWA,SAAO,cAAc;CACpF,MAAM,SAAS,SAAS,KAAK;CAC7B,OAAOA,SAAO,SAAS,MAAM,IAAI,SAASA,SAAO,QAAQ,MAAM;AACjE,CAAC,IAAG,UAAS,MAAM,cAAc,KAAK,IAAIA,SAAO,UAAU,KAAK,IAAIA,SAAO,KAAK,KAAK,CAAC;AACtF,MAAM,YAAY,KAAK,WAAW,UAAU,YAAY,QAAQ,GAAG,CAAC,CAACA,SAAO,aAAa,UAAU,YAAY,YAAW,UAAS,kBAAkB,UAAU,KAAK,GAAG,OAAO,IAAG,WAAU;CACzL,MAAM,UAAU,UAAU,mBAAmB,MAAM;CACnD,OAAO,YAAY,KAAA,IAAYA,SAAO,OAAOA,SAAO,IAAI,IAAI,MAAM,OAAO,CAAC;AAC5E,CAAC,CAAC;AAIF,MAAM,kBAAkB,QAAQ,MAAM,OAAO,eAAe,GAAG,YAAY,EACzE,aAAa,OAAO,SAAS,EAC/B,CAAC;AAmCD,MAAM,eAAe,IAAI,cAAc,IAAI,MAAM,IAAI;CACnD,MAAM,QAAQ,SAAS,UAAU;EAC/B,OAAO,QAAQ,MAAM,QAAQ,SAAS,QAAQ;CAChD;CACA,IAAI,QAAQ,UAAU,UAAU;EAC9B,IAAI,OAAO,OAAO,WAAW,QAAQ,GACnC,OAAO,QAAQ,IAAI,WAAW,QAAQ;EAGxC,OAAO,QAAQ,IAAI,QAAQ,UAAU,QAAQ;CAC/C;AACF,CAAC;AACD,MAAM,gBAAgB,OAAO,MAAM,CAAC,MAAM;CACxC,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,OAAO,MAAM;EACnB,IAAI,KAAK,SAAS,UAAU,KAAK,SAAS,UAAU,KAAK,SAAS,QAChE,IAAI,KAAK,IAAI;OACR,IAAI,KAAK,UAAU,KAAA,GACxB,aAAa,KAAK,OAAO,GAAG;CAEhC;CACA,OAAO;AACT;AACA,MAAM,oBAAmB,SAAQ;CAE/B,SADc,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAK,OAAO,UACvD,KAAK,MAAM,KAAK,WAAW,KAAK;AACnD;AACA,MAAM,gBAAgB,IAAI,MAAM,aAAa,UAAU,YAAY;CACjE,MAAM,YAAY,cAAc,WAAW;CAE3C,GAAG,MAAM,YAAY,OAAO,GAAG,qBAAqB,CAAC,IAAG,QAAO,SAAS,KAAK,YAAW,WAAU,SAAS,QAAQ,GAAG,GAAG,aAAa,OAAO,CAAC,CAAC,CAAC;AAClJ;AACA,MAAM,cAAc,WAAW,OAAO;CACpC,MAAM,OAAO,KAAK,MAAM,SAAS,KAAKA,SAAO,cAAc,KAAK,GAAG,IAAI,CAAC,GAAG,WAAW,QAAQ,GAAG,CAAC;CAClG,MAAM,YAAW,UAAS,MAAM,MAAM,YAAY,KAAK,MAAM,YAAY,OAAO,GAAG,eAAe,OAAM,QAAO,IAAI,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;CACrI,MAAM,QAAO,WAAU,MAAM,MAAM,YAAY,GAAG,IAAI,KAAK,CAAC,CAAC,MAAM,YAAY,OAAO,GAAG,eAAe,OAAO,MAAM,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;CACzK,MAAM,QAAQ,MAAM,aAAa,MAAM,YAAY,aAAa,IAAI,MAAM,cAAc,QAAQ,QAAQA,SAAO,SAAS,UAAUA,SAAO,cAAc,KAAK,QAAQ,GAAG,CAAC,CAAC,IAAG,UAAS,UAAU,KAAK,GAAG,OAAO;CAC9M,OAAO,OAAO,OAAO,SAAS,EAAE,GAAG;EACjC,MAAM,SAAS,GAAG,IAAI;EACtB,SAAQ,cAAa,SAAS,GAAG,OAAO,SAAS,CAAC;EAClD,QAAO,cAAa,SAAS,GAAG,MAAM,SAAS,CAAC;EAChD,MAAM,SAAS,GAAG,IAAI;EACtB;EACA,OAAO,SAAS,GAAG,KAAK;EACxB;CACF,CAAC;AACH;AACA,MAAM,YAAW,QAAO,MAAM,aAAa,MAAM,YAAY,aAAa,IAAI,MAAM,cAAc,QAAQ,QAAQ;CAChH,MAAM,SAAS,KAAK,QAAQ,GAAG;CAC/B,IAAI,WAAW,KAAA,KAAa,OAAO,WAAW,WAC5C,MAAM,IAAI,UAAU,yFAAyF;CAE/G,OAAO,WAAW;AACpB,GAAG,OAAO;AACV,MAAM,aAAY,QAAO,QAAQ,aAAa,GAAG,SAAS;CACxD,MAAM,sBAAsB,SAAS,uBAAuB;CAC5D,MAAM,cAAc,sBAAsB,SAASG,QAAM,aAAa,QAAQ,OAAO;CACrF,MAAM,UAAU,SAAS,WAAWH,SAAO,QAAQG,QAAM,WAAW;CACpE,MAAM,eAAe,SAAS,gBAAgB;CAC9C,MAAM,eAAe,SAAS,eAAe,KAAA,IAAY,CAAC,IAAI,EAC5D,YAAY,QAAQ,WACtB;CACA,MAAM,oBAAmB,YAAW,YAAY,IAAI;EAClD,QAAQ,YAAW,WAAU,QAAQH,SAAO,OAAO,MAAM,CAAC,GAAG,EAAE;EAC/D,MAAM,SAAS,EAAE;EACjB;EACA,MAAM,aAAa,eAAe;GAChC,OAAO,UAAU,EAAE,CAAC,CAACG,QAAM,aAAa,aAAa,WAAW,GAAG;IACjE,GAAG;IACH,SAASA,QAAM,kBAAkB,OAAO;IACxC;IAGA,cAAc,gBAAgB,eAAe,iBAAiB;GAChE,CAAC;EACH;CACF,CAAC;CACD,IAAI,cAAc;EAGhB,MAAM,UAAU,kBAAiB,WAAUH,SAAO,YAAW,UAASG,QAAM,iBAAiB,aAAaA,QAAM,kBAAkB,OAAO,GAAG,KAAK,CAAC,CAAC,KAAKH,SAAO,OAAOA,SAAO,SAAQ,YAAWA,SAAO,QAAQ,QAAQ,OAAO,CAAC,CAAC,CAAC,CAAC;EAClO,IAAI,KAAK,WAAW,GAClB,OAAO,KAAK,EAAE,CAAC,OAAO;EAExB,OAAOI,SAAW,KAAK,IAAI,oBAAoB,KAAK,EAAE,CAAC,OAAO,CAAC;CACjE;CACA,MAAM,QAAQJ,SAAO,QAAQ,MAAM,KAAK,CAAC;CACzC,MAAM,gBAAgBG,QAAM,iBAAiB,aAAa,SAAS,KAAK,CAAC,CAAC,KAAKH,SAAO,OAAOA,SAAO,QAAQA,SAAO,OAAO;CAC1H,IAAI,SAAS;CACb,MAAM,mBAAmB;EACvB,IAAI,QACF,OAAO,QAAQ,QAAQ;EAEzB,SAAS;EAET,OAAO,WAAW,MAAM,MAAM,OAAOC,OAAK,IAAI,CAAC;CACjD;CACA,MAAM,UAAU,kBAAiB,WAAUD,SAAO,QAAQ,gBAAe,YAAWA,SAAO,QAAQ,QAAQ,OAAO,CAAC,CAAC;CACpH,IAAI,KAAK,WAAW,GAAG;EACrB,MAAM,eAAe,gBAAgB;EACrC,MAAM,gBAAgB,IAAI,IAAI,aAAa,KAAK;EAChD,KAAK,EAAE,CAAC,OAAO;EACf,MAAM,aAAa,aAAa,aAAa,MAAM,QAAO,SAAQ,CAAC,cAAc,IAAI,IAAI,CAAC,CAAC;EAC3F,IAAI,WAAW,WAAW,GAAG;GAC3B,eAAiB,WAAW,GAAG,YAAY,SAAS,OAAO,CAAC;GAC5D;EACF;EACA,MAAM,eAAe,IAAI,IAAI,UAAU;EAGvC,MAAM,uBAAuB,CAAC,WAAW,KAAK,gBAAgB;EAC9D,IAAI,YAAY,WAAW;EAC3B,YAEC,EACC,gBACA,QACA,WACI;GACJ,IAAI,CAAC,aAAa,IAAI,IAAI,GACxB;GAEF,IAAI,sBACF,qBAAqB;IACnB;IACA,IAAI,cAAc,GAChB,OAAO,WAAW;GAEtB,CAAC;GAEH,OAAO,WAAWA,SAAO,OAAO,aAAa,GAAG,EAC9C,OACF,CAAC;EACH,GAAG,YAAY,SAAS,OAAO,CAAC;EAChC,eAAiB,WAAW,GAAG,YAAY,SAAS,OAAO,CAAC;EAC5D;CACF;CACA,OAAOI,SAAW,KAAK,IAAI,oBAAoB;EAC7C,gBAAkB,WAAWJ,SAAO,OAAO,aAAa,CAAC,GAAG,YAAY,SAAS,OAAO,CAAC;EACzF,eAAiB,WAAW,GAAG,YAAY,SAAS,OAAO,CAAC;EAC5D,OAAO,KAAK,EAAE,CAAC,OAAO;CACxB,CAAC;AACH;;AAEA,MAAa,aAAa,MAAM,UAAUE,WAAS,QAAQ,EAAE,MAAM,KAAK,MAAMF,SAAO,QAAQA,SAAO,SAASA,SAAO,MAAM,KAAK,SAAS,OAAO,EAAE,GAAG,SAAS,OAAM,MAAKA,SAAO,QAAQE,WAAS,oBAAoBA,WAAS,gBAAgB,EAAE,OAAO,GAAGA,WAAS,gBAAgB,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAGF,SAAO,KAAK;;AAE7S,MAAa,eAAc,OAAM,YAAY,IAAI;CAC/C,QAAQ,WAAW,KAAKA,SAAO,QAAQA,SAAO,QAAQ,OAAO,CAAC,GAAG,EAAE;CACnE,MAAM,WAAWA,SAAO,QAAQ,EAAE;CAClC;CACA,OAAO,UAAU,EAAE;CACnB,MAAM,SAAS,EAAE;AACnB,CAAC;;AAED,MAAaK,OAAkB,0BAAYC,IAAI;;AAE/C,MAAa,EAEX,QAEA,OAEA,MAEA,SACED;;;;;;;;;;ACtGJ,MAAa,KAAKE;;;ACxKlB,MAAM,UAAU;CAAE,OAAO;CAAyC,aAAa;EAFjE,WAAW;EAAO,UAAU;EAAa,YAAY;EAAO,UAAU;CAEH;AAAE;AAEnF,MAAM,iBAAiB;CACrB,MAAM;CACN,SAAS;EAAE,YAAY;EAAS,OAAO,QAAQ;CAAM;AACvD;AAEA,MAAM,SAAS,UAAkB;CAAE,MAAM;CAA4B;AAAK;AAE1E,MAAM,SACJ,QACA,YAEA,oBAAoB;CAAE,QAAQ,SAAS,KAAK,UAAU;CAAG,WAAW,OAAO;AAAQ,CAAC,CAAC,CACnF,SACA,OACF,CAAC,CAAC,KAAK,OAAO,YAAY,OAAO,MAAM,OAAO,SAAS;;AAGzD,MAAM,cACJ,QACA,SACA,UAEA,OAAO,IAAI,aAAa;CACtB,MAAM,YAAY,OAAO,MAAM,QAAQ,OAAO;CAC9C,IAAI,UAAU,KAAA,GAAW;EACvB,OAAO,OAAO;EACd,OAAO,OAAO,UAAU,MAAM,aAAa,CAAC;EAC5C,OAAO,OAAO,MAAM,MAAM,IAAI;CAChC;CACA,OAAO,OAAO,MAAM,KAAK,SAAS;AACpC,CAAC;AAEH,SAAS,qCAAqC;CAC5C,GAAG,KAAK,4EACN,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO,cAAc,KAAK,EACvC,UAAU;GACR;GACA,MAAM,KAAK;GACX,MAAM,IAAI;GACV;IAAE,MAAM;IAAsB,MAAM;IAAS,OAAO,QAAQ;IAAO,UAAU,CAAC;GAAE;GAChF;IAAE,MAAM;IAAS,OAAO;KAAE,SAAS;KAAa,MAAM;IAAK;GAAE;EAC/D,EACF,CAAC;EACD,MAAM,OAAO,OAAO,WAAW,QAAQ,OAAO,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;GAC1E,MAAM;GACN,WAAW;EACb,CAAC;EAED,aAAO,IAAI,CAAC,CAAC,QACX,KAAK,QAAQ;GACX;IAAE,MAAM;IAAW,MAAM;GAAM;GAC/B;IAAE,MAAM;IAAW,MAAM;GAAQ;GACjC;IAAE,MAAM;IAAS,MAAM;GAAQ;GAC/B;IAAE,MAAM;IAAS,MAAM;IAAQ,SAAS;GAAY;EACtD,CAAC,CACH;EACA,MAAM,OAAQ,OAAO,OAAO;EAC5B,aAAO,KAAK,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ;GACtC;GACA;GACA;EACF,CAAC;CACH,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,CACvB;CAEA,GAAG,KAAK,4DACN,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO,cAAc,KAAK,EAAE,UAAU,CAAC,cAAc,EAAE,CAAC;EACvE,MAAM,OAAO,OAAO,WAAW,QAAQ,OAAO,OAAO,EAAE,MAAM,KAAK,CAAC;EAEnE,aAAO,KAAK,UAAU,IAAI,KAAK,KAAK,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,UAAU,aAAa;CACpF,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,CACvB;CAEA,GAAG,KAAK,gDACN,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO,cAAc,KAAK,EAAE,UAAU,CAAC,cAAc,EAAE,CAAC;EACvE,MAAM,OAAO,OAAO,WAAW,QAAQ,OAAO,KAAK,sBAAsB,CAAC;EAE1E,aAAO,KAAK,UAAU,IAAI,KAAK,KAAK,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,UAAU,sBAAsB;CAC7F,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,CACvB;CAEA,GAAG,OAAO,4DACR,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO,cAAc,KAAK,EACvC,UAAU;GAAC;GAAgB,MAAM,KAAK;GAAG,MAAM,KAAK;EAAC,EACvD,CAAC;EACD,MAAM,YAAY,OAAO,MAAM,QAAQ,OAAO,KAAK;EACnD,OAAO,OAAO;EACd,OAAO,UAAU,SAAS,OAAO,MAAM,UAAU,CAAC;EAClD,OAAO,UAAU,OAAO,UAAU;EAClC,OAAO,OAAO,MAAM,GAAI;EAExB,aAAO,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,QACnC,KAAK,QAAQ;GACX;IAAE,MAAM;IAAW,MAAM;GAAM;GAC/B;IAAE,MAAM;IAAW,MAAM;GAAS;GAClC;IAAE,MAAM;IAAS,MAAM;GAAQ;EACjC,CAAC,CACH;CACF,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,CACvB;AACF,CAAC"}
@@ -1,5 +1,6 @@
1
1
  //#region \0rolldown/runtime.js
2
2
  var __defProp = Object.defineProperty;
3
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
3
4
  var __exportAll = (all, no_symbols) => {
4
5
  let target = {};
5
6
  for (var name in all) __defProp(target, name, {
@@ -10,4 +11,4 @@ var __exportAll = (all, no_symbols) => {
10
11
  return target;
11
12
  };
12
13
  //#endregion
13
- export { __exportAll as t };
14
+ export { __exportAll as n, __commonJSMin as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/mistral",
3
- "version": "0.17.0",
3
+ "version": "0.19.0-dev-20261109183959",
4
4
  "description": "Mistral provider for @effect-uai/core: chat-completions plus Voxtral speech-to-text and text-to-speech.",
5
5
  "keywords": [
6
6
  "ai",
@@ -59,18 +59,18 @@
59
59
  "access": "public"
60
60
  },
61
61
  "dependencies": {
62
- "@effect-uai/chat-completions": "0.17.0"
62
+ "@effect-uai/chat-completions": "0.19.0-dev-20261109183959"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/ws": "^8.5.13",
66
- "effect": "4.0.0-rc.111",
66
+ "effect": "4.1.2",
67
67
  "typescript": "^7.0.2",
68
68
  "ws": "^8.18.0",
69
- "@effect-uai/core": "0.17.0"
69
+ "@effect-uai/core": "0.19.0-dev-20261109183959"
70
70
  },
71
71
  "peerDependencies": {
72
- "@effect-uai/core": ">=0.2.0 <1",
73
- "effect": ">=4.0.0-rc.111 <5.0.0",
72
+ "@effect-uai/core": "0.19.0-dev-20261109183959",
73
+ "effect": ">=4.0.1 <5.0.0",
74
74
  "ws": "^8.0.0"
75
75
  },
76
76
  "peerDependenciesMeta": {
package/src/Mistral.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Context, Effect, Layer, Option, Redacted, Result, Schema, Stream } from "effect"
2
- import { HttpClient, HttpClientRequest } from "effect/unstable/http"
2
+ import { HttpClient, HttpClientRequest } from "effect/http"
3
3
  import * as AiError from "@effect-uai/core/AiError"
4
4
  import * as Capabilities from "@effect-uai/core/Capabilities"
5
5
  import {
@@ -19,6 +19,7 @@ import {
19
19
  decodeChunk,
20
20
  emptyAccumulator,
21
21
  itemsToMessages,
22
+ rejectToolOutputMedia,
22
23
  responseFormatWire,
23
24
  toolChoiceWire,
24
25
  toolsWire,
@@ -123,6 +124,7 @@ const buildStream = (cfg: Config) => {
123
124
  reason:
124
125
  "Assistant messages carry no image on this wire. Use `Turn.imagesAsInput` to resend it as user content.",
125
126
  })
127
+ yield* rejectToolOutputMedia("mistral", request.history)
126
128
  const httpRequest = HttpClientRequest.post(url).pipe(
127
129
  HttpClientRequest.setHeader("authorization", `Bearer ${Redacted.value(cfg.apiKey)}`),
128
130
  HttpClientRequest.bodyJsonUnsafe(buildRequestBody(request)),
@@ -9,7 +9,7 @@
9
9
  * header the WS upgrade requires.
10
10
  */
11
11
  import { Effect, Layer } from "effect"
12
- import { HttpClient } from "effect/unstable/http"
12
+ import { HttpClient } from "effect/http"
13
13
  import {
14
14
  type CommonTranscribeRequest,
15
15
  SttStreaming,
@@ -1,15 +1,6 @@
1
- import {
2
- Array as Arr,
3
- Context,
4
- Effect,
5
- Encoding,
6
- Layer,
7
- Redacted,
8
- Result,
9
- Schema,
10
- Stream,
11
- } from "effect"
12
- import { HttpClient, HttpClientRequest } from "effect/unstable/http"
1
+ import { Array as Arr, Context, Effect, Layer, Redacted, Result, Schema, Stream } from "effect"
2
+ import { Base64 } from "effect/encoding"
3
+ import { HttpClient, HttpClientRequest } from "effect/http"
13
4
  import * as AiError from "@effect-uai/core/AiError"
14
5
  import type { AudioBlob, AudioChunk, AudioFormat } from "@effect-uai/core/Audio"
15
6
  import * as Capabilities from "@effect-uai/core/Capabilities"
@@ -140,7 +131,7 @@ const WireResponse = Schema.Struct({ audio_data: Schema.String })
140
131
  const decodeResponse = Schema.decodeUnknownEffect(WireResponse)
141
132
 
142
133
  const decodeAudioData = (b64: string): Effect.Effect<Uint8Array, AiError.AiError> =>
143
- Result.match(Encoding.decodeBase64(b64), {
134
+ Result.match(Base64.decode(b64), {
144
135
  onSuccess: Effect.succeed,
145
136
  onFailure: (cause) =>
146
137
  Effect.fail(
@@ -1,5 +1,5 @@
1
1
  import { Array as Arr, Context, Effect, Layer, Redacted, Schema, Stream } from "effect"
2
- import { HttpClient, HttpClientRequest } from "effect/unstable/http"
2
+ import { HttpClient, HttpClientRequest } from "effect/http"
3
3
  import * as AiError from "@effect-uai/core/AiError"
4
4
  import * as Capabilities from "@effect-uai/core/Capabilities"
5
5
  import { isAudioUrl } from "@effect-uai/core/Audio"
package/src/audioCodec.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Array as Arr, Effect, Encoding, Match, Option, Result } from "effect"
1
+ import { Array as Arr, Effect, Match, Option, Result } from "effect"
2
+ import { Base64 } from "effect/encoding"
2
3
  import * as AiError from "@effect-uai/core/AiError"
3
4
  import type { AudioFormat, AudioSource } from "@effect-uai/core/Audio"
4
5
 
@@ -7,7 +8,7 @@ import type { AudioFormat, AudioSource } from "@effect-uai/core/Audio"
7
8
  // ---------------------------------------------------------------------------
8
9
 
9
10
  const decodeBase64ToBytes = (b64: string): Effect.Effect<Uint8Array, AiError.AiError> =>
10
- Result.match(Encoding.decodeBase64(b64), {
11
+ Result.match(Base64.decode(b64), {
11
12
  onSuccess: Effect.succeed,
12
13
  onFailure: (cause) =>
13
14
  Effect.fail(new AiError.InvalidRequest({ provider: "mistral", param: "audio", raw: cause })),
package/src/codec.test.ts CHANGED
@@ -19,7 +19,14 @@ describe("itemsToMessages", () => {
19
19
  { type: "message", role: "assistant", content: [{ type: "output_text", text: "checking" }] },
20
20
  { type: "function_call", call_id: "c1", name: "a", arguments: "{}" },
21
21
  { type: "function_call", call_id: "c2", name: "b", arguments: "{}" },
22
- { type: "function_call_output", call_id: "c1", output: "ra" },
22
+ {
23
+ type: "function_call_output",
24
+ call_id: "c1",
25
+ output: [
26
+ { type: "input_text", text: "ra" },
27
+ { type: "input_json", value: { n: 1 } },
28
+ ],
29
+ },
23
30
  ]
24
31
  expect(itemsToMessages(history)).toEqual([
25
32
  {
@@ -30,7 +37,7 @@ describe("itemsToMessages", () => {
30
37
  { id: "c2", type: "function", function: { name: "b", arguments: "{}" } },
31
38
  ],
32
39
  },
33
- { role: "tool", tool_call_id: "c1", content: "ra" },
40
+ { role: "tool", tool_call_id: "c1", content: 'ra\n{"n":1}' },
34
41
  ])
35
42
  })
36
43
 
package/src/codec.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Array as Arr, Effect, Encoding, Match, Option, Result } from "effect"
1
+ import { Array as Arr, Effect, Match, Option, Result } from "effect"
2
+ import { Base64 } from "effect/encoding"
2
3
  import type { ContentBlock, HistoryItem, InputImage } from "@effect-uai/core/Items"
3
4
 
4
5
  // The streaming decoder, tool encoding, and terminal-turn assembly are the
@@ -10,12 +11,14 @@ export {
10
11
  accumulatorToTurn,
11
12
  applyChunk,
12
13
  emptyAccumulator,
14
+ rejectToolOutputMedia,
13
15
  responseFormatWire,
14
16
  toolsWire,
15
17
  } from "@effect-uai/chat-completions/codec"
16
18
  import {
17
19
  type WireChunk,
18
20
  decodeChunk as decodeChunkShared,
21
+ toolOutputText,
19
22
  } from "@effect-uai/chat-completions/codec"
20
23
 
21
24
  // ---------------------------------------------------------------------------
@@ -37,7 +40,7 @@ type WireMessage = {
37
40
  const imageSourceToUrl = Match.type<InputImage["source"]>().pipe(
38
41
  Match.tag("url", (s) => s.url),
39
42
  Match.tag("base64", (s) => `data:${s.mimeType};base64,${s.base64}`),
40
- Match.tag("bytes", (s) => `data:${s.mimeType};base64,${Encoding.encodeBase64(s.bytes)}`),
43
+ Match.tag("bytes", (s) => `data:${s.mimeType};base64,${Base64.encode(s.bytes)}`),
41
44
  Match.exhaustive,
42
45
  )
43
46
 
@@ -104,7 +107,7 @@ const foldItem = (acc: ReadonlyArray<WireMessage>, item: HistoryItem): ReadonlyA
104
107
  function_call: (f) => appendToolCall(acc, f.call_id, f.name, f.arguments),
105
108
  function_call_output: (o) => [
106
109
  ...acc,
107
- { role: "tool", tool_call_id: o.call_id, content: o.output },
110
+ { role: "tool", tool_call_id: o.call_id, content: toolOutputText(o) },
108
111
  ],
109
112
  // Reasoning items are not replayed to the chat-completions endpoint.
110
113
  reasoning: () => acc,
package/src/http.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Effect, Match } from "effect"
2
- import { HttpClientRequest } from "effect/unstable/http"
2
+ import { HttpClientRequest } from "effect/http"
3
3
  import * as AiError from "@effect-uai/core/AiError"
4
4
  import * as Multipart from "@effect-uai/core/Multipart"
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { Effect, Exit, Redacted, Stream } from "effect"
2
- import { HttpClient, HttpClientResponse } from "effect/unstable/http"
2
+ import { HttpClient, HttpClientResponse } from "effect/http"
3
3
  import { describe, expect, it } from "vitest"
4
4
  import { TurnEvent } from "@effect-uai/core/Turn"
5
5
  import { make as makeMistral } from "./Mistral.js"
@@ -0,0 +1,116 @@
1
+ import { describe, it } from "@effect/vitest"
2
+ import { Effect, Exit, Fiber, Redacted, Stream } from "effect"
3
+ import { TestClock } from "effect/testing"
4
+ import { expect } from "vitest"
5
+ import * as FakeWebSocket from "@effect-uai/core/testing/FakeWebSocket"
6
+ import { streamTranscription } from "./realtimeStt.js"
7
+
8
+ const pcm = { container: "raw", encoding: "pcm_s16le", sampleRate: 16000, channels: 1 } as const
9
+
10
+ const request = { model: "voxtral-mini-transcribe-realtime-2602", inputFormat: pcm }
11
+
12
+ const sessionCreated = {
13
+ type: "session.created",
14
+ session: { request_id: "req_1", model: request.model },
15
+ }
16
+
17
+ const delta = (text: string) => ({ type: "transcription.text.delta", text })
18
+
19
+ const start = <E>(
20
+ server: FakeWebSocket.FakeWebSocketServer,
21
+ audioIn: Stream.Stream<Uint8Array, E>,
22
+ ) =>
23
+ streamTranscription({ apiKey: Redacted.make("test-key"), webSocket: server.connect })(
24
+ audioIn,
25
+ request,
26
+ ).pipe(Stream.runCollect, Effect.exit, Effect.forkChild)
27
+
28
+ /** Run a transcription against `server`, closing it, if asked, once the frames are in. */
29
+ const transcribe = <E>(
30
+ server: FakeWebSocket.FakeWebSocketServer,
31
+ audioIn: Stream.Stream<Uint8Array, E>,
32
+ close?: { readonly code: number; readonly afterSent?: number },
33
+ ) =>
34
+ Effect.gen(function* () {
35
+ const collector = yield* start(server, audioIn)
36
+ if (close !== undefined) {
37
+ yield* server.greeted
38
+ yield* server.awaitSent(close.afterSent ?? 0)
39
+ yield* server.close(close.code)
40
+ }
41
+ return yield* Fiber.join(collector)
42
+ })
43
+
44
+ describe("Mistral Realtime STT socket", () => {
45
+ it.live("configures the session before any audio, and ends on a clean close", () =>
46
+ Effect.gen(function* () {
47
+ const server = yield* FakeWebSocket.make({
48
+ greeting: [
49
+ sessionCreated,
50
+ delta("hel"),
51
+ delta("lo"),
52
+ { type: "transcription.done", text: "hello", model: request.model, segments: [] },
53
+ { type: "error", error: { message: "slow down", code: 3001 } },
54
+ ],
55
+ })
56
+ const exit = yield* transcribe(server, Stream.make(new Uint8Array([1, 2])), {
57
+ code: 1000,
58
+ afterSent: 3,
59
+ })
60
+
61
+ expect(exit).toEqual(
62
+ Exit.succeed([
63
+ { _tag: "partial", text: "hel" },
64
+ { _tag: "partial", text: "hello" },
65
+ { _tag: "final", text: "hello" },
66
+ { _tag: "error", code: "3001", message: "slow down" },
67
+ ]),
68
+ )
69
+ const sent = (yield* server.sent) as ReadonlyArray<{ type: string }>
70
+ expect(sent.map((f) => f.type)).toEqual([
71
+ "session.update",
72
+ "input_audio.append",
73
+ "input_audio.end",
74
+ ])
75
+ }).pipe(Effect.scoped),
76
+ )
77
+
78
+ it.live("fails rather than ending when the connection drops", () =>
79
+ Effect.gen(function* () {
80
+ const server = yield* FakeWebSocket.make({ greeting: [sessionCreated] })
81
+ const exit = yield* transcribe(server, Stream.never, { code: 1011 })
82
+
83
+ expect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain("Unavailable")
84
+ }).pipe(Effect.scoped),
85
+ )
86
+
87
+ it.live("fails with the audio input's own error", () =>
88
+ Effect.gen(function* () {
89
+ const server = yield* FakeWebSocket.make({ greeting: [sessionCreated] })
90
+ const exit = yield* transcribe(server, Stream.fail("microphone unplugged"))
91
+
92
+ expect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain("microphone unplugged")
93
+ }).pipe(Effect.scoped),
94
+ )
95
+
96
+ it.effect("commits a synthetic final once the deltas go quiet", () =>
97
+ Effect.gen(function* () {
98
+ const server = yield* FakeWebSocket.make({
99
+ greeting: [sessionCreated, delta("hel"), delta("lo ")],
100
+ })
101
+ const collector = yield* start(server, Stream.never)
102
+ yield* server.greeted
103
+ yield* TestClock.withLive(Effect.sleep("5 millis"))
104
+ yield* TestClock.adjust("1 second")
105
+ yield* server.close(1000)
106
+
107
+ expect(yield* Fiber.join(collector)).toEqual(
108
+ Exit.succeed([
109
+ { _tag: "partial", text: "hel" },
110
+ { _tag: "partial", text: "hello " },
111
+ { _tag: "final", text: "hello" },
112
+ ]),
113
+ )
114
+ }).pipe(Effect.scoped),
115
+ )
116
+ })
@@ -24,24 +24,25 @@
24
24
  * when the deltas go quiet for `utteranceSilence`.
25
25
  */
26
26
  import {
27
- Cause,
28
27
  Clock,
29
28
  Duration,
30
29
  Effect,
31
- Encoding,
32
30
  Match,
33
- Queue,
31
+ Option,
32
+ Predicate,
34
33
  Redacted,
35
34
  Ref,
36
35
  Schema,
37
36
  Stream,
38
37
  } from "effect"
39
- import * as Socket from "effect/unstable/socket/Socket"
38
+ import { Base64 } from "effect/encoding"
39
+ import * as Socket from "effect/socket/Socket"
40
40
  import * as AiError from "@effect-uai/core/AiError"
41
41
  import type { AudioFormat } from "@effect-uai/core/Audio"
42
42
  import * as JSONL from "@effect-uai/core/JSONL"
43
43
  import type { TranscriptEvent } from "@effect-uai/core/Transcript"
44
44
  import type { CommonStreamTranscribeRequest } from "@effect-uai/core/Transcriber"
45
+ import * as WebSocketSession from "@effect-uai/core/WebSocketSession"
45
46
  import { WebSocket as WSWebSocket } from "ws"
46
47
 
47
48
  export type Config = {
@@ -55,6 +56,11 @@ export type Config = {
55
56
  * adapter delimits turns. Default 700 ms.
56
57
  */
57
58
  readonly utteranceSilence?: Duration.Duration
59
+ /**
60
+ * Build the socket yourself, for a proxy or an in-memory transport. The
61
+ * default attaches the bearer token through the `ws` package.
62
+ */
63
+ readonly webSocket?: Socket.WebSocketConstructor["Service"]
58
64
  }
59
65
 
60
66
  const DEFAULT_SILENCE = Duration.millis(700)
@@ -95,7 +101,7 @@ const sessionUpdateFrame = (cfg: Config) =>
95
101
  })
96
102
 
97
103
  const audioAppendFrame = (bytes: Uint8Array) =>
98
- JSON.stringify({ type: "input_audio.append", audio: Encoding.encodeBase64(bytes) })
104
+ JSON.stringify({ type: "input_audio.append", audio: Base64.encode(bytes) })
99
105
 
100
106
  const audioEndFrame = JSON.stringify({ type: "input_audio.end" })
101
107
 
@@ -120,68 +126,73 @@ const ServerEvent = Schema.Union([
120
126
  ])
121
127
  const decodeServerEvent = Schema.decodeUnknownEffect(ServerEvent)
122
128
 
123
- /** Mutable turn state shared by the message handler and the silence finalizer. */
129
+ /** Mutable turn state shared by the frame handler and the silence finalizer. */
124
130
  type TurnState = {
125
131
  readonly text: Ref.Ref<string>
126
132
  readonly lastActivityMs: Ref.Ref<number>
127
133
  }
128
134
 
129
- const emitFinal = (
130
- queue: Queue.Queue<TranscriptEvent, Cause.Done>,
135
+ const takeFinal = (
131
136
  state: TurnState,
132
137
  override?: string,
133
- ): Effect.Effect<void> =>
138
+ ): Effect.Effect<TranscriptEvent | undefined> =>
134
139
  Effect.gen(function* () {
135
140
  const acc = yield* Ref.getAndSet(state.text, "")
136
141
  const text = (override !== undefined && override.length > 0 ? override : acc).trim()
137
- if (text.length > 0) yield* Queue.offer(queue, { _tag: "final", text })
142
+ return text.length > 0 ? { _tag: "final", text } : undefined
138
143
  })
139
144
 
140
- const handleServerMessage =
141
- (queue: Queue.Queue<TranscriptEvent, Cause.Done>, state: TurnState) => (raw: string) =>
145
+ const frameToEvent =
146
+ (state: TurnState) =>
147
+ (raw: string): Effect.Effect<TranscriptEvent | undefined> =>
142
148
  Effect.gen(function* () {
143
149
  const json = yield* JSONL.parseSafe(raw)
144
- if (json === undefined) return
150
+ if (json === undefined) return undefined
145
151
  const decoded = yield* decodeServerEvent(json).pipe(Effect.option)
146
- if (decoded._tag === "None") return
147
- yield* Match.value(decoded.value).pipe(
152
+ if (Option.isNone(decoded)) return undefined
153
+ return yield* Match.value(decoded.value).pipe(
148
154
  Match.when({ type: "transcription.text.delta" }, (m) =>
149
155
  Effect.gen(function* () {
150
156
  const now = yield* Clock.currentTimeMillis
151
157
  const next = yield* Ref.updateAndGet(state.text, (t) => t + m.text)
152
158
  yield* Ref.set(state.lastActivityMs, now)
153
159
  // Cumulative partial so the UI shows the growing sentence.
154
- yield* Queue.offer(queue, { _tag: "partial", text: next })
160
+ return { _tag: "partial", text: next } satisfies TranscriptEvent
155
161
  }),
156
162
  ),
157
163
  // End-of-audio: commit whatever's left as the final utterance.
158
- Match.when({ type: "transcription.done" }, (m) => emitFinal(queue, state, m.text)),
164
+ Match.when({ type: "transcription.done" }, (m) => takeFinal(state, m.text)),
159
165
  Match.when({ type: "error" }, (m) =>
160
- Queue.offer(queue, {
166
+ Effect.succeed<TranscriptEvent>({
161
167
  _tag: "error",
162
168
  ...(m.error.code != null && { code: String(m.error.code) }),
163
169
  message: m.error.message,
164
170
  }),
165
171
  ),
166
172
  // session.created / .updated / language / segment: no user-visible event.
167
- Match.orElse(() => Effect.void),
173
+ Match.orElse(() => Effect.succeed(undefined)),
168
174
  )
169
175
  })
170
176
 
171
- // Background loop: commit a synthetic final once the deltas go quiet.
172
- const silenceFinalizer = (
173
- queue: Queue.Queue<TranscriptEvent, Cause.Done>,
177
+ // Commits a synthetic final once the deltas go quiet; checks now, then every 150 ms.
178
+ const silenceFinals = (
174
179
  state: TurnState,
175
180
  silence: Duration.Duration,
176
- ): Effect.Effect<never> => {
181
+ ): Stream.Stream<TranscriptEvent> => {
177
182
  const silenceMs = Duration.toMillis(silence)
178
- return Effect.gen(function* () {
179
- const now = yield* Clock.currentTimeMillis
180
- const last = yield* Ref.get(state.lastActivityMs)
181
- const acc = yield* Ref.get(state.text)
182
- if (acc.trim().length > 0 && now - last >= silenceMs) yield* emitFinal(queue, state)
183
- yield* Effect.sleep("150 millis")
184
- }).pipe(Effect.forever)
183
+ return Stream.tick("150 millis").pipe(
184
+ Stream.mapEffect(() =>
185
+ Effect.gen(function* () {
186
+ const now = yield* Clock.currentTimeMillis
187
+ const last = yield* Ref.get(state.lastActivityMs)
188
+ const acc = yield* Ref.get(state.text)
189
+ return acc.trim().length > 0 && now - last >= silenceMs
190
+ ? yield* takeFinal(state)
191
+ : undefined
192
+ }),
193
+ ),
194
+ Stream.filter(Predicate.isNotUndefined),
195
+ )
185
196
  }
186
197
 
187
198
  // ---------------------------------------------------------------------------
@@ -208,49 +219,42 @@ export const streamTranscription =
208
219
  Stream.unwrap(
209
220
  Effect.gen(function* () {
210
221
  yield* ensureInputFormat(request.inputFormat)
211
- const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request.model), {
212
- // Effect's Socket treats all close codes as errors by default —
213
- // whitelist standard clean-close codes (1000 / 1001 / 1005).
214
- closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,
215
- }).pipe(Effect.provideService(Socket.WebSocketConstructor, authedWsConstructor(cfg)))
216
- const queue = yield* Queue.bounded<TranscriptEvent, Cause.Done>(64)
222
+ const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request.model)).pipe(
223
+ Effect.provideService(
224
+ Socket.WebSocketConstructor,
225
+ cfg.webSocket ?? authedWsConstructor(cfg),
226
+ ),
227
+ )
217
228
  const state: TurnState = {
218
229
  text: yield* Ref.make(""),
219
230
  lastActivityMs: yield* Ref.make(0),
220
231
  }
221
- const write = yield* socket.writer
232
+ const { write } = yield* socket.writer
233
+ const asAiError = WebSocketSession.toAiError("mistral")
222
234
 
223
- // session.update first, then stream base64 PCM frames, then signal end.
224
- // Fork-scoped so the Stream's downstream scope tears them down on
225
- // disconnect / cancel.
226
- yield* Effect.gen(function* () {
227
- yield* write(sessionUpdateFrame(cfg))
228
- yield* Stream.runForEach(audioIn, (bytes) => write(audioAppendFrame(bytes)))
229
- yield* write(audioEndFrame)
230
- }).pipe(
231
- Effect.tapError((cause) =>
232
- Effect.logWarning("[voxtral-realtime] audio send failed", { cause }),
233
- ),
234
- Effect.ignore,
235
- Effect.forkScoped,
235
+ const frames = Stream.fromPull(Socket.readerString(socket)).pipe(
236
+ Stream.scoped,
237
+ Stream.catchIf(WebSocketSession.isCleanClose, () => Stream.empty),
238
+ Stream.mapError(asAiError),
239
+ Stream.mapEffect(frameToEvent(state)),
240
+ Stream.filter(Predicate.isNotUndefined),
236
241
  )
237
-
238
- yield* silenceFinalizer(queue, state, cfg.utteranceSilence ?? DEFAULT_SILENCE).pipe(
239
- Effect.forkScoped,
242
+ const events = Stream.merge(
243
+ frames,
244
+ silenceFinals(state, cfg.utteranceSilence ?? DEFAULT_SILENCE),
245
+ { haltStrategy: "left" },
240
246
  )
241
247
 
242
- // `Queue.end` flushes pending events then ends the stream cleanly;
243
- // `Queue.shutdown` would drop queued items and interrupt takes. A
244
- // connection/read failure is logged (otherwise the stream would end
245
- // silently with no transcripts).
246
- yield* socket.runString(handleServerMessage(queue, state)).pipe(
247
- Effect.tapError((cause) =>
248
- Effect.logWarning("[voxtral-realtime] socket closed", { cause }),
248
+ // session.update first, then base64 PCM, then signal end. Writes wait for the reader to connect.
249
+ const outgoing = Stream.concat(
250
+ Stream.concat(
251
+ Stream.make(sessionUpdateFrame(cfg)),
252
+ Stream.map(audioIn, audioAppendFrame),
249
253
  ),
250
- Effect.ensuring(Queue.end(queue)),
251
- Effect.forkScoped,
252
- )
254
+ Stream.make(audioEndFrame),
255
+ ).pipe(Stream.mapEffect((frame) => Effect.mapError(write(frame), asAiError)))
253
256
 
254
- return Stream.fromQueue(queue)
257
+ // The server's close ends the transcript; the audio running out does not.
258
+ return Stream.merge(events, Stream.drain(outgoing), { haltStrategy: "left" })
255
259
  }),
256
260
  )