Current section

Files

Jump to
nous lib nous stream_normalizer mistral.ex
Raw

lib/nous/stream_normalizer/mistral.ex

defmodule Nous.StreamNormalizer.Mistral do
@moduledoc """
Stream normalizer for Mistral API.
Handles Mistral's SSE format with string-keyed JSON maps.
Also handles the pre-parsed `{:finish, "stop"}` tuple from SSE `[DONE]` events.
## Format
Mistral streaming chunks arrive as:
- Parsed JSON maps with string keys
- `{:finish, "stop"}` tuple for stream completion
"""
@behaviour Nous.StreamNormalizer
@impl true
def normalize_chunk(chunk) do
case chunk do
# Pre-parsed finish tuple from SSE [DONE] event
{:finish, reason} when is_binary(reason) ->
[{:finish, reason}]
# Already a tuple event, pass through
{event_type, _} = event
when event_type in [:text_delta, :tool_call_delta, :thinking_delta, :error] ->
[event]
# Parsed JSON chunk
chunk when is_map(chunk) ->
[parse_json_chunk(chunk)]
# Binary data (shouldn't happen if SSE parsing is done first)
chunk when is_binary(chunk) ->
parse_sse_chunk(chunk)
_ ->
[{:unknown, chunk}]
end
end
@impl true
def complete_response?(_chunk), do: false
@impl true
def convert_complete_response(_chunk), do: []
defp parse_sse_chunk(data) do
case data do
"data: [DONE]" ->
[{:finish, "stop"}]
"data: " <> json_data ->
case Jason.decode(json_data) do
{:ok, json} -> [parse_json_chunk(json)]
{:error, _} -> [{:unknown, data}]
end
_ ->
[{:unknown, data}]
end
end
defp parse_json_chunk(chunk) do
choices = Map.get(chunk, "choices", [])
choice = List.first(choices)
if choice do
delta = Map.get(choice, "delta", %{})
content = Map.get(delta, "content")
tool_calls = Map.get(delta, "tool_calls")
finish_reason = Map.get(choice, "finish_reason")
cond do
content && content != "" ->
{:text_delta, content}
tool_calls ->
{:tool_call_delta, tool_calls}
finish_reason ->
{:finish, finish_reason}
true ->
{:unknown, chunk}
end
else
{:unknown, chunk}
end
end
end