feat: start on AI integration
This commit is contained in:
108
lib/bds/ai/openai_compatible_runtime.ex
Normal file
108
lib/bds/ai/openai_compatible_runtime.ex
Normal file
@@ -0,0 +1,108 @@
|
||||
defmodule BDS.AI.OpenAICompatibleRuntime do
|
||||
@moduledoc false
|
||||
|
||||
alias BDS.AI.HttpClient
|
||||
|
||||
def generate(endpoint, request, _opts) when is_map(endpoint) and is_map(request) do
|
||||
url = completions_url(endpoint.url)
|
||||
|
||||
headers =
|
||||
%{
|
||||
"content-type" => "application/json",
|
||||
"accept" => "application/json"
|
||||
}
|
||||
|> maybe_put_auth(endpoint.api_key)
|
||||
|
||||
payload = %{
|
||||
"model" => request.model,
|
||||
"messages" => request.messages,
|
||||
"max_tokens" => request.max_output_tokens
|
||||
}
|
||||
|> maybe_put_tools(request.tools)
|
||||
|
||||
with {:ok, response} <- HttpClient.post(url, headers, Jason.encode!(payload)),
|
||||
200 <- response.status do
|
||||
normalize_response(response.body)
|
||||
else
|
||||
status when is_integer(status) -> {:error, %{kind: :http_error, status: status}}
|
||||
{:error, reason} -> {:error, %{kind: :http_error, reason: reason}}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_response(body) do
|
||||
payload = Jason.decode!(body)
|
||||
message = get_in(payload, ["choices", Access.at(0), "message"]) || %{}
|
||||
content = normalize_content(message["content"])
|
||||
tool_calls = normalize_tool_calls(message["tool_calls"] || [])
|
||||
usage = normalize_usage(payload["usage"] || %{})
|
||||
|
||||
json =
|
||||
case content do
|
||||
nil -> nil
|
||||
value when is_binary(value) ->
|
||||
case Jason.decode(value) do
|
||||
{:ok, decoded} when is_map(decoded) -> decoded
|
||||
_other -> nil
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, %{content: content, json: json, tool_calls: tool_calls, usage: usage}}
|
||||
end
|
||||
|
||||
defp completions_url(url) do
|
||||
cond do
|
||||
String.ends_with?(url, "/chat/completions") -> url
|
||||
String.ends_with?(url, "/") -> url <> "chat/completions"
|
||||
true -> url <> "/chat/completions"
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_put_auth(headers, nil), do: headers
|
||||
defp maybe_put_auth(headers, ""), do: headers
|
||||
defp maybe_put_auth(headers, api_key), do: Map.put(headers, "authorization", "Bearer #{api_key}")
|
||||
|
||||
defp maybe_put_tools(payload, []), do: payload
|
||||
defp maybe_put_tools(payload, nil), do: payload
|
||||
|
||||
defp maybe_put_tools(payload, tools) do
|
||||
Map.put(payload, "tools", tools)
|
||||
|> Map.put("tool_choice", "auto")
|
||||
end
|
||||
|
||||
defp normalize_tool_calls(tool_calls) do
|
||||
Enum.map(tool_calls, fn tool_call ->
|
||||
%{
|
||||
id: tool_call["id"],
|
||||
name: get_in(tool_call, ["function", "name"]),
|
||||
arguments: decode_arguments(get_in(tool_call, ["function", "arguments"]))
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp decode_arguments(nil), do: %{}
|
||||
|
||||
defp decode_arguments(arguments) when is_binary(arguments) do
|
||||
case Jason.decode(arguments) do
|
||||
{:ok, decoded} when is_map(decoded) -> decoded
|
||||
_other -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_content(nil), do: nil
|
||||
defp normalize_content(content) when is_binary(content), do: content
|
||||
|
||||
defp normalize_content(content) when is_list(content) do
|
||||
content
|
||||
|> Enum.map(fn item -> item["text"] || "" end)
|
||||
|> Enum.join()
|
||||
end
|
||||
|
||||
defp normalize_usage(usage) do
|
||||
%{
|
||||
input_tokens: usage["prompt_tokens"],
|
||||
output_tokens: usage["completion_tokens"],
|
||||
cache_read_tokens: get_in(usage, ["prompt_tokens_details", "cached_tokens"]),
|
||||
cache_write_tokens: get_in(usage, ["completion_tokens_details", "cached_tokens"])
|
||||
}
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user