Current section

Files

Jump to
erlang_adk src models adk_llm.erl
Raw

src/models/adk_llm.erl

-module(adk_llm).
-type config() :: map().
-type memory() :: list(map()).
-type tools() :: list(module() | map()).
-type provider_outcome() :: {ok, binary() | string() |
adk_content:content()}
| {tool_calls, list()}
| streamed.
-type provider_result() ::
{provider_result,
#{version := 1,
provider := binary(),
type := binary(),
outcome := provider_outcome(),
metadata := map()}}.
-export_type([config/0, memory/0, tools/0,
provider_outcome/0, provider_result/0]).
-callback generate(Config :: config(), Memory :: memory(), Tools :: tools()) ->
{ok, binary() | string() | adk_content:content()}
| {tool_calls, list()} | provider_result() | {error, term()}.
-callback stream(Config :: config(), Memory :: memory(), Tools :: tools(), Callback :: fun((binary()) -> ok)) ->
ok | {tool_calls, list()} | provider_result() | {error, term()}.
-callback stream_content(Config :: config(), Memory :: memory(),
Tools :: tools(),
Callback :: fun((adk_content:content()) -> ok)) ->
ok | {tool_calls, list()} | provider_result() | {error, term()}.
%% Providers may expose these callbacks without forcing older/custom providers
%% to change. Capabilities describe this adapter's implemented behavior, not
%% every feature the remote model might support.
-callback capabilities() -> map().
-callback validate_config(Config :: config()) -> ok | {error, term()}.
-optional_callbacks([stream_content/4, capabilities/0, validate_config/1]).
-export([generate/3, stream/4, stream_content/4,
capabilities/1, validate_config/1]).
%% @doc Dispatch the call to the specified provider.
generate(Config, Memory, Tools) ->
dispatch(Config, generate, [Config, Memory, Tools]).
%% @doc Dispatch the streaming call to the specified provider.
stream(Config, Memory, Tools, Callback) ->
dispatch(Config, stream, [Config, Memory, Tools, Callback]).
%% @doc Dispatch the optional canonical content-delta stream. Providers which
%% implement text streaming only fail explicitly instead of coercing maps to
%% text or silently dropping parts.
stream_content(Config, Memory, Tools, Callback) ->
dispatch_optional(Config, stream_content,
[Config, Memory, Tools, Callback], content_streaming).
%% @doc Return normalized adapter capabilities. Unknown custom providers remain
%% usable and conservatively report only the two required behavior callbacks.
-spec capabilities(config() | module()) -> {ok, map()} | {error, term()}.
capabilities(Provider) when is_atom(Provider) ->
provider_capabilities(Provider);
capabilities(Config) when is_map(Config) ->
case provider(Config) of
{ok, Provider} -> provider_capabilities(Provider);
{error, _} = Error -> Error
end;
capabilities(Value) ->
{error, {invalid_llm_config, Value}}.
%% @doc Validate the provider reference and invoke its optional validator.
-spec validate_config(config()) -> ok | {error, term()}.
validate_config(Config) when is_map(Config) ->
case provider(Config) of
{ok, Provider} ->
case erlang:function_exported(Provider, validate_config, 1) of
true ->
try Provider:validate_config(Config) of
ok -> ok;
{error, _} = Error -> Error;
Other -> {error, {invalid_provider_validation, Other}}
catch
Class:Reason ->
{error, adk_failure:exception(
llm_provider, validate_config,
Class, Reason)}
end;
false -> ok
end;
{error, _} = Error -> Error
end;
validate_config(Value) ->
{error, {invalid_llm_config, Value}}.
dispatch(Config, Function, Args) ->
case validate_config(Config) of
ok ->
{ok, Provider} = provider(Config),
try apply(Provider, Function, Args) of
{error, Reason} ->
{error, provider_error(Function, Reason)};
Result -> Result
catch
Class:Reason ->
{error, adk_failure:exception(
llm_provider, Function, Class, Reason)}
end;
{error, _} = Error -> Error
end.
dispatch_optional(Config, Function, Args, Capability) ->
case validate_config(Config) of
ok ->
{ok, Provider} = provider(Config),
case erlang:function_exported(Provider, Function, length(Args)) of
true ->
try apply(Provider, Function, Args) of
{error, Reason} ->
{error, provider_error(Function, Reason)};
Result -> Result
catch
Class:Reason ->
{error, adk_failure:exception(
llm_provider, Function,
Class, Reason)}
end;
false ->
{error, {unsupported_provider_capability, Capability}}
end;
{error, _} = Error -> Error
end.
provider(Config) ->
case maps:find(provider, Config) of
{ok, Provider} when is_atom(Provider) ->
case code:ensure_loaded(Provider) of
{module, Provider} ->
case erlang:function_exported(Provider, generate, 3)
andalso erlang:function_exported(
Provider, stream, 4) of
true -> {ok, Provider};
false -> {error, {invalid_llm_provider, Provider}}
end;
{error, Reason} ->
{error, {llm_provider_unavailable, Provider,
adk_failure:external(
llm_provider, load, Reason)}}
end;
{ok, Invalid} -> {error, {invalid_llm_provider, Invalid}};
error -> {error, missing_llm_provider}
end.
provider_capabilities(Provider) when is_atom(Provider) ->
case code:ensure_loaded(Provider) of
{module, Provider} ->
Required = #{generate => erlang:function_exported(
Provider, generate, 3),
streaming => erlang:function_exported(
Provider, stream, 4)},
case maps:get(generate, Required)
andalso maps:get(streaming, Required) of
false -> {error, {invalid_llm_provider, Provider}};
true ->
case erlang:function_exported(
Provider, capabilities, 0) of
false -> {ok, Required};
true ->
try Provider:capabilities() of
Capabilities when is_map(Capabilities) ->
{ok, maps:merge(Required, Capabilities)};
Other ->
{error, {invalid_provider_capabilities,
Provider, Other}}
catch
Class:Reason ->
{error, adk_failure:exception(
llm_provider, capabilities,
Class, Reason)}
end
end
end;
{error, Reason} ->
{error, {llm_provider_unavailable, Provider,
adk_failure:external(
llm_provider, load, Reason)}}
end.
%% Data-free provider status atoms are part of the documented adapter
%% contract. Any compound term may carry a response body or credential and is
%% therefore replaced by structural metadata.
provider_error(_Function, Reason) when is_atom(Reason) -> Reason;
provider_error(Function, Reason) ->
adk_failure:external(llm_provider, Function, Reason).