Current section
Files
Jump to
Current section
Files
lib/ai/stream/openai_compatible_transformer.ex
defmodule AI.Stream.OpenAICompatibleTransformer do
@moduledoc """
Stream transformer for OpenAI-compatible API streams.
This transformer handles streams from OpenAI-compatible APIs like LMStudio or other
open-source/self-hosted models that implement the OpenAI API specification but might
have slight differences in event formatting.
"""
@behaviour AI.Stream.Transformer
alias AI.Stream.Event
@impl AI.Stream.Transformer
def transform(source_stream, options \\ %{}) do
# Extract options or use defaults
# Only append finish event if explicitly requested (defaults to false)
detect_eos = Map.get(options, :detect_end_of_stream, false)
# For test simplicity, if we get a list just process it directly
if is_list(source_stream) do
IO.puts("WHY IS THIS A LIST?")
# Convert each element in the list
result = Enum.map(source_stream, &convert_event_to_struct/1)
# If configured, add end-of-stream event if none exists and the stream is not empty
result =
if detect_eos && result != [] &&
not Enum.any?(result, fn
%Event.Finish{} -> true
_ -> false
end) do
result ++ [%Event.Finish{reason: "complete"}]
else
result
end
# Convert back to a stream
Stream.map(result, & &1)
else
# Otherwise use Stream.resource for a proper stream
transform_stream(source_stream, detect_eos)
end
end
# Process a real stream
defp transform_stream(source_stream, _detect_eos) do
# Simply map each event to its event struct
# This preserves the streaming nature and doesn't buffer events
Stream.map(source_stream, &convert_event_to_struct/1)
end
# Direct conversion for non-streaming case
defp convert_event_to_struct({:text_delta, text}) when is_binary(text),
do: %Event.TextDelta{content: text}
defp convert_event_to_struct({:finish, reason}) when is_binary(reason),
do: %Event.Finish{reason: reason} |> dbg
defp convert_event_to_struct({:error, error}),
do: %Event.Error{error: error}
defp convert_event_to_struct({:tool_call, %{id: id, name: name, arguments: arguments}}),
do: %Event.ToolCall{id: id, name: name, arguments: arguments}
defp convert_event_to_struct({:tool_call_delta, id, delta}) when is_map(delta),
do: %Event.Metadata{data: %{type: :tool_call_delta, id: id, delta: delta}}
defp convert_event_to_struct({:metadata, data}) when is_map(data) do
# Handle special case for some compatible providers that include
# content directly in the metadata instead of as text_delta
cond do
# Check for content at top-level (some providers do this)
is_map(data) && Map.has_key?(data, "content") && is_binary(data["content"]) ->
%Event.TextDelta{content: data["content"]}
# Check for text key (some providers use this format)
is_map(data) && Map.has_key?(data, "text") && is_binary(data["text"]) ->
%Event.TextDelta{content: data["text"]}
# Check in "message" (some local LLMs do this)
is_map(data) && get_in(data, ["message", "content"]) &&
is_binary(get_in(data, ["message", "content"])) ->
%Event.TextDelta{content: get_in(data, ["message", "content"])}
# Regular metadata
true ->
%Event.Metadata{data: data}
end
end
defp convert_event_to_struct(unknown),
do: %Event.Error{error: {:unknown_event_format, unknown}}
end