Packages

A well-typed, idiomatic Gleam client for Anthropic's Claude API with streaming support and tool use

Current section

Files

Jump to
anthropic_gleam src anthropic@api.erl
Raw

src/anthropic@api.erl

-module(anthropic@api).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/anthropic/api.gleam").
-export([chat/2, create_message/2, event_text/1, stream_text/1, stream_complete/1, stream_has_error/1, stream_message_id/1, stream_model/1, chat_stream/2, chat_stream_with_callback/3]).
-export_type([stream_result/0, stream_error/0]).
-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(
" API functions for Anthropic Messages API\n"
"\n"
" This module provides the core functions for interacting with Claude's\n"
" Messages API, including message creation and response parsing.\n"
"\n"
" ## Quick Start\n"
"\n"
" For most use cases, use the unified chat functions:\n"
"\n"
" ```gleam\n"
" import anthropic/api\n"
" import anthropic/client\n"
" import anthropic/types/request\n"
" import anthropic/types/message.{user_message}\n"
"\n"
" // Initialize client\n"
" let assert Ok(client) = client.init()\n"
"\n"
" // Create a request\n"
" let req = request.new(\n"
" \"claude-sonnet-4-20250514\",\n"
" [user_message(\"Hello, Claude!\")],\n"
" 1024,\n"
" )\n"
"\n"
" // Non-streaming chat\n"
" case api.chat(client, req) {\n"
" Ok(response) -> io.println(request.response_text(response))\n"
" Error(err) -> io.println(error.error_to_string(err))\n"
" }\n"
"\n"
" // Streaming chat (batch mode - collects all events)\n"
" case api.chat_stream(client, req) {\n"
" Ok(result) -> io.println(api.stream_text(result))\n"
" Error(err) -> io.println(error.error_to_string(err))\n"
" }\n"
" ```\n"
).
-type stream_result() :: {stream_result,
list(anthropic@streaming:stream_event())}.
-type stream_error() :: {http_error, anthropic@error:anthropic_error()} |
{sse_parse_error, binary()} |
{event_decode_error, binary()} |
{api_error, integer(), binary()}.
-file("src/anthropic/api.gleam", 86).
?DOC(" Parse a response body into CreateMessageResponse\n").
-spec parse_response(binary()) -> {ok,
anthropic@request:create_message_response()} |
{error, anthropic@error:anthropic_error()}.
parse_response(Body) ->
anthropic@internal@decoder:parse_response_body(Body).
-file("src/anthropic/api.gleam", 119).
?DOC(
" Send a chat message and receive a response (non-streaming)\n"
"\n"
" This is the primary function for interacting with Claude. It sends a message\n"
" and returns the complete response.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import anthropic/api\n"
" import anthropic/client\n"
" import anthropic/types/request\n"
" import anthropic/types/message.{user_message}\n"
"\n"
" let assert Ok(client) = client.init()\n"
" let req = request.new(\n"
" \"claude-sonnet-4-20250514\",\n"
" [user_message(\"What is the capital of France?\")],\n"
" 1024,\n"
" )\n"
"\n"
" case api.chat(client, req) {\n"
" Ok(response) -> io.println(request.response_text(response))\n"
" Error(err) -> handle_error(err)\n"
" }\n"
" ```\n"
).
-spec chat(
anthropic@client:client(),
anthropic@request:create_message_request()
) -> {ok, anthropic@request:create_message_response()} |
{error, anthropic@error:anthropic_error()}.
chat(Client, Message_request) ->
gleam@result:'try'(
anthropic@internal@validation:validate_request_or_error(Message_request),
fun(_) ->
Body = anthropic@request:request_to_json_string(Message_request),
gleam@result:'try'(
anthropic@client:post_and_handle(
Client,
<<"/v1/messages"/utf8>>,
Body
),
fun(Response_body) -> parse_response(Response_body) end
)
end
).
-file("src/anthropic/api.gleam", 74).
?DOC(
" Create a message using the Anthropic Messages API\n"
"\n"
" @deprecated Use `api.chat` instead for a more intuitive API\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" // Prefer using api.chat instead:\n"
" case api.chat(client, request) {\n"
" Ok(response) -> io.println(response_text(response))\n"
" Error(err) -> io.println(error_to_string(err))\n"
" }\n"
" ```\n"
).
-spec create_message(
anthropic@client:client(),
anthropic@request:create_message_request()
) -> {ok, anthropic@request:create_message_response()} |
{error, anthropic@error:anthropic_error()}.
create_message(Client, Message_request) ->
chat(Client, Message_request).
-file("src/anthropic/api.gleam", 283).
?DOC(
" Extract text from a single streaming event\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" list.each(result.events, fn(event) {\n"
" case api.event_text(event) {\n"
" Ok(text) -> io.print(text)\n"
" Error(_) -> Nil\n"
" }\n"
" })\n"
" ```\n"
).
-spec event_text(anthropic@streaming:stream_event()) -> {ok, binary()} |
{error, nil}.
event_text(Event) ->
case Event of
{content_block_delta_event_variant, Delta_event} ->
case erlang:element(3, Delta_event) of
{text_content_delta, Text_delta} ->
{ok, erlang:element(2, Text_delta)};
_ ->
{error, nil}
end;
_ ->
{error, nil}
end.
-file("src/anthropic/api.gleam", 265).
?DOC(
" Get the full text from a streaming result\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" case api.chat_stream(client, request) {\n"
" Ok(result) -> io.println(api.stream_text(result))\n"
" Error(_) -> io.println(\"Error\")\n"
" }\n"
" ```\n"
).
-spec stream_text(stream_result()) -> binary().
stream_text(Result) ->
_pipe = erlang:element(2, Result),
_pipe@1 = gleam@list:filter_map(_pipe, fun event_text/1),
gleam@string:join(_pipe@1, <<""/utf8>>).
-file("src/anthropic/api.gleam", 296).
?DOC(" Check if a streaming result completed successfully\n").
-spec stream_complete(stream_result()) -> boolean().
stream_complete(Result) ->
gleam@list:any(erlang:element(2, Result), fun(Event) -> case Event of
message_stop_event ->
true;
_ ->
false
end end).
-file("src/anthropic/api.gleam", 306).
?DOC(" Check if a streaming result contains an error\n").
-spec stream_has_error(stream_result()) -> boolean().
stream_has_error(Result) ->
gleam@list:any(erlang:element(2, Result), fun(Event) -> case Event of
{error_event, _} ->
true;
_ ->
false
end end).
-file("src/anthropic/api.gleam", 316).
?DOC(" Get the message ID from a streaming result\n").
-spec stream_message_id(stream_result()) -> {ok, binary()} | {error, nil}.
stream_message_id(Result) ->
_pipe = erlang:element(2, Result),
gleam@list:find_map(_pipe, fun(Event) -> case Event of
{message_start_event, Msg} ->
{ok, erlang:element(2, Msg)};
_ ->
{error, nil}
end end).
-file("src/anthropic/api.gleam", 327).
?DOC(" Get the model from a streaming result\n").
-spec stream_model(stream_result()) -> {ok, binary()} | {error, nil}.
stream_model(Result) ->
_pipe = erlang:element(2, Result),
gleam@list:find_map(_pipe, fun(Event) -> case Event of
{message_start_event, Msg} ->
{ok, erlang:element(5, Msg)};
_ ->
{error, nil}
end end).
-file("src/anthropic/api.gleam", 342).
?DOC(" Parse SSE body text into streaming events\n").
-spec parse_sse_body(binary()) -> {ok, stream_result()} |
{error, stream_error()}.
parse_sse_body(Body) ->
State = anthropic@internal@sse:new_parser_state(),
Parse_result = anthropic@internal@sse:parse_chunk(State, Body),
Events = begin
_pipe = erlang:element(2, Parse_result),
gleam@list:filter_map(
_pipe,
fun(Sse_event) ->
case anthropic@streaming@decoder:decode_event(Sse_event) of
{ok, Event} ->
{ok, Event};
{error, _} ->
{error, nil}
end
end
)
end,
Remaining_events = case anthropic@internal@sse:flush(
erlang:element(3, Parse_result)
) of
{ok, Sse_event@1} ->
case anthropic@streaming@decoder:decode_event(Sse_event@1) of
{ok, Event@1} ->
[Event@1];
{error, _} ->
[]
end;
{error, _} ->
[]
end,
{ok, {stream_result, lists:append(Events, Remaining_events)}}.
-file("src/anthropic/api.gleam", 422).
?DOC(" Convert ConnectError to string\n").
-spec connect_error_to_string(gleam@httpc:connect_error()) -> binary().
connect_error_to_string(Err) ->
case Err of
{posix, Code} ->
<<"POSIX error: "/utf8, Code/binary>>;
{tls_alert, Code@1, Detail} ->
<<<<<<"TLS alert "/utf8, Code@1/binary>>/binary, ": "/utf8>>/binary,
Detail/binary>>
end.
-file("src/anthropic/api.gleam", 406).
?DOC(" Convert httpc error to AnthropicError\n").
-spec http_error_to_anthropic_error(gleam@httpc:http_error()) -> anthropic@error:anthropic_error().
http_error_to_anthropic_error(Err) ->
case Err of
invalid_utf8_response ->
anthropic@error:http_error(<<"Invalid UTF-8 in response"/utf8>>);
{failed_to_connect, Ip4, Ip6} ->
anthropic@error:network_error(
<<<<<<<<"Failed to connect to server (IPv4: "/utf8,
(connect_error_to_string(Ip4))/binary>>/binary,
", IPv6: "/utf8>>/binary,
(connect_error_to_string(Ip6))/binary>>/binary,
")"/utf8>>
);
response_timeout ->
anthropic@error:timeout_error(0)
end.
-file("src/anthropic/api.gleam", 370).
?DOC(" Make a streaming HTTP request using gleam_httpc\n").
-spec make_streaming_request(anthropic@client:client(), binary()) -> {ok,
gleam@http@response:response(binary())} |
{error, anthropic@error:anthropic_error()}.
make_streaming_request(Api_client, Body) ->
Base_url = erlang:element(3, erlang:element(2, Api_client)),
Full_url = <<Base_url/binary, "/v1/messages"/utf8>>,
gleam@result:'try'(
begin
_pipe = gleam@http@request:to(Full_url),
gleam@result:map_error(
_pipe,
fun(_) ->
anthropic@error:config_error(
<<"Invalid URL: "/utf8, Full_url/binary>>
)
end
)
end,
fun(Req) ->
Req@1 = begin
_pipe@1 = Req,
_pipe@2 = gleam@http@request:set_method(_pipe@1, post),
_pipe@3 = gleam@http@request:set_header(
_pipe@2,
<<"content-type"/utf8>>,
<<"application/json"/utf8>>
),
_pipe@4 = gleam@http@request:set_header(
_pipe@3,
<<"x-api-key"/utf8>>,
anthropic@config:api_key_to_string(
erlang:element(2, erlang:element(2, Api_client))
)
),
_pipe@5 = gleam@http@request:set_header(
_pipe@4,
<<"anthropic-version"/utf8>>,
<<"2023-06-01"/utf8>>
),
_pipe@6 = gleam@http@request:set_header(
_pipe@5,
<<"accept"/utf8>>,
<<"text/event-stream"/utf8>>
),
gleam@http@request:set_body(_pipe@6, Body)
end,
_pipe@7 = gleam@httpc:configure(),
_pipe@8 = gleam@httpc:timeout(
_pipe@7,
erlang:element(5, erlang:element(2, Api_client))
),
_pipe@9 = gleam@httpc:dispatch(_pipe@8, Req@1),
gleam@result:map_error(
_pipe@9,
fun(Err) -> http_error_to_anthropic_error(Err) end
)
end
).
-file("src/anthropic/api.gleam", 190).
?DOC(
" Send a chat message with streaming response (batch mode)\n"
"\n"
" This function sends a message and returns all streaming events after\n"
" the response completes. For true real-time streaming, use the sans-io\n"
" functions in `anthropic/streaming/handler`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import anthropic/api\n"
" import anthropic/client\n"
" import anthropic/types/request\n"
" import anthropic/types/message.{user_message}\n"
"\n"
" let assert Ok(client) = client.init()\n"
" let req = request.new(\n"
" \"claude-sonnet-4-20250514\",\n"
" [user_message(\"Tell me a story\")],\n"
" 2048,\n"
" )\n"
"\n"
" case api.chat_stream(client, req) {\n"
" Ok(result) -> io.println(api.stream_text(result))\n"
" Error(err) -> handle_stream_error(err)\n"
" }\n"
" ```\n"
).
-spec chat_stream(
anthropic@client:client(),
anthropic@request:create_message_request()
) -> {ok, stream_result()} | {error, stream_error()}.
chat_stream(Client, Message_request) ->
Streaming_request = anthropic@request:with_stream(Message_request, true),
case anthropic@internal@validation:validate_request_or_error(
Streaming_request
) of
{error, Err} ->
{error, {http_error, Err}};
{ok, _} ->
Body = anthropic@request:request_to_json_string(Streaming_request),
case make_streaming_request(Client, Body) of
{error, Err@1} ->
{error, {http_error, Err@1}};
{ok, Http_response} ->
case erlang:element(2, Http_response) of
200 ->
parse_sse_body(erlang:element(4, Http_response));
Status ->
{error,
{api_error,
Status,
erlang:element(4, Http_response)}}
end
end
end.
-file("src/anthropic/api.gleam", 238).
?DOC(
" Send a streaming chat message with a callback for each event\n"
"\n"
" **Note**: Despite the callback, this function collects ALL events before\n"
" calling the callbacks. It does NOT provide true real-time streaming.\n"
" For real-time streaming, use the sans-io functions in `anthropic/streaming/handler`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" api.chat_stream_with_callback(client, request, fn(event) {\n"
" case api.event_text(event) {\n"
" Ok(text) -> io.print(text)\n"
" Error(_) -> Nil\n"
" }\n"
" })\n"
" ```\n"
).
-spec chat_stream_with_callback(
anthropic@client:client(),
anthropic@request:create_message_request(),
fun((anthropic@streaming:stream_event()) -> nil)
) -> {ok, stream_result()} | {error, stream_error()}.
chat_stream_with_callback(Client, Message_request, Callback) ->
gleam@result:'try'(
chat_stream(Client, Message_request),
fun(Stream_result) ->
gleam@list:each(erlang:element(2, Stream_result), Callback),
{ok, Stream_result}
end
).