Packages
llm_composer
0.20.1
0.20.2
0.20.1
0.20.0
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.2
0.16.1
0.16.0
0.15.0
0.14.2
0.14.1
0.14.0
0.13.1
0.13.0
0.12.3
0.12.2
0.12.0
0.11.2
0.11.1
0.11.0
0.10.0
0.8.0
0.7.0
0.6.0
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.0
LlmComposer is an Elixir library that facilitates chat interactions with language models, providing tools to handle user messages, generate responses, and execute functions automatically based on model outputs.
Current section
Files
Jump to
Current section
Files
lib/llm_composer/provider_response/parser/open_ai.ex
defmodule LlmComposer.ProviderResponse.Parser.OpenAI do
@moduledoc false
alias LlmComposer.Cost.CostAssembler
alias LlmComposer.FunctionCallExtractors
alias LlmComposer.Helpers
alias LlmComposer.LlmResponse
alias LlmComposer.Message
require Logger
@spec parse({:ok | :error, any()}, atom(), keyword()) ::
{:ok, LlmResponse.t()} | {:error, term()}
def parse({:error, %{body: body}}, _provider, _opts), do: {:error, body}
def parse({:error, resp}, _provider, _opts), do: {:error, resp}
def parse({:ok, %{response: stream}} = raw_result, provider, _opts) when is_function(stream) do
{:ok,
LlmResponse.new(%{
provider: provider,
status: :ok,
stream: stream,
raw: raw_result
})}
end
def parse({:ok, %{response: %Stream{} = stream}} = raw_result, provider, _opts) do
{:ok,
LlmResponse.new(%{
provider: provider,
status: :ok,
stream: stream,
raw: raw_result
})}
end
def parse({:ok, %{response: response} = provider_response}, provider, opts) do
if streamed_chunks?(response) do
{:ok,
LlmResponse.new(%{
provider: provider,
status: :ok,
stream: response,
raw: %{provider_response | response: response}
})}
else
response = Helpers.normalize_json(response)
log_fallback(response, opts)
case Map.get(response, "choices", []) do
[first_choice | _rest] ->
main_response = Map.get(first_choice, "message", %{})
role = normalize_role(Map.get(main_response, "role", "assistant"))
content = normalize_content(Map.get(main_response, "content"))
base_msg = Message.new(role, content, %{original: main_response})
message = %{
base_msg
| content: content,
function_calls: FunctionCallExtractors.from_tool_calls(main_response),
reasoning: Map.get(main_response, "reasoning"),
reasoning_details: Map.get(main_response, "reasoning_details")
}
{input_tokens, output_tokens, cached_tokens} =
CostAssembler.extract_tokens(provider, response)
cost_info = CostAssembler.get_cost_info(provider, response, opts)
{:ok,
LlmResponse.new(%{
cached_tokens: cached_tokens,
provider_model: response["model"],
provider: provider,
status: :ok,
main_response: message,
input_tokens: input_tokens,
output_tokens: output_tokens,
cost_info: cost_info,
metadata: Map.get(provider_response, :metadata, %{}),
raw: response,
reasoning_tokens:
get_in(response, ["usage", "completion_tokens_details", "reasoning_tokens"]),
response_id: response["id"]
})}
[] ->
Logger.warning("[#{provider}] response had no choices: #{inspect(response)}")
{:error,
%{
reason: :missing_choices,
provider: provider,
response: response
}}
end
end
end
def parse(result, provider, _opts) do
{:error,
%{
reason: :unhandled_response_format,
provider: provider,
response: result
}}
end
defp log_fallback(response, opts) do
if Keyword.get(opts, :models) && not is_function(response) do
original_model = Keyword.get(opts, :model)
used_model = Map.get(response, "model")
if original_model && used_model && original_model != used_model do
Logger.warning("The '#{used_model}' model has been used instead of '#{original_model}'")
end
end
end
defp normalize_role(role) when is_atom(role), do: role
defp normalize_role(role) when is_binary(role), do: String.to_existing_atom(role)
defp normalize_role(_role), do: :assistant
defp normalize_content(content) when is_binary(content) or is_nil(content), do: content
defp normalize_content(content) when is_list(content) do
Enum.map_join(content, "", fn
%{"text" => text} when is_binary(text) -> text
%{text: text} when is_binary(text) -> text
_ -> ""
end)
end
defp normalize_content(_content), do: nil
defp streamed_chunks?(response) when is_list(response), do: Enum.all?(response, &is_binary/1)
defp streamed_chunks?(_response), do: false
end