Packages

Gleam SDK for the Anthropic Claude API with agentic tool-use loop and BEAM concurrency

Current section

Files

Jump to
claude_gleam src claude@messages.erl
Raw

src/claude@messages.erl

-module(claude@messages).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/claude/messages.gleam").
-export([new_params/2, build_request/1, create/1, create_with_retry/2, create_simple/2, build_stream_request/1, create_stream/1]).
-export_type([request_params/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(
" Functions for calling the Anthropic Messages API.\n"
"\n"
" ## Streaming limitation\n"
"\n"
" The streaming functions (`create_stream`, `build_stream_request`) rely on\n"
" `gleam_httpc`, which buffers the **entire** HTTP response before returning.\n"
" This means SSE events are parsed from the complete buffered body rather than\n"
" received incrementally as the API generates tokens. For true incremental\n"
" streaming, a different HTTP client with chunked transfer-encoding support\n"
" would be required.\n"
"\n"
" The streaming functions are still useful for obtaining the granular SSE\n"
" event structure (content block deltas, token-level events, etc.) even\n"
" though all events arrive at once after the response completes.\n"
).
-type request_params() :: {request_params,
claude@client:config(),
gleam@option:option(binary()),
gleam@option:option(integer()),
list(claude@types@message:message_param()),
claude@types@tool:registry(),
gleam@option:option(binary()),
gleam@option:option(claude@types@tool:tool_choice()),
gleam@option:option(integer())}.
-file("src/claude/messages.gleam", 54).
?DOC(
" Create a new `RequestParams` with sensible defaults.\n"
"\n"
" Only `config` and `messages` are required. The rest default to `None`\n"
" (or an empty tool registry), which causes the API call to use the\n"
" client-level defaults.\n"
).
-spec new_params(
claude@client:config(),
list(claude@types@message:message_param())
) -> request_params().
new_params(Config, Messages) ->
{request_params,
Config,
none,
none,
Messages,
claude@types@tool:registry(),
none,
none,
none}.
-file("src/claude/messages.gleam", 74).
?DOC(
" Build an HTTP request for the Messages API without sending it.\n"
"\n"
" This is useful for testing and inspection. The request can later be sent\n"
" with `gleam/httpc.send`.\n"
).
-spec build_request(request_params()) -> {ok,
gleam@http@request:request(binary())} |
{error, claude@types@error:api_error()}.
build_request(Params) ->
Actual_model = gleam@option:unwrap(
erlang:element(3, Params),
erlang:element(4, erlang:element(2, Params))
),
Actual_max_tokens = gleam@option:unwrap(
erlang:element(4, Params),
erlang:element(5, erlang:element(2, Params))
),
Body = begin
_pipe = claude@json@encode:create_params(
Actual_model,
Actual_max_tokens,
erlang:element(5, Params),
erlang:element(6, Params),
erlang:element(7, Params),
erlang:element(8, Params),
erlang:element(9, Params),
false
),
gleam@json:to_string(_pipe)
end,
Url = <<(erlang:element(3, erlang:element(2, Params)))/binary,
"/v1/messages"/utf8>>,
_pipe@1 = gleam@http@request:to(Url),
_pipe@2 = gleam@result:map_error(
_pipe@1,
fun(_) ->
{bad_request_error, <<"Invalid base URL: "/utf8, Url/binary>>}
end
),
gleam@result:map(_pipe@2, fun(Req) -> _pipe@3 = Req,
_pipe@4 = gleam@http@request:set_method(_pipe@3, post),
_pipe@5 = gleam@http@request:set_header(
_pipe@4,
<<"content-type"/utf8>>,
<<"application/json"/utf8>>
),
_pipe@6 = gleam@http@request:set_header(
_pipe@5,
<<"x-api-key"/utf8>>,
erlang:element(2, erlang:element(2, Params))
),
_pipe@7 = gleam@http@request:set_header(
_pipe@6,
<<"anthropic-version"/utf8>>,
<<"2023-06-01"/utf8>>
),
gleam@http@request:set_body(_pipe@7, Body) end).
-file("src/claude/messages.gleam", 115).
?DOC(
" Send a Messages API request and return the decoded response.\n"
"\n"
" On success (HTTP 200), decodes the response body into a Message.\n"
" On error status codes, maps to the appropriate ApiError.\n"
" On connection failure, returns a ConnectionError.\n"
).
-spec create(request_params()) -> {ok, claude@types@message:message()} |
{error, claude@types@error:api_error()}.
create(Params) ->
gleam@result:'try'(
build_request(Params),
fun(Req) -> case gleam@httpc:send(Req) of
{ok, Resp} ->
case erlang:element(2, Resp) of
200 ->
case claude@json@decode:message(
erlang:element(4, Resp)
) of
{ok, Msg} ->
{ok, Msg};
{error, _} ->
{error,
{bad_request_error,
<<"Failed to decode response: "/utf8,
(erlang:element(4, Resp))/binary>>}}
end;
Status ->
{error,
claude@types@error:from_status(
Status,
erlang:element(4, Resp)
)}
end;
{error, _} ->
{error,
{connection_error, <<"Failed to connect to API"/utf8>>}}
end end
).
-file("src/claude/messages.gleam", 150).
-spec retry_loop(request_params(), integer(), integer(), integer()) -> {ok,
claude@types@message:message()} |
{error, claude@types@error:api_error()}.
retry_loop(Params, Max_retries, Attempt, Backoff_ms) ->
case create(Params) of
{ok, Msg} ->
{ok, Msg};
{error, Err} ->
case Attempt >= Max_retries of
true ->
{error, Err};
false ->
case Err of
{rate_limit_error, _, {some, Ms}} ->
gleam_erlang_ffi:sleep(Ms),
retry_loop(
Params,
Max_retries,
Attempt + 1,
Backoff_ms * 2
);
{rate_limit_error, _, _} ->
gleam_erlang_ffi:sleep(Backoff_ms),
retry_loop(
Params,
Max_retries,
Attempt + 1,
Backoff_ms * 2
);
{server_error, _, _} ->
gleam_erlang_ffi:sleep(Backoff_ms),
retry_loop(
Params,
Max_retries,
Attempt + 1,
Backoff_ms * 2
);
_ ->
{error, Err}
end
end
end.
-file("src/claude/messages.gleam", 143).
?DOC(
" Send a Messages API request with automatic retry for transient errors.\n"
"\n"
" Retries on RateLimitError and ServerError up to `max_retries` times.\n"
" Uses exponential backoff starting at 1 second, doubling each retry.\n"
" If a RateLimitError includes a retry_after value, that is used instead.\n"
).
-spec create_with_retry(request_params(), integer()) -> {ok,
claude@types@message:message()} |
{error, claude@types@error:api_error()}.
create_with_retry(Params, Max_retries) ->
retry_loop(Params, Max_retries, 0, 1000).
-file("src/claude/messages.gleam", 184).
?DOC(" Convenience: send a simple user message with config defaults.\n").
-spec create_simple(claude@client:config(), binary()) -> {ok,
claude@types@message:message()} |
{error, claude@types@error:api_error()}.
create_simple(Config, Message) ->
create(
{request_params,
Config,
none,
none,
[claude@types@message:new_user(Message)],
claude@types@tool:registry(),
none,
none,
none}
).
-file("src/claude/messages.gleam", 212).
?DOC(
" Build an HTTP request for the streaming Messages API without sending it.\n"
"\n"
" Identical to `build_request` but includes `\"stream\": true` in the body.\n"
"\n"
" **Important limitation:** This builds a request intended for streaming, but\n"
" when sent via `gleam_httpc` (as `create_stream` does), the entire response\n"
" will be buffered before returning. The request itself is correct -- the API\n"
" will respond with SSE-formatted data -- but `gleam_httpc` does not support\n"
" reading the response incrementally. For true incremental streaming, a\n"
" different HTTP client with chunked transfer-encoding support would be needed.\n"
).
-spec build_stream_request(request_params()) -> {ok,
gleam@http@request:request(binary())} |
{error, claude@types@error:api_error()}.
build_stream_request(Params) ->
Actual_model = gleam@option:unwrap(
erlang:element(3, Params),
erlang:element(4, erlang:element(2, Params))
),
Actual_max_tokens = gleam@option:unwrap(
erlang:element(4, Params),
erlang:element(5, erlang:element(2, Params))
),
Body = begin
_pipe = claude@json@encode:create_params(
Actual_model,
Actual_max_tokens,
erlang:element(5, Params),
erlang:element(6, Params),
erlang:element(7, Params),
erlang:element(8, Params),
erlang:element(9, Params),
true
),
gleam@json:to_string(_pipe)
end,
Url = <<(erlang:element(3, erlang:element(2, Params)))/binary,
"/v1/messages"/utf8>>,
_pipe@1 = gleam@http@request:to(Url),
_pipe@2 = gleam@result:map_error(
_pipe@1,
fun(_) ->
{bad_request_error, <<"Invalid base URL: "/utf8, Url/binary>>}
end
),
gleam@result:map(_pipe@2, fun(Req) -> _pipe@3 = Req,
_pipe@4 = gleam@http@request:set_method(_pipe@3, post),
_pipe@5 = gleam@http@request:set_header(
_pipe@4,
<<"content-type"/utf8>>,
<<"application/json"/utf8>>
),
_pipe@6 = gleam@http@request:set_header(
_pipe@5,
<<"x-api-key"/utf8>>,
erlang:element(2, erlang:element(2, Params))
),
_pipe@7 = gleam@http@request:set_header(
_pipe@6,
<<"anthropic-version"/utf8>>,
<<"2023-06-01"/utf8>>
),
gleam@http@request:set_body(_pipe@7, Body) end).
-file("src/claude/messages.gleam", 263).
?DOC(
" Send a streaming Messages API request and return parsed SSE events.\n"
"\n"
" **Important limitation:** This function uses `gleam_httpc` which buffers\n"
" the complete HTTP response before returning. The SSE events are parsed\n"
" from the buffered response body, so events are NOT received incrementally\n"
" as they are generated by the API. For true streaming, a different HTTP\n"
" client with chunked transfer-encoding support would be needed.\n"
"\n"
" This function is still useful for obtaining the granular SSE event\n"
" structure (e.g., content block deltas, token-level events) even though\n"
" the events arrive all at once after the full response is received.\n"
"\n"
" On success (HTTP 200), parses the body as SSE and returns events.\n"
" On error status codes, maps to the appropriate ApiError.\n"
" On connection failure, returns a ConnectionError.\n"
).
-spec create_stream(request_params()) -> {ok,
list(claude@streaming:stream_event())} |
{error, claude@types@error:api_error()}.
create_stream(Params) ->
gleam@result:'try'(
build_stream_request(Params),
fun(Req) -> case gleam@httpc:send(Req) of
{ok, Resp} ->
case erlang:element(2, Resp) of
200 ->
{ok,
claude@streaming:parse_sse(
erlang:element(4, Resp)
)};
Status ->
{error,
claude@types@error:from_status(
Status,
erlang:element(4, Resp)
)}
end;
{error, _} ->
{error,
{connection_error, <<"Failed to connect to API"/utf8>>}}
end end
).