-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}.