ML-147: Upgrade to ServerSentEvents 1.0.0

This commit is contained in:
Claudio Ortolina
2026-04-27 22:02:45 +01:00
parent b57ba3c179
commit 597cc89821
5 changed files with 50 additions and 31 deletions
@@ -5,11 +5,12 @@ status: Done
assignee:
- Codex
created_date: '2026-04-24 11:12'
updated_date: '2026-04-25 06:37'
updated_date: '2026-04-27 21:02'
labels: []
dependencies:
- ML-21
priority: low
ordinal: 1000
---
## Description
@@ -1,12 +1,14 @@
---
id: ML-147
title: Upgrade to ServerSentEvents 1.0
status: To Do
status: Done
assignee: []
created_date: '2026-04-25 21:26'
updated_date: '2026-04-27 21:02'
labels: []
dependencies: []
priority: medium
ordinal: 2000
---
## Description
+43 -27
View File
@@ -9,6 +9,11 @@ defmodule OpenAI.API do
Mid-stream failures in `chat_stream/6` come from parsed SSE event payloads
rather than HTTP responses and remain as message strings passed through the
stream callback plumbing.
`chat_stream/6` streams response chunks with `Req`'s `into: :self`, so the
calling process receives and consumes Req's async body messages while the
request is active. Call it from a short-lived worker/task process rather than
directly from a LiveView or GenServer that has unrelated mailbox traffic.
"""
alias OpenAI.API.ErrorResponse
@@ -63,40 +68,18 @@ defmodule OpenAI.API do
stream: true,
temperature: temperature
},
into: fn {:data, data}, {req, resp} ->
buffer = Request.get_private(req, :sse_buffer, "")
{events, buffer} = ServerSentEvents.parse(buffer <> data)
req = Request.put_private(req, :sse_buffer, buffer)
decode_events(events, cb, req, resp)
end
into: :self
)
|> do_chat_stream()
|> do_chat_stream(cb)
end
defp decode_events([], _cb, req, resp), do: {:cont, {req, resp}}
defp decode_events([%{data: json} | rest], cb, req, resp) do
case decode_responses_event(json, cb) do
{:error, message} ->
Logger.error(message)
{:halt, {req, Req.Response.put_private(resp, :error, message)}}
_ ->
decode_events(rest, cb, req, resp)
end
end
defp do_chat_stream(req) do
defp do_chat_stream(req, cb) do
case Req.post(req) do
{:ok, %{status: status} = resp} when status in 200..299 ->
if message = Req.Response.get_private(resp, :error) do
{:error, message}
else
:ok
end
decode_response_stream(resp.body, cb)
{:ok, response} ->
response = decode_async_error_body(response)
{:error, ErrorResponse.from_response(response)}
{:error, exception} ->
@@ -104,6 +87,39 @@ defmodule OpenAI.API do
end
end
defp decode_response_stream(stream, cb) do
stream
|> ServerSentEvents.decode_stream()
|> Enum.reduce_while(:ok, fn %{data: json}, :ok ->
case decode_responses_event(json, cb) do
{:error, message} ->
Logger.error(message)
{:halt, {:error, message}}
_ ->
{:cont, :ok}
end
end)
end
defp decode_async_error_body(%{body: %Req.Response.Async{} = body} = response) do
body =
body
|> Enum.into("")
|> decode_error_body()
%{response | body: body}
end
defp decode_async_error_body(response), do: response
defp decode_error_body(body) when is_binary(body) do
case JSON.decode(body) do
{:ok, decoded} -> decoded
{:error, _reason} -> body
end
end
@spec get_embeddings(String.t(), OpenAI.Config.t()) ::
{:ok, [float()]} | {:error, ErrorResponse.t() | Exception.t()}
def get_embeddings(text, config) do
+1 -1
View File
@@ -129,7 +129,7 @@ defmodule MusicLibrary.MixProject do
# HTTP Clients
{:finch, "~> 0.21.0"},
{:req, "~> 0.5.8"},
{:server_sent_events, "~> 0.2"},
{:server_sent_events, "~> 1.0"},
# Mailers
{:idna, "~> 7.1", override: true},
+1 -1
View File
@@ -67,7 +67,7 @@
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
"rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
"server_sent_events": {:hex, :server_sent_events, "0.2.1", "f83b34f01241302a8bf451efc8dde3a36c533d5715463c31c653f3db8695f636", [:mix], [], "hexpm", "c8099ce4f9acd610eb7c8e0f89dba7d5d1c13300ea9884b0bd8662401d3cf96f"},
"server_sent_events": {:hex, :server_sent_events, "1.0.0", "e82089ac6b93ebd3c0562fd728492bbe4b5140678ffc891abfa8cce717c2c1ff", [:mix], [], "hexpm", "7899caea3e27850549f671fc9e6c53d55a8e6a78474f6b9623820aae6bb41ec7"},
"sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"},
"sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"},
"spitfire": {:hex, :spitfire, "0.3.11", "79dfcb033762470de472c1c26ea2b4e3aca74700c685dbffd9a13466272c323d", [:mix], [], "hexpm", "eb6e2dadf63214e8bfe65ca9788cef2b03b01027365d78d3c0e3d9ebd3d5b7b4"},