Current section

Files

Jump to
llm_composer lib llm_composer providers_runner.ex
Raw

lib/llm_composer/providers_runner.ex

defmodule LlmComposer.ProvidersRunner do
@moduledoc """
Handles provider execution logic including fallback strategies, routing,
and error handling for multiple provider configurations.
"""
alias LlmComposer.LlmResponse
alias LlmComposer.Message
alias LlmComposer.Settings
require Logger
@type messages :: [Message.t()]
@doc """
Runs provider execution with fallback support for multiple providers.
"""
@spec run(messages(), Settings.t(), Message.t()) :: {:ok, any()} | {:error, atom()}
def run(messages, %Settings{providers: [{provider, provider_opts}]} = settings, system_msg) do
# only one provider, directly run it.
provider_opts = get_provider_opts(provider_opts, settings)
provider.run(messages, system_msg, provider_opts)
end
def run(messages, %Settings{providers: providers} = settings, system_msg)
when is_list(providers) and length(providers) > 1 do
router = get_provider_router()
execute_with_fallback(router, providers, messages, system_msg, settings)
end
# old case, TODO: remove for llm_composer 0.12.0
def run(
messages,
%Settings{provider: provider, provider_opts: provider_opts} = settings,
system_msg
)
when provider != nil do
provider_opts = get_provider_opts(provider_opts, settings)
provider.run(messages, system_msg, provider_opts)
end
def run(_messages, %Settings{}, _system_msg) do
{:error, :no_providers_configured}
end
@spec execute_with_fallback(
module(),
[{module(), keyword()}],
messages(),
Message.t(),
Settings.t()
) ::
{:ok, any()} | {:error, atom()}
defp execute_with_fallback(router, all_providers, messages, system_msg, settings) do
case router.select_provider(all_providers) do
{:ok, {selected_provider, provider_opts}} ->
provider_opts = get_provider_opts(provider_opts, settings)
{exec_time_us, result} =
:timer.tc(fn ->
selected_provider.run(messages, system_msg, provider_opts)
end)
metrics = build_metrics(result, exec_time_us, selected_provider, provider_opts)
Logger.debug("#{selected_provider.name()} metrics: #{inspect(metrics)}")
case handle_provider_result(result, selected_provider, router, metrics) do
{:halt, ok_res} ->
ok_res
{:cont, _err_res} ->
execute_with_fallback(router, all_providers, messages, system_msg, settings)
end
:none_available ->
{:error, :no_providers_available}
end
end
@spec handle_provider_result(
{:ok, map()} | {:error, any()},
module(),
module(),
map()
) :: {:cont, {:error, any()}} | {:halt, {:ok, any()}}
defp handle_provider_result({:ok, _res} = ok_res, provider, router, metrics) do
router.on_provider_success(provider, ok_res, metrics)
{:halt, ok_res}
end
defp handle_provider_result({:error, error} = err_res, provider, router, metrics) do
Logger.warning(
"[#{provider.name()}] failed (#{inspect(error)}) in #{Float.round(metrics.latency_ms, 2)} ms"
)
router.on_provider_failure(provider, err_res, metrics)
# if failure, we continue with next provider if any
{:cont, err_res}
end
@spec get_provider_opts(keyword(), Settings.t()) :: keyword()
defp get_provider_opts(opts, settings) do
opts
|> Keyword.put_new(:functions, settings.functions)
|> Keyword.put_new(:stream_response, settings.stream_response)
|> Keyword.put_new(:track_costs, settings.track_costs)
|> Keyword.put_new(:api_key, settings.api_key)
end
defp get_provider_router do
:llm_composer
|> Application.get_env(:provider_router, [])
|> Keyword.get(:name, LlmComposer.ProviderRouter.Simple)
end
@spec build_metrics({:ok, LlmResponse.t()} | {:error, term()}, number(), module(), keyword()) ::
map()
defp build_metrics(result, exec_time_us, provider, provider_opts) do
latency_ms = exec_time_us / 1000
status =
case result do
{:ok, _} -> :ok
{:error, _error} -> :error
end
%{
latency_ms: latency_ms,
status: status,
provider: provider.name(),
model: Keyword.fetch!(provider_opts, :model)
}
end
end