Packages
nous
0.11.3
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
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