Current section
Files
Jump to
Current section
Files
src/starlet.erl
-module(starlet).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/starlet.gleam").
-export([provider_name/1, mock_client/1, from_provider/2, system/2, user/2, assistant/2, temperature/2, max_tokens/2, with_timeout/2, timeout/1, with_tools/2, tools/1, with_json_output/2, with_free_text/1, text/1, json/1, tool_calls/1, has_tool_calls/1, make_turn_for_testing/1, send/1, with_tool_results/2, apply_tool_results/3, step/1, chat/2]).
-export_type([starlet_error/0, message/0, request/0, response/0, provider_config/1, tools_off/0, tools_on/0, free_text/0, json_format/0, empty/0, ready/0, no_ext/0, step/2, client/1, chat/4, turn/3]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" A unified, provider-agnostic interface for LLM APIs.\n"
"\n"
" ## Quick Start\n"
"\n"
" ```gleam\n"
" import starlet\n"
" import starlet/ollama\n"
"\n"
" let client = ollama.new(\"http://localhost:11434\")\n"
"\n"
" let chat =\n"
" starlet.chat(client, \"qwen3:0.6b\")\n"
" |> starlet.system(\"You are a helpful assistant.\")\n"
" |> starlet.user(\"Hello!\")\n"
"\n"
" case starlet.send(chat) {\n"
" Ok(#(new_chat, turn)) -> starlet.text(turn)\n"
" Error(err) -> // handle error\n"
" }\n"
" ```\n"
"\n"
" ## Typestate\n"
"\n"
" The `Chat` type uses phantom types to enforce correct usage at compile time:\n"
" - You must add a user message before sending\n"
" - System prompts can only be set before adding messages\n"
"\n"
" ## Error Handling\n"
"\n"
" ```gleam\n"
" import starlet.{Transport, Http, Decode, Provider}\n"
"\n"
" case starlet.send(chat) {\n"
" Ok(#(chat, turn)) -> // success\n"
" Error(Transport(msg)) -> // network error\n"
" Error(Http(status, body)) -> // non-200 response\n"
" Error(Decode(msg)) -> // JSON parse error\n"
" Error(Provider(name, msg, raw)) -> // provider error\n"
" }\n"
" ```\n"
).
-type starlet_error() :: {transport, binary()} |
{http, integer(), binary()} |
{decode, binary()} |
{provider, binary(), binary(), binary()} |
{tool, starlet@tool:tool_error()} |
{rate_limited, gleam@option:option(integer())}.
-type message() :: {user_message, binary()} |
{assistant_message, binary(), list(starlet@tool:call())} |
{tool_result_message, binary(), binary(), binary()}.
-type request() :: {request,
binary(),
gleam@option:option(binary()),
list(message()),
list(starlet@tool:definition()),
gleam@option:option(float()),
gleam@option:option(integer()),
gleam@option:option(gleam@json:json()),
integer()}.
-type response() :: {response, binary(), list(starlet@tool:call())}.
-type provider_config(ORL) :: {provider_config,
binary(),
binary(),
fun((request(), ORL) -> {ok, {response(), ORL}} |
{error, starlet_error()})}.
-type tools_off() :: tools_off.
-type tools_on() :: tools_on.
-type free_text() :: free_text.
-type json_format() :: json_format.
-type empty() :: empty.
-type ready() :: ready.
-type no_ext() :: no_ext.
-type step(ORM, ORN) :: {done,
chat(tools_on(), ORM, ready(), ORN),
turn(tools_on(), ORM, ORN)} |
{tool_call,
chat(tools_on(), ORM, ready(), ORN),
turn(tools_on(), ORM, ORN),
list(starlet@tool:call())}.
-type client(ORO) :: {client, provider_config(ORO), ORO}.
-type chat(ORP, ORQ, ORR, ORS) :: {chat,
client(ORS),
binary(),
gleam@option:option(binary()),
list(message()),
list(starlet@tool:definition()),
gleam@option:option(float()),
gleam@option:option(integer()),
ORS,
gleam@option:option(gleam@json:json()),
integer()} |
{gleam_phantom, ORP, ORQ, ORR}.
-type turn(ORT, ORU, ORV) :: {turn, binary(), list(starlet@tool:call()), ORV} |
{gleam_phantom, ORT, ORU}.
-file("src/starlet.gleam", 161).
?DOC(" Returns the name of the provider (e.g., \"ollama\", \"openai\").\n").
-spec provider_name(client(any())) -> binary().
provider_name(Client) ->
{client, P, _} = Client,
erlang:element(2, P).
-file("src/starlet.gleam", 167).
?DOC(false).
-spec mock_client(
fun((request()) -> {ok, response()} | {error, starlet_error()})
) -> client(no_ext()).
mock_client(Respond) ->
Send = fun(Req, Ext) -> case Respond(Req) of
{ok, Response} ->
{ok, {Response, Ext}};
{error, E} ->
{error, E}
end end,
{client, {provider_config, <<"mock"/utf8>>, <<""/utf8>>, Send}, no_ext}.
-file("src/starlet.gleam", 182).
?DOC(false).
-spec from_provider(provider_config(OSB), OSB) -> client(OSB).
from_provider(P, Default_ext) ->
{client, P, Default_ext}.
-file("src/starlet.gleam", 238).
?DOC(
" Sets the system prompt for the chat.\n"
"\n"
" Must be called before adding any user messages.\n"
).
-spec system(chat(OSK, OSL, empty(), OSM), binary()) -> chat(OSK, OSL, empty(), OSM).
system(Chat, Text) ->
{chat,
erlang:element(2, Chat),
erlang:element(3, Chat),
{some, Text},
erlang:element(5, Chat),
erlang:element(6, Chat),
erlang:element(7, Chat),
erlang:element(8, Chat),
erlang:element(9, Chat),
erlang:element(10, Chat),
erlang:element(11, Chat)}.
-file("src/starlet.gleam", 248).
?DOC(
" Adds a user message to the chat.\n"
"\n"
" This transitions the chat to the `Ready` state, allowing it to be sent.\n"
).
-spec user(chat(OSV, OSW, any(), OSY), binary()) -> chat(OSV, OSW, ready(), OSY).
user(Chat, Text) ->
{chat,
erlang:element(2, Chat),
erlang:element(3, Chat),
erlang:element(4, Chat),
lists:append(erlang:element(5, Chat), [{user_message, Text}]),
erlang:element(6, Chat),
erlang:element(7, Chat),
erlang:element(8, Chat),
erlang:element(9, Chat),
erlang:element(10, Chat),
erlang:element(11, Chat)}.
-file("src/starlet.gleam", 259).
?DOC(
" Adds an assistant message to the chat history.\n"
"\n"
" Useful for providing few-shot examples or resuming a conversation.\n"
" Requires the chat to already have a user message.\n"
).
-spec assistant(chat(OTH, OTI, ready(), OTJ), binary()) -> chat(OTH, OTI, ready(), OTJ).
assistant(Chat, Text) ->
{chat,
erlang:element(2, Chat),
erlang:element(3, Chat),
erlang:element(4, Chat),
lists:append(erlang:element(5, Chat), [{assistant_message, Text, []}]),
erlang:element(6, Chat),
erlang:element(7, Chat),
erlang:element(8, Chat),
erlang:element(9, Chat),
erlang:element(10, Chat),
erlang:element(11, Chat)}.
-file("src/starlet.gleam", 272).
?DOC(
" Sets the sampling temperature (typically 0.0 to 2.0).\n"
"\n"
" Lower values make output more deterministic, higher values more creative.\n"
).
-spec temperature(chat(OTS, OTT, OTU, OTV), float()) -> chat(OTS, OTT, OTU, OTV).
temperature(Chat, Value) ->
{chat,
erlang:element(2, Chat),
erlang:element(3, Chat),
erlang:element(4, Chat),
erlang:element(5, Chat),
erlang:element(6, Chat),
{some, Value},
erlang:element(8, Chat),
erlang:element(9, Chat),
erlang:element(10, Chat),
erlang:element(11, Chat)}.
-file("src/starlet.gleam", 280).
?DOC(" Sets the maximum number of tokens to generate in the response.\n").
-spec max_tokens(chat(OUE, OUF, OUG, OUH), integer()) -> chat(OUE, OUF, OUG, OUH).
max_tokens(Chat, Value) ->
{chat,
erlang:element(2, Chat),
erlang:element(3, Chat),
erlang:element(4, Chat),
erlang:element(5, Chat),
erlang:element(6, Chat),
erlang:element(7, Chat),
{some, Value},
erlang:element(9, Chat),
erlang:element(10, Chat),
erlang:element(11, Chat)}.
-file("src/starlet.gleam", 297).
?DOC(
" Sets the HTTP request timeout in milliseconds.\n"
"\n"
" Default is 60,000ms (60 seconds). Increase for long-running requests.\n"
"\n"
" ```gleam\n"
" starlet.chat(client, \"gpt-4o\")\n"
" |> starlet.with_timeout(120_000) // 2 minutes\n"
" |> starlet.user(\"Solve this complex problem...\")\n"
" |> starlet.send()\n"
" ```\n"
).
-spec with_timeout(chat(OUQ, OUR, OUS, OUT), integer()) -> chat(OUQ, OUR, OUS, OUT).
with_timeout(Chat, Timeout_ms) ->
{chat,
erlang:element(2, Chat),
erlang:element(3, Chat),
erlang:element(4, Chat),
erlang:element(5, Chat),
erlang:element(6, Chat),
erlang:element(7, Chat),
erlang:element(8, Chat),
erlang:element(9, Chat),
erlang:element(10, Chat),
Timeout_ms}.
-file("src/starlet.gleam", 305).
?DOC(" Returns the current timeout in milliseconds.\n").
-spec timeout(chat(any(), any(), any(), any())) -> integer().
timeout(Chat) ->
erlang:element(11, Chat).
-file("src/starlet.gleam", 310).
?DOC(" Enable tools on a chat. Transitions ToolsOff → ToolsOn.\n").
-spec with_tools(
chat(tools_off(), OVK, OVL, OVM),
list(starlet@tool:definition())
) -> chat(tools_on(), OVK, OVL, OVM).
with_tools(Chat, Tool_defs) ->
{chat,
erlang:element(2, Chat),
erlang:element(3, Chat),
erlang:element(4, Chat),
erlang:element(5, Chat),
Tool_defs,
erlang:element(7, Chat),
erlang:element(8, Chat),
erlang:element(9, Chat),
erlang:element(10, Chat),
erlang:element(11, Chat)}.
-file("src/starlet.gleam", 318).
?DOC(" Get the tool definitions from a tools-enabled chat.\n").
-spec tools(chat(tools_on(), any(), any(), any())) -> list(starlet@tool:definition()).
tools(Chat) ->
erlang:element(6, Chat).
-file("src/starlet.gleam", 326).
?DOC(
" Enable JSON output with a schema. Transitions FreeText → JsonFormat.\n"
"\n"
" The model will be constrained to output valid JSON matching the schema.\n"
" Use `json(turn)` to extract the JSON string from the response.\n"
).
-spec with_json_output(chat(OWE, free_text(), OWF, OWG), jscheam@schema:type()) -> chat(OWE, json_format(), OWF, OWG).
with_json_output(Chat, Output_schema) ->
{chat,
erlang:element(2, Chat),
erlang:element(3, Chat),
erlang:element(4, Chat),
erlang:element(5, Chat),
erlang:element(6, Chat),
erlang:element(7, Chat),
erlang:element(8, Chat),
erlang:element(9, Chat),
{some, jscheam@schema:to_json(Output_schema)},
erlang:element(11, Chat)}.
-file("src/starlet.gleam", 334).
?DOC(" Disable JSON output, return to free text. Transitions JsonFormat → FreeText.\n").
-spec with_free_text(chat(OWP, json_format(), OWQ, OWR)) -> chat(OWP, free_text(), OWQ, OWR).
with_free_text(Chat) ->
{chat,
erlang:element(2, Chat),
erlang:element(3, Chat),
erlang:element(4, Chat),
erlang:element(5, Chat),
erlang:element(6, Chat),
erlang:element(7, Chat),
erlang:element(8, Chat),
erlang:element(9, Chat),
none,
erlang:element(11, Chat)}.
-file("src/starlet.gleam", 347).
?DOC(
" Extracts the text content from a turn.\n"
" Only available for free text format turns.\n"
).
-spec text(turn(any(), free_text(), any())) -> binary().
text(Turn) ->
erlang:element(2, Turn).
-file("src/starlet.gleam", 353).
?DOC(
" Extracts the JSON content from a turn.\n"
" Only available for JSON format turns.\n"
).
-spec json(turn(any(), json_format(), any())) -> binary().
json(Turn) ->
erlang:element(2, Turn).
-file("src/starlet.gleam", 358).
?DOC(" Extract tool calls from a turn. Only available when tools are enabled.\n").
-spec tool_calls(turn(tools_on(), any(), any())) -> list(starlet@tool:call()).
tool_calls(Turn) ->
erlang:element(3, Turn).
-file("src/starlet.gleam", 363).
?DOC(" Check if a turn has any tool calls.\n").
-spec has_tool_calls(turn(tools_on(), any(), any())) -> boolean().
has_tool_calls(Turn) ->
not gleam@list:is_empty(erlang:element(3, Turn)).
-file("src/starlet.gleam", 368).
?DOC(false).
-spec make_turn_for_testing(binary()) -> turn(tools_off(), free_text(), no_ext()).
make_turn_for_testing(Content) ->
{turn, Content, [], no_ext}.
-file("src/starlet.gleam", 383).
?DOC(
" Sends the chat to the LLM and returns the response.\n"
"\n"
" Returns a tuple of the updated chat (with the assistant's response appended\n"
" to the history) and the turn containing the response text.\n"
"\n"
" ```gleam\n"
" case starlet.send(chat) {\n"
" Ok(#(new_chat, turn)) -> starlet.text(turn)\n"
" Error(err) -> // handle error\n"
" }\n"
" ```\n"
).
-spec send(chat(OXY, OXZ, ready(), OYA)) -> {ok,
{chat(OXY, OXZ, ready(), OYA), turn(OXY, OXZ, OYA)}} |
{error, starlet_error()}.
send(Chat) ->
{chat,
Client,
Model,
System_prompt,
Messages,
Tools,
Temperature,
Max_tokens,
Ext,
Json_schema,
Timeout_ms} = Chat,
{client, P, _} = Client,
Request = {request,
Model,
System_prompt,
Messages,
Tools,
Temperature,
Max_tokens,
Json_schema,
Timeout_ms},
case (erlang:element(4, P))(Request, Ext) of
{ok, {Response, New_ext}} ->
New_messages = lists:append(
Messages,
[{assistant_message,
erlang:element(2, Response),
erlang:element(3, Response)}]
),
New_chat = {chat,
erlang:element(2, Chat),
erlang:element(3, Chat),
erlang:element(4, Chat),
New_messages,
erlang:element(6, Chat),
erlang:element(7, Chat),
erlang:element(8, Chat),
New_ext,
erlang:element(10, Chat),
erlang:element(11, Chat)},
Turn = {turn,
erlang:element(2, Response),
erlang:element(3, Response),
New_ext},
{ok, {New_chat, Turn}};
{error, Err} ->
{error, Err}
end.
-file("src/starlet.gleam", 432).
?DOC(
" Apply pre-computed tool results to the chat.\n"
" Use when you've already run the tools yourself.\n"
).
-spec with_tool_results(
chat(tools_on(), OYO, ready(), OYP),
list(starlet@tool:tool_result())
) -> chat(tools_on(), OYO, ready(), OYP).
with_tool_results(Chat, Results) ->
Result_messages = gleam@list:map(
Results,
fun(R) ->
{tool_result_message,
erlang:element(2, R),
erlang:element(3, R),
gleam@json:to_string(erlang:element(4, R))}
end
),
{chat,
erlang:element(2, Chat),
erlang:element(3, Chat),
erlang:element(4, Chat),
lists:append(erlang:element(5, Chat), Result_messages),
erlang:element(6, Chat),
erlang:element(7, Chat),
erlang:element(8, Chat),
erlang:element(9, Chat),
erlang:element(10, Chat),
erlang:element(11, Chat)}.
-file("src/starlet.gleam", 460).
-spec run_all_tools(
list(starlet@tool:call()),
fun((starlet@tool:call()) -> {ok, starlet@tool:tool_result()} |
{error, starlet@tool:tool_error()}),
list(starlet@tool:tool_result())
) -> {ok, list(starlet@tool:tool_result())} | {error, starlet@tool:tool_error()}.
run_all_tools(Calls, Run, Acc) ->
case Calls of
[] ->
{ok, lists:reverse(Acc)};
[Call | Rest] ->
case Run(Call) of
{ok, Result} ->
run_all_tools(Rest, Run, [Result | Acc]);
{error, E} ->
{error, E}
end
end.
-file("src/starlet.gleam", 449).
?DOC(
" Run tools and apply their results in one step.\n"
" The runner is called for each tool call; errors short-circuit.\n"
).
-spec apply_tool_results(
chat(tools_on(), OYZ, ready(), OZA),
list(starlet@tool:call()),
fun((starlet@tool:call()) -> {ok, starlet@tool:tool_result()} |
{error, starlet@tool:tool_error()})
) -> {ok, chat(tools_on(), OYZ, ready(), OZA)} | {error, starlet_error()}.
apply_tool_results(Chat, Calls, Run) ->
case run_all_tools(Calls, Run, []) of
{ok, Results} ->
{ok, with_tool_results(Chat, Results)};
{error, E} ->
{error, {tool, E}}
end.
-file("src/starlet.gleam", 478).
?DOC(
" Send a tools-enabled chat and categorize the response.\n"
" Returns either Done (no tool calls) or ToolCall (tools requested).\n"
).
-spec step(chat(tools_on(), OZV, ready(), OZW)) -> {ok, step(OZV, OZW)} |
{error, starlet_error()}.
step(Chat) ->
case send(Chat) of
{ok, {New_chat, Turn}} ->
case has_tool_calls(Turn) of
true ->
{ok, {tool_call, New_chat, Turn, erlang:element(3, Turn)}};
false ->
{ok, {done, New_chat, Turn}}
end;
{error, E} ->
{error, E}
end.
-file("src/starlet.gleam", 216).
?DOC(
" Creates a new chat with the given client and model name.\n"
"\n"
" The chat inherits the provider's extension type from the client,\n"
" allowing provider-specific features to be configured.\n"
"\n"
" ```gleam\n"
" let chat = starlet.chat(client, \"qwen3:0.6b\")\n"
" ```\n"
).
-spec chat(client(OSE), binary()) -> chat(tools_off(), free_text(), empty(), OSE).
chat(Client, Model) ->
{client, _, Default_ext} = Client,
{chat, Client, Model, none, [], [], none, none, Default_ext, none, 60000}.