Skip to content

Migrating to 0.18

0.18 has one required action: upgrade Effect to 4.0 stable. effect-uai’s own API renames nothing.

If you hit a compile error that looks like an effect-uai rename, you are crossing an earlier breaking release. Apply the 0.16 page and the ones before it first.

Required: upgrade Effect

The effect peer dependency moves to the stable line:

BeforeAfter
>=4.0.0-rc.111 <5.0.0>=4.0.1 <5.0.0
Terminal window
pnpm add effect@">=4.0.1 <5.0.0"

Upgrade the @effect/* platform packages you use (@effect/platform-node, @effect/platform-bun, @effect/vitest, …) to 4.0.1 alongside it. This is not optional: effect-uai now imports effect/http, effect/socket and the other modules 4.0 moved out of effect/unstable/*, so a release candidate no longer resolves.

Effect 4.0 changes you may hit in your own code

None of these are effect-uai changes, but they come with the bump:

Effect rcEffect 4.0
effect/unstable/httpeffect/http (likewise every unstable/* path)
Encoding.encodeBase64(bytes)Base64.encode(bytes) from effect/encoding
Encoding.decodeBase64(text)Base64.decode(text) from effect/encoding
Config.redacted("KEY")Config.Redacted("KEY")
Config.string, Config.port, …Config.String, Config.Port, …

Config.option, Config.orElse and Config.withDefault keep their names.

If you use Effect’s Socket directly: runString and the closeCodeIsError option are gone. Read with Stream.fromPull(Socket.readerString(socket)), and note that every close now fails the reader, 1000 included. @effect-uai/core/WebSocketSession exports isCleanClose for exactly that check:

import { Stream } from "effect"
import * as Socket from "effect/socket/Socket"
import { isCleanClose } from "@effect-uai/core/WebSocketSession"
const frames = Stream.fromPull(Socket.readerString(socket)).pipe(
Stream.scoped,
Stream.catchIf(isCleanClose, () => Stream.empty),
)

Behavior change: realtime streams fail when the connection drops

The realtime transcribers and synthesizers (OpenAI, ElevenLabs, Inworld, Mistral) used to end their output stream quietly whatever happened to the socket. Now:

  • A dropped connection fails the stream with AiError. A clean server close (1000, 1001, 1005) still ends it normally.
  • A failing input stream fails the output stream with that error, where it used to be swallowed.

Both were already part of the declared AiError | E error type. If you handle errors on these streams, there is nothing to change: you now receive the failures you were already prepared for. If you relied on the stream always ending cleanly, add the handling you need:

import { Stream } from "effect"
import * as Transcriber from "@effect-uai/core/Transcriber"
mic.frames.pipe(
Transcriber.streamTranscriptionFrom(request),
Stream.catchTag("Unavailable", () => Stream.empty), // treat a drop as the end
)

What’s new (additive, no migration needed)

  • @effect-uai/browser: CdpConnectConfig.headers is sent on the CDP WebSocket handshake, for endpoints that want a bearer token (obscura 0.2.3+ with OBSCURA_CDP_TOKEN). Needs Node or Bun.
  • @effect-uai/discord: gateway resumes send the last sequence number seen, so Discord replays the events missed during a reconnect. They used to send null.
  • @effect-uai/core/testing/FakeWebSocket: greeted and awaitSent(count) let a test wait for the client instead of sleeping.