Streaming
Every effect system supported by sttp-ai exposes the same extension method — createStreamedInteraction — added to GeminiClient, taking an InteractionRequest. It turns on server-sent-event streaming (stream = true) and decodes each event into an InteractionStreamEvent.
Effect System |
Import |
Element type |
|---|---|---|
fs2 (cats-effect) |
|
|
ZIO |
|
|
Akka Streams (Scala 2.13 only) |
|
|
Pekko Streams |
|
|
Ox (Scala 3 only) |
|
|
InteractionStreamEvent.eventType is deliberately an open String, not an enum: the API documents interaction.completed and error, plus additional incremental event types that may be added over time. Match on the value you care about and ignore the rest — an unrecognized eventType must not break your consumer.
case class InteractionStreamEvent(
eventType: String,
eventId: Option[String] = None,
interaction: Option[InteractionResponse] = None,
error: Option[StreamError] = None,
metadata: Option[StreamMetadata] = None
)
Using fs2 (cats-effect)
//> using dep com.softwaremill.sttp.ai::gemini:0.8.0
//> using dep com.softwaremill.sttp.ai::fs2:0.8.0
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import sttp.ai.gemini.GeminiClient
import sttp.ai.gemini.requests.InteractionRequest
import sttp.ai.gemini.streaming.fs2.GeminiFs2Streaming.*
import sttp.client4.httpclient.fs2.HttpClientFs2Backend
object FS2StreamingExample:
def main(args: Array[String]): Unit =
val client = GeminiClient.fromEnv
val request = InteractionRequest.simple("gemini-2.5-flash", "Write a haiku about the sea.")
val program = HttpClientFs2Backend.resource[IO]().use { backend =>
client
.createStreamedInteraction[IO](request)
.send(backend)
.map(_.body)
.flatMap {
case Left(error) => IO.println(s"Error: ${error.getMessage}")
case Right(stream) =>
stream
.evalTap { event =>
IO.println(event.eventType) *> IO.whenA(event.eventType == "interaction.completed") {
IO.println(event.interaction.map(_.outputText).getOrElse(""))
}
}
.compile
.drain
}
}
program.unsafeRunSync()
Using Ox (Scala 3)
The Ox extension is defined directly on GeminiClient, returning a plain Request (not a StreamRequest, since Ox streams from a blocking InputStream rather than a stream capability type) whose body is a Flow[Either[Exception, InteractionStreamEvent]] — deserialization failures of individual events surface per-element as a Left, instead of failing the whole flow. Any sttp4 backend works, e.g. the plain blocking DefaultSyncBackend.
//> using dep com.softwaremill.sttp.ai::gemini:0.8.0
//> using dep com.softwaremill.sttp.ai::ox:0.8.0
import sttp.ai.gemini.GeminiClient
import sttp.ai.gemini.requests.InteractionRequest
import sttp.ai.gemini.streaming.ox.*
import sttp.client4.DefaultSyncBackend
object OxStreamingExample:
def main(args: Array[String]): Unit =
val client = GeminiClient.fromEnv
val request = InteractionRequest.simple("gemini-2.5-flash", "Write a haiku about the sea.")
val backend = DefaultSyncBackend()
try
client.createStreamedInteraction(request).send(backend).body match {
case Left(error) => println(s"Error: ${error.getMessage}")
case Right(flow) =>
flow.runForeach {
case Right(event) => println(event.eventType)
case Left(error) => println(s"Deserialization error: ${error.getMessage}")
}
}
finally backend.close()