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