Changelog
View SourceAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.7.0 - 2026-10-05
Upgrading from 0.6
Everything here can break a 0.6 caller; the details are in the sections below.
- Dependency floors:
:telemetry ~> 1.3andreq ~> 0.6 and >= 0.6.1. Runmix deps.update telemetry req. :poisonis no longer pulled in by Claudio. If your app used it only through Claudio, add it to your own deps.Claudio.Client.new/2validates its config: a missing/empty:tokenor an unknown key raisesArgumentError. Fix the config;version: nilnow means the default version.- Invalid
retry/timeout/recv_timeoutraiseArgumentErrorwhen the client is built (they were ignored or failed on first request). Usetrue/false/a keyword list forretry, and a non-negative integer (ms) or:infinityfor timeouts. - Retries now actually happen (408, 429, 5xx, 529, connection errors, every method; streams excepted). If you had
retry: true(or a keyword list) configured, it used to have no effect and now retries; setretry: falseto keep the old no-retry behaviour. Apps with noretry:keep Req's default (GET/HEAD only).Retry-Afteris honoured on 429/503/529, also overdelay:. [:claudio, :messages, :create, :stop]token keys are absent (not0) when a 200 carries nousage. Read them withMap.get/3. Theerrormetadata key is deprecated; useerror_type.APIError.typeis an atom forbilling_error,request_too_largeandtimeout_error(was a string), and a body withouterror.typeis typed from the HTTP status. Match the atoms.- Builder functions raise
ArgumentErroron common mistakes (add_message(:system, ...), string roles,nilcontent, out-of-range sampling values, stringmax_tokens/tool_choice, keyword tools/thinking configs);set_max_tokens/2also rejects non-positive integers. Fix the call as the message says. - Option validation: the cache helpers,
add_message_with_document/5,search_result_block/4,add_computer_tool/4,Files.upload/3,Batches.list/2,Batches.wait_for_completion/3andClaudio.Agent.run/4raise on unknown options (previously ignored;wait_for_completion/3also on bad:poll_interval/:timeout/:callbackvalues), and cache helpers on attlother than"5m"/"1h". Remove the stray option. - A streaming
Req.Responsecarriesprivate.claudio. Code matchingprivate: %{}exactly or comparing whole responses must relax the match. Claudio.Agent.run/4errors are 4-tuples:{:error, reason, last_response_or_nil, messages}(was{:error, reason}). Match the 4-tuple.Batches.get_results/2returns string-keyed maps (was atom keys). Read"custom_id", not:custom_id.Responseshapes changed:stop_reason: :compactionis an atom; server-result blocks andcompactionare typed maps (readrawfor the old map);usagehas new atom keys (nilwhen unsent); parsedtool_use/server_tool_useblocks gaincaller/toolset_name. Update exact-map matches.- MCP:
ServerConfigloses:tool_configuration(use:default_config/:configs);allow_tools/2takes exact names (patterns raise);add_mcp_server/2raises on a duplicate server name. Claudio.Skillssends no beta header andlist/2has no"has_more"; page withnext_page, or callClaudio.Client.with_betas(client, ["skills-2025-10-02"]).- Stream parse errors carry
%Jason.DecodeError{}(was%Poison.ParseError{}), andbuild_final_message/1returns an error for a truncated stream.
Added
ServerConfig.to_toolset/1,set_default_config/2,configure_tool/3,split_raw/1.Files.list/2GA pagination options:pageand:ids.add_code_execution_tool/2:versionoption.Response.stop_details(also accumulated byClaudio.Messages.Stream.build_final_message/1).Thinking & effort helpers (
Claudio.Messages.Request), no per-model validation:enable_adaptive_thinking/2(display::summarized/:omitted/:updates;:updatesdeclaresthinking-display-updates-2026-08-18) anddisable_thinking/1.set_effort/2(:low…:max, GA) andset_task_budget/3(output_config.task_budget, declarestask-budgets-2026-03-13) — both merge intooutput_config.
Response.get_thinking/1,Response.thinking_interrupted?/1,Claudio.Messages.Stream.accumulate_thinking/1.Telemetry:
:thinking_tokensin[:claudio, :messages, :create, :stop]and[:claudio, :messages, :stream, :usage]metadata, when the API reports it.5.x request surface (
Claudio.Messages.Request), no per-model or placement validation:add_system_message/3— mid-conversationrole: "system"messages (GA);clear_at:declaresmid-conversation-system-clear-at-2026-08-21,effort:(per-message effort) declaresmid-conversation-output-config-2026-07-01.set_speed/2(:fast/:standard, always declaresfast-mode-2026-02-01),set_inference_geo/2(:global/:us, GA),enable_cache_diagnostics/2(GA).
Response.diagnostics(raw cache-diagnostics map).Refusal fallbacks:
Request.set_fallbacks/2(:defaultor up to three models / override maps; declaresserver-side-fallback-2026-07-01); typed:fallbackcontent blocks (original kept underraw:and replayed verbatim);Response.fallbacks/1,Response.served_by/1;usage.iterations.Context management (
Claudio.Messages.Request), no local limits (the API's 400 is authoritative):add_clear_tool_uses/2,add_clear_thinking/2(always placed first) — declarecontext-management-2025-06-27;add_compaction/2(threshold,compact_20260112) — declarescompact-2026-01-12.request_compaction/2— on-demandcompaction: {"type": "summarize"}, declarescompact-2026-09-04;apply_compaction/2continues from a compaction summary (either kind) by replacing the history with the block onward.add_message/3declares the replay beta for acompactionblock (signed →compact-2026-09-04, unsigned →compact-2026-01-12).
Typed
:compactioncontent blocks (original underraw:, replayed verbatim),Response.compaction_block/1,Response.context_management(rawapplied_edits; also read from the streamedmessage_delta).Tool extensions (
Claudio.Messages.Request):add_tool/3(defer_loading:,allowed_callers:—:direct/:code_execution→"code_execution_20260120");add_tool_search_tool/2(:regex/:bm25, GA);add_advisor_tool/3(declaresadvisor-tool-2026-03-01;add_message/3declares it for replayed advisor blocks);add_computer_toolset/2/add_browser_toolset/2(computer_toolset_20260801/browser_toolset_20260801, GA);add_computer_tool/4version: :"20251124".Shallowly typed server-result blocks (
web_fetch_tool_result,code_execution_tool_result,bash_code_execution_tool_result,text_editor_code_execution_tool_result,tool_search_tool_result,advisor_tool_result,container_upload;raw:replayed verbatim),Response.get_server_tool_results/1,2,Response.container(also from the stream).Tools.create_tool_result/4(toolset_name:);Tools.halt_result/1,Tools.halt_text/1.Thinking block binding:
enable_adaptive_thinking/2block_binding:andRequest.set_thinking_block_binding/2(:error/:drop_block; declarethinking-binding-controls-2026-08-01);Response.input_transformations(raw list,nilwithout the beta; replaced by a streamedmessage_deltacopy after a fallback).createand linked stream:startcarryoutput_typeandstop_sequenceswhen set (OTelgen_ai.output.type,gen_ai.request.stop_sequences).Claudio.Messages.Stream.to_response/2— consume a stream in one pass (on_text:/on_event:callbacks) and get a%Response{}; an SSEerrorevent becomes anAPIError.add_web_search_tool/2/add_web_fetch_tool/2:response_inclusion:(needsversion: :"20260318") andallowed_callers:.Request.disable_thinking/2mode: :between_tools— Claude Sonnet 5.5's way to turn off up-front thinking (thinking: {"type": "between_tools"}).disable_thinking/1still sendsdisabled. Theset_tool_choice/2docs now list Sonnet 5.5 among models that 400 on forced tool use.Files.upload/3acceptsexpires_in_seconds:(sent as a multipart form field; the API documents 3600..7776000).upload/3now also rejects unknown options.A2A HTTP transport
:headersoption: extra request headers ([{name, value}], e.g. W3Ctraceparent/tracestatefor cross-service tracing) are sent on every request. They cannot replace the transport's owncontent-typeorauthorization(same name in any case is dropped).Telemetry for OpenTelemetry/GenAI dashboards (see
guides/telemetry.md):[:claudio, :messages, :create]gains request params (max_tokens,temperature,top_p,top_k,effort),server_address, response fields (response_id,response_model— the fallback model when one served —stop_reason,request_id), a boundederror_type(nevernilon an error stop;:unknownwhen unclassified) andstatus_code, and token counts as measurements (still also metadata). New[:claudio, :messages, :count_tokens]span; new[:claudio, :messages, :stream, :start | :stop]around each stream consumption (duration measured from the start of consumption to its end, tokens, exactly one:stop), linked to thecreatespan whenparse_events/1is given the whole response; new per-attempt[:claudio, :http, :request, :start | :stop]for every endpoint (retries visible asattempt).createandcount_tokens:startcarryserver_port; a linked stream:startalso carriesrequest_model, the request params that were set,server_addressandserver_port. No event carries headers, bodies, the API key or message content, with two exceptions: the deprecatederrorkey on a failedcreate :stop(aninspectof the error, which can include the API's error body or, for a malformed 200, the response body), and the:exceptionevents'reasonandstacktrace(standard:telemetry.spanbehavior), which can include request or response data. Exporters should useerror_type.error_typeand token values are bounded: a server-supplied type string passes only if it is an identifier ([a-z][a-z0-9_]{0,63}), else:unknown, and a token key is kept only when its value is a non-negative integer.Claudio.Client.new/2accepts:timeout,:recv_timeoutand:retryper client. A per-client value wins overconfig :claudio, Claudio.Client, which remains the fallback, so one application can run clients with different retry behaviour.Managed Agents foundation (beta
managed-agents-2026-04-01, attached per request):Claudio.ManagedAgents.Agents(create / get incl.version:/ update / list / archive / list_versions),Claudio.ManagedAgents.Environments(create / get / update / list / archive / delete),Claudio.ManagedAgents.Sessions(create / get / update / list / archive / delete,send_events/3,list_events/3, session resources). List options encode the API's bracketed filters (statuses: [...]→statuses[]=…,created_at: [gte: dt]→created_at[gte]=…).Claudio.ManagedAgents.stream/2walks cursor-paged lists lazily. Raw-map returns, likeClaudio.Skills.Telemetry guide (docs): context propagation into
Tasks and atraceparentrequest step for trace headers to the API.
Changed
Claudio.MCP.ServerConfig::tool_configurationstruct field removed; tool selection lives on the toolset (:default_config,:configs).allow_tools/2now takes exact tool names and raisesArgumentErroron*/?patterns (the connector matches names literally; a pattern would enable no tools). Raw maps with legacytool_configurationare translated with a warning.Claudio.Skillsno longer attachesanthropic-beta: skills-2025-10-02(Skills API is GA).Skills.list/2responses lose"has_more"(verified live;list_versions/3not probed) — page withnext_page→:page, or opt back in withClaudio.Client.with_betas(client, ["skills-2025-10-02"]).Request.add_mcp_server/2raisesArgumentErrorwhentoolsalready holds anmcp_toolsetfor that server and the new server carries tool config (it would otherwise be dropped).ServerConfig.allow_tools/2replaces any previous allowlist: tools enabled only by an earlier call are no longer enabled (other per-tool settings are kept).set_default_config/2/configure_tool/3store setting keys as strings.Request.add_mcp_server/2raisesArgumentErrorwhen a server with the same name is already on the request (the connector requires unique server names).Request.add_code_execution_tool/2defaults tocode_execution_20260521(same runtime as20260120).Response.usagekeeps every field the API returns: documented fields are atom keys (new:cache_creation,service_tier,inference_geo,speed,iterations); any other field is kept under the key it arrived with instead of being dropped. Documented fields the API did not send now appear asnil, so exactusage == %{...}comparisons need the new keys.Claudio.Messages.count_tokens/2strips every field the count endpoint rejects (see Fixed), for raw maps too.Response.to_assistant_content/1applies the API's continuation rules after a server-side fallback: before the lastfallbackblock it dropsthinking,redacted_thinking,connector_textandtool_use, and keepsserver_tool_use/mcp_tool_useonly when their result is present. Output is unchanged for responses without afallbackblock, or with it first (the normal non-streaming shape).Response.get_tool_uses/1,Tools.extract_tool_uses/1(sohas_tool_uses?/1) andMCP.ResultMapper.extract_mcp_calls/1skiptool_useblocks before the lastfallbackblock — they came from the model that declined.Request.add_message/3declaresserver-side-fallback-2026-07-01when its content holds afallbackblock; the API rejects a replayedfallbackblock without it.Response.stop_reasonis:compaction(was the string"compaction").Request.add_message/3sends a typed content block that carriesraw:(e.g. fromResponse.compaction_block/1) as its original API map instead of the typed map.- Parsed
tool_useblocks gaincallerandtoolset_name,server_tool_usegainscaller,web_search_tool_resultgainscallerandraw(nilwhen absent). Code matching the whole map with==must add them. The same holds for the mapsTools.extract_tool_uses/1returns (now withtoolset_nameandcaller). Request.add_computer_tool/4raisesArgumentErroron unknown options (they were ignored).Claudio.Agentresumespause_turn(counts toward:max_turns) instead of returning it, carries the responsecontainerto the next request, and dispatches client-toolset calls to the handler keyed bytoolset_name. A handler of the wrong arity is now an error result instead of a crash.Claudio.Agentcontinues afterstop_reason: :compaction(Request.apply_compaction/2, then another call with no user turn; counts toward:max_turns) instead of returning the summary as the final reply. A handler returning anything other than{:ok, _}/{:error, _}raisesArgumentErrornaming the handler (was aCaseClauseError).Claudio.Agent.run/4errors keep the history: an API error mid-loop returns{:error, reason, last_response_or_nil, messages}(was{:error, reason}, which discarded executed turns). Match the 4-tuple, like:max_turns_exceeded.Claudio.Batches.get_results/2decodes results with string keys (waskeys: :atoms), like every other response, and returns{:error, {:invalid_result_line, n, line}}for a malformed line instead of silently dropping it.- Server-result blocks that were raw string-keyed maps in 0.6 (
web_fetch_tool_result,code_execution_tool_result,bash_code_execution_tool_result,text_editor_code_execution_tool_result,tool_search_tool_result,advisor_tool_result,container_upload,compaction) now parse as typed maps (%{type: :atom, ..., raw: map}); code matching%{"type" => "web_fetch_tool_result"}onresponse.contentmust match the atom type or readraw. - Stream parse errors carry
%Jason.DecodeError{}(was%Poison.ParseError{}). Claudio.MCP.ToolAdapter.to_claudio_tool/2omits a nil"description"and raises on invalid names (see Fixed).- The Messages streaming path requires a complete stream: a block that never closes is an error (see Fixed).
- Unknown-option errors name the function:
Request.add_web_search_tool/2: unknown option :max_use; allowed: :version, ...(wasKeyword.validate!/2's "unknown keys [...]"). A non-keywordoptsis anArgumentErrornaming the function instead of aFunctionClauseErrorfromKeyword. StillArgumentError; code matching the old message text must update. - Stream
:stoperror_typeis an atom for known API error types (as oncreate); unknown identifiers stay strings. Claudio.Messages.Response.get_text/1accepts the legacyClaudio.Messages.create_message/2map.- Builder functions raise
ArgumentErrornaming the fix for common mistakes (add_message(:system, …), string roles,nilcontent, out-of-range sampling values, stringmax_tokens/tool_choice, keyword tools / thinking configs) instead ofFunctionClauseError.set_max_tokens/2now also rejects non-positive integers. - Cache helpers (
set_system_with_cache/3,add_message_with_cache/4,add_tool_with_cache/3,set_cache_control/2) raise on unknown options and on attlother than"5m"/"1h";add_message_with_document/5andsearch_result_block/4raise on unknown options (previously ignored). APIError.typeis an atom forbilling_error,request_too_largeandtimeout_error(was a string); a JSON error body withouterror.typeis typed from the HTTP status, and its top-levelmessageis used.Client.new/2validates its config: it raisesArgumentErroron a missing/empty:tokenand on unknown config keys, and accepts a keyword list.version: nilnow falls back to the default like the other keys (it used to drop theanthropic-versionheader).- Legacy
Claudio.Messages.create_message/2now emits the[:claudio, :messages, :create]span. It recognises only the string key"stream" => trueas streaming (as before); an atomstream: trueis routed, and labelled, as non-streaming. create :stoptoken keys (measurements and metadata) are absent, not0, when a 200 response carries nousage, and a non-integer token value is dropped. Handlers that readmeta.input_tokensunconditionally should useMap.get/3.- A streaming
Req.Responsefromcreate/2/create_message/2now carriesprivate.claudio(the linkClaudio.Messages.Stream.parse_events/1reads). Code that matchesprivate: %{}exactly or compares whole responses will see it. Claudio.Client.new/2adds a:claudio_telemetryReq step (request, response and error steps) that emits the HTTP events. User steps andReq.merge/2keep working; code that asserts on the step lists will see it.Claudio.Messages.Stream.parse_events/1no longer raises on a malformedmessage_start/message_delta(a non-mapmessageorusage): it passes the events through.Batches.wait_for_completion/3andBatches.list/2raise on unknown options;wait_for_completion/3also raises on a non-positive-integer:poll_interval/:timeoutor a:callbackthat is not a 1-arity function.Claudio.Messages.Stream.parse_events/1also accepts the whole%Req.Response{}.- The
:telemetryrequirement is now~> 1.3(was~> 1.0): span stop measurements need 1.3. Applications locked to an older:telemetrywill be asked to update it. - An invalid
retryvalue (anything buttrue,falseor a keyword list) or an unknown retry key now raisesArgumentError, for per-client and app config alike. Previously it was silently ignored and Req's default (GET/HEAD-only retries) applied. timeout/recv_timeoutmust be a non-negative integer (ms) or:infinity; anything else raisesArgumentErrorwhen the client is built (per-client and app config alike). Previously a bad value failed on the first request with an error that did not name it.retry:values (delay,max_delay,max_retries) must be non-negative integers; anything else raisesArgumentErroratClient.new/2(previously a baddelayraisedArithmeticErrorfrom inside the request).Claudio.Messages.Request.add_message_with_image/5also takesmedia_type:as a keyword option;Claudio.Agent.run/4raises on unknown options.
Deprecated
Claudio.Tools.create_tool_result_message/1(identity function): pass the result list straight toClaudio.Messages.Request.add_message/3.- The
errormetadata key on[:claudio, :messages, :create, :stop](aninspectstring that can contain the API's error response body, or a malformed 200's body). It will be removed in 0.8.0; useerror_type.
Fixed
- MCP connector now emits a valid
mcp-client-2025-11-20request:Request.add_mcp_server/2adds themcp_toolsetentry totoolsand declares the beta. Previously the request had no toolset and no beta header and was rejected. Response.usagekeepsoutput_tokens_details(raw map, e.g.thinking_tokens); it was dropped by the usage parser.set_output_format/2(and the new output-config helpers) no longer emit a duplicate key whenset_output_config/2was given atom keys — existing keys are stringified first.Claudio.Messages.Stream.build_final_message/1mergesmessage_deltausage overmessage_startusage instead of replacing it, soinput_tokens(and cache counters) survive when the delta omits them; previously the parsedResponse.usagecame back as a raw, incomplete map.Claudio.Messages.Stream.build_final_message/1keeps a streamed threshold-compaction summary (compaction_delta); it was dropped, leaving"content": null.Request.set_context_management/2also declarescompact-2026-01-12when its edits hold acompact_20260112edit; with onlycontext-management-2025-06-27the API rejects it. Its doc example ("strategy" => "auto") was not a real API shape and is replaced.Response.to_assistant_content/1re-emitstoolset_nameandcallerontool_use(andcalleronserver_tool_use/web_search_tool_result); replaying a client-toolset call withouttoolset_namewas rejected.Claudio.Messages.Stream.build_final_message/1decodes streamed tool input (input_json_deltachunks ontool_use/server_tool_use/mcp_tool_use) into the block's"input"; it was left as an undecoded"partial_json"string with an emptyinput. Invalid JSON (e.g. cut off bymax_tokens) returns{:error, {:invalid_tool_input_json, index, partial_json}}.- Streaming:
Claudio.Messages.Stream.parse_events/1no longer tears apart an event split across network chunks (it emitted anevent:with no data and adata:with no event, which crashedbuild_final_message/1or silently lost the event). CRLF line endings, multi-linedata:and a final event without a trailing blank line are handled.build_final_message/1keeps interleaved blocks (keyed by index) and returns an error for a truncated stream —{:incomplete_stream, indexes}for a block that never closed,{:incomplete_stream, :no_message_stop}whenmessage_stopnever arrived — instead of a message silently missing content andstop_reason. Streamed usage telemetry mergesmessage_startusage (input and cache counts were lost). Response.to_assistant_content/1no longer sendsserver_nameon a replayedmcp_tool_result— the API rejects it, so replaying any MCP-connector turn failed.Request.add_message/3declaresmcp-client-2025-11-20when its content holds anmcp_tool_useormcp_tool_resultblock; replaying an MCP call on a turn withoutadd_mcp_server/2was a 400.Request.add_message/3with parsedResponsecontent (e.g.response.content) sends each block in API shape (keeping acache_controlyou added); it sent"caller": null, which the API rejects.Claudio.Messages.count_tokens/2also stripstemperature,top_k,top_p,stop_sequences,metadata,service_tierandcontainer(Request and raw-map forms) — each made the count endpoint return 400, so a request using them couldn't be counted.Claudio.APIError.from_response/2handles non-JSON bodies (an empty 5xx, a proxy's HTML page — streaming or not) and odd JSON error shapes instead of raising; the type comes from the HTTP status (429 →:rate_limit_error, 529 →:overloaded_error, …). A 200 whose body isn't a JSON object is anAPIErrortoo.- Retries actually happen: the documented
config :claudio, Claudio.Client, retry: ...was a no-op placeholder (and Req's default never retries POST). It now retries 408, 429, 5xx, 529 and connection errors on every method;retry: falsedisables retries (Req's GET/HEAD default too). Streaming requests are not retried. config :claudio, default_api_version: ..., default_beta_features: [...](the documented form) is honoured; only the nestedconfig :claudio, :claudio, ...form was read.Claudio.Batches.create/2dropsstreamfrom%Request{}items (batch items are never streamed).Claudio.Agent: a handler returning{:error, %SomeException{}}/{:error, {:tuple, ...}}no longer crashes with a protocol error (the reason becomes text);stream: truerequests andmax_turns< 1 raise a clearArgumentError(was aCaseClauseError/ one call); a failed on-demand compaction no longer re-sends the compaction request every turn.Claudio.MCP.ToolAdapter.to_claudio_tool/2emits tools the API accepts: no"description": null, aninput_schemawith"type", and anArgumentErrorfor names outside^[a-zA-Z0-9_-]{1,128}$(were 400s).- MCP adapters:
HermesMCPunwraps Hermes response structs (list functions returned{:ok, []}), passesopts, and documents the client-first module ({Hermes.Client.Base, MyClient}— the old{MyClient, pid}usage crashed);ExMCPrequestsformat: :map(list functions returned{:ok, []}under ex_mcp 1.5's default); no adapter reports a bug inside the library as "not available". Version constraints corrected (ex_mcp ~> 1.5,mcp_ex ~> 0.1). Tools.create_tool_result/4rejects content the API would reject (an empty error result, a list of non-block values) and raisesArgumentErrorfor unencodable values instead of a protocol error;Tools.extract_tool_uses/1normalizes a rawtool_usewithoutinput.Requesttool helpers: atom-keyed tool maps no longer get a duplicate JSON key;add_tool_with_cache/3validates its options (:ttl,:allowed_callers) instead of silently dropping others;add_web_search_tool/2,add_web_fetch_tool/2,add_text_editor_tool/2validate options and accept dated version atoms;add_message_with_image/5detects PNG, GIF and WebP data when no media type is given (it always sentimage/jpeg).- Removed the
:poisondependency and a bug where every GET/DELETE request (Models, Files, Batches, Admin, Skills) sent the literal body"Elixir.Poison"(#17). If your app used Poison only through Claudio, add it yourself. - Retries honour
Retry-Afteron 529 (overloaded) as well as 429/503, and also whenretry: [delay: …]is set (previously a configured delay overrode the server's Retry-After). Claudio.Tools.tool_definitionandtool_resulttypespecs described atom-keyed maps;define_tool/3andcreate_tool_result/4return string-keyed maps. The types now match (documentation-only; no runtime change).- The legacy streaming
Claudio.Messages.create_message/2now matchescreate/2: it is never retried (a retriedinto: :selfrequest left the failed attempt's body messages in the caller's mailbox), and a non-200 error body is drained off the mailbox, so theAPIErrorcarries the API's message instead of a generic "Streaming request failed". - Draining a streaming error body (both streaming paths) now cancels the response when its 2s
deadline expires, so chunks still in flight no longer land in the caller's mailbox after
the call returns, and a transport error mid-body ends the drain at once instead of
waiting out the deadline. The
APIErrorkeeps the HTTP status either way. - Draining a streaming error body now receives only the messages of its own response (a selective receive on the body's ref). Unrelated messages in the caller's mailbox are no longer taken off and re-sent to the end, so their order is untouched.
- Telemetry guide: the OpenTelemetry handler records Erlang errors (
:function_clause, …) as exceptions; it is now checked in CI (scripts/check_otel_guide.exs).
Security
- The
reqrequirement is now~> 0.6 and >= 0.6.1(was~> 0.5). Every req release before 0.6.1 carries EEF-CVE-2026-49755 (decompression-bomb DoS) and everything before 0.6.0 carries EEF-CVE-2026-49756 (multipart header injection, whichFiles.upload/3andSkills.create/2uploads would reach). Applications locked to an older req will be asked to update it. Verified: the suite passes on req 0.6.1 (with finch 0.22.0, mint 1.11.0) as well as the locked 0.7.4. - Locked dependencies updated past Hex security advisories (req 0.5.15 → 0.7.4,
mint 1.6.2 → 1.11.0, hpax 1.0.0 → 1.1.0, and the test-only plug/cowboy stack).
mix.lockdoes not ship with the package: applications using Claudio should runmix deps.update req mint hpaxthemselves. Test-only cowboy/cowlib are pinned to 2.18.0/2.19.0 (cowlib 2.20.0 does not compile on OTP 26); three cowlib advisories in that test-only server are acknowledged inmix.exs(hex: [ignore_advisories: ...]).
Internal
- Added Credo (
--strict) and Dialyxir, amix precommitalias, and CI checks for Credo, Dialyzer,docs --warnings-as-errors,hex.audit, and unlocked dependencies.
Docs
- Examples use
claude-opus-5-5(sampling-setter examples useclaude-haiku-4-5, since Opus 4.7+ and 5.x reject sampling params);enable_thinking/2andset_tool_choice/2document the 400s on current models; Files documented as GA. - README and the getting-started guide: install snippet (
~> 0.7), working streaming, Batches and telemetry examples (the old ones called functions or events that don't exist); CLAUDE.md arities and key-style notes corrected; the CHANGELOG is published on hexdocs; CI covers Elixir 1.15 / OTP 25, 1.18 / OTP 27 and 1.19 / OTP 28.
0.6.0 - 2026-06-19
A large coverage release closing the gap between Claudio and the current Anthropic API surface (roadmap specs S1–S9). All additions are backward-compatible — no breaking changes.
Added
- Models API (
Claudio.Models) — GA, no beta header.list/2(GET /v1/models, paginated::limit,:before_id,:after_id) andget/2(GET /v1/models/{id}, id or alias).
- Structured outputs (
Claudio.Messages.Request) — GA.set_output_format/2buildsoutput_config.formatfrom a JSON schema;set_output_config/2is the raw setter.add_strict_tool/2(strict: true) andadd_tool_with_eager_streaming/2(eager_input_streaming: true).
- Message-level prompt caching —
add_message_with_cache/4(per-messagecache_controlbreakpoints) andset_cache_control/2(top-level). - Per-feature beta-header management —
Request.add_beta/2/required_betas/1andClient.with_betas/2; feature setters (e.g.set_context_management/2) declare their own betas, which the send path merges intoanthropic-betaautomatically. - Citations & search results — GA.
add_message_with_document/5threads:citations/:title/:context(backward-compatible with the/4arity).search_result_block/4builds RAGsearch_resultcontent blocks.Response.get_citations/1aggregates citations preserved ontextblocks (char_location,page_location,content_block_location,search_result_location,web_search_result_location).server_tool_useandweb_search_tool_resultcontent blocks are now typed;Response.get_server_tool_uses/1extracts them.
- Server-side tool helpers (
Claudio.Messages.Request) — only computer-use declares a beta (computer-use-2025-01-24, auto-declared):add_web_search_tool/2,add_web_fetch_tool/2,add_code_execution_tool/1,add_bash_tool/1,add_text_editor_tool/2,add_memory_tool/1,add_computer_tool/4.
- Admin API (
Claudio.Admin) — GA, uses an Admin API key (sk-ant-admin…) via the existingx-api-keyheader.- Organization (
get_organization/1), members, invites, workspaces, API keys, and usage/cost reports (usage_report/2,cost_report/2).
- Organization (
- Bearer / OAuth auth (
Claudio.Client) —auth_type: :bearersendsAuthorization: Bearer <token>(for OAuth / Workload Identity Federation tokens) instead ofx-api-key. Defaults to:api_key(no behavior change). - Agent Skills API (
Claudio.Skills) — betaskills-2025-10-02(attached automatically).list/2,get/2,delete/2, version sub-resources, and multipartcreate/2/create_version/3.
Fixed
- Extended-thinking multi-turn round-trips —
thinkingblocks now preservesignature,redacted_thinkingblocks are typed and preserved, and the streaming parser keepssignature_delta/citations_delta. Replaying an extended-thinking + tool-use turn no longer triggers400 invalid_request_error.Response.to_assistant_content/1is the single serializer (the divergentAgentserializer was removed). - Response content getters tolerate untyped blocks —
get_text/1,get_tool_uses/1,get_citations/1,get_server_tool_uses/1, andget_mcp_tool_uses/1,2no longer raiseKeyErroron raw blocks Claudio doesn't type (e.g.code_execution_tool_resultfrom dynamic-filtering web search).
Notes
- Not implemented (documented): Bedrock/Vertex transports (SigV4 / GCP ADC)
and the OAuth token-exchange flow are out of scope (supply an already-obtained
bearer token); prompt-tools (
/v1/experimental/*) are deferred (experimental, access-gated, beta header unverified).
0.5.0 - 2026-05-01
Added
- Anthropic Files API support (beta
files-api-2025-04-14)Claudio.Files.upload/3— multipart upload to/v1/filesClaudio.Files.list/2— paginated listing with:limit,:before_id,:after_idClaudio.Files.get/2— fetch file metadataClaudio.Files.download/2— fetch raw file bytes (binary, not JSON-decoded)Claudio.Files.delete/2— delete a file- Uploaded files are referenced from messages via the existing
Claudio.Messages.Request.add_message_with_document/4helper (no API change required there) - Module grouped under "Files API" in ex_doc
- Callers must opt in to the beta by passing
beta: ["files-api-2025-04-14"]toClaudio.Client.new/2, or viaconfig :claudio, :claudio, default_beta_features: ["files-api-2025-04-14"]
0.4.0 - 2026-04-24
Changed
- BREAKING: Streaming event data now decoded with string keys
Claudio.Messages.Stream.parse_events/1previously decoded SSE event payloads withPoison.decode(keys: :atoms), producing atom-keyed data maps. It now decodes with Poison's default (string keys), matching the raw Anthropic JSON convention.- This is a breaking change for external consumers that pattern-match on
atom keys inside
event.data(e.g.%{delta: %{type: "text_delta"}}). Downstream code should switch to string keys (%{"delta" => %{"type" => "text_delta"}}). - Claudio's internal helpers (
accumulate_text/1,apply_delta/2,build_final_message/1,update_current_block/2) already readdata["x"] || data[:x]defensively, so this is a no-op inside Claudio.
Fixed
- Fail loudly on malformed SSE JSON payloads
parse_event/1previously swallowedPoison.decode/1errors as{:ok, %{event: ..., data: nil}}, hiding corruption and leaving downstream consumers to operate on silently-missing data.- Decode failures now return
{:error, {:invalid_event_data_json, event_type, reason}}, consistent with the existing{:invalid_event, _}error tag emitted by the same function.
[0.3.0] - 2026-04-19
Added
- Messages telemetry usage metadata
[:claudio, :messages, :create, :stop]now includes flat token usage metadata keys for non-streaming success::input_tokens:output_tokens:cache_creation_input_tokens:cache_read_input_tokens
- Nil usage values are omitted from telemetry metadata to keep downstream integer matching clean
- Streaming usage telemetry event
- New
[:claudio, :messages, :stream, :usage]event emitted when stream consumption reachesmessage_stopand final usage is available
- New
Changed
- Documented streaming telemetry contract in
Claudio.Messages.Streamto clarify that final streaming usage is emitted separately from request-span stop metadata
0.2.0 - 2026-04-18
Added
- Full MCP (Model Context Protocol) Support
- Modular adapter system for different MCP server implementations
- Integration with
ex_mcp,hermes_mcp, andmcp_ex - Tool adapter for seamless conversion between MCP tools and Claudio tools
Claudio.AgentStateless Tool-Calling Loop- Autonomous agent loop that handles multiple rounds of tool execution
- Support for custom callbacks and max iterations
- Agent-to-Agent (A2A) Protocol Support
- Typed client for agent communication
- Support for multi-agent workflows with common transport interfaces (HTTP, gRPC)
- Extensible transport layer using the Strategy pattern
- Cloud Observability & Telemetry
Emits
:telemetryevents for all LLM API calls ([:claudio, :request, :start | :stop | :exception])- Correction (2026-10-01):
[:claudio, :request, ...]never shipped; 0.2.0 emitted[:claudio, :messages, :create]. See the telemetry guide for the current events.
- Correction (2026-10-01):
- Track request duration, token usage, and error reasons
- Support for custom Finch connection pools to manage concurrency
- Issue Tracking with Beads
- Initialized
bd(beads) for issue and task tracking in the repository
- Initialized
Changed
- Updated default model to
claude-sonnet-4-5-20250929across documentation and examples - Removed unused dependencies from
mix.lock
Fixed
- Critical: Fixed
UndefinedFunctionErrorwhen streaming requests fail by ensuring async body is drained on error - Improved error reason reporting in telemetry events
0.1.2 - 2025-01-26
Added
- GitHub Actions CI workflow for automated testing
- Test on Elixir 1.15-1.17 and OTP 26-27
- Run unit tests on all PRs and pushes to main
- Run integration tests on main branch (requires ANTHROPIC_API_KEY)
- Check code formatting and unused dependencies
Changed
- Major README overhaul with comprehensive documentation
- Added "Why Claudio?" section highlighting key benefits
- Added detailed real-world examples for all features
- Added streaming, tool calling, vision, and batch processing examples
- Added Best Practices section with 7 actionable tips
- Added Contributing guidelines
- Added badges (Hex version, docs, CI status, license)
- Improved Quick Start guide with 3-step setup
- Updated GETTING_STARTED guide to remove broken links
- Updated LICENSE copyright to Antonio Cabrera
- Improved documentation configuration in mix.exs to include guides
0.1.1 - 2025-01-26
Fixed
- Critical: Fixed
UndefinedFunctionErrorwhen streaming requests fail with non-200 status codes APIError.from_response/2now properly handlesReq.Response.Asyncstructs from failed streaming requests- Added pattern match for struct responses before map clause to prevent Access behaviour errors
- Returns generic error message for streaming failures instead of attempting to parse struct body
Added
- Test case for streaming error response handling
0.1.0 - 2025-01-26
Added
New Modules
Claudio.Messages.Request- Fluent request builder API for constructing Messages API requestsClaudio.Messages.Response- Structured response parsing with helper methodsClaudio.Messages.Stream- Server-Sent Events (SSE) parser for streaming responsesClaudio.Batches- Complete Message Batches API implementationClaudio.Tools- Utilities for tool/function callingClaudio.APIError- Structured error handling exception
Request Builder Features
- Chainable methods for all API parameters (temperature, top_p, top_k, etc.)
- System prompt configuration
- Stop sequences support
- Tool definitions and tool choice
- Thinking mode configuration
- Metadata support
- Streaming enablement
Response Parsing
- Structured content block parsing (text, thinking, tool_use, tool_result)
- Stop reason atom conversion for pattern matching
- Helper methods:
get_text/1,get_tool_uses/1 - Support for both string and atom keys
Streaming Support
- SSE event parsing with buffer accumulation
- Event types: message_start, content_block_delta, message_delta, etc.
- Delta types: text_delta, input_json_delta, thinking_delta
accumulate_text/1for extracting text streamsfilter_events/2for event filteringbuild_final_message/1for message reconstruction
Tool/Function Calling
define_tool/3for creating tool definitions with JSON schemasextract_tool_uses/1for extracting tool requestscreate_tool_result/3for creating tool responseshas_tool_uses?/1for checking tool usage- Support for error tool results
Message Batches API
create/2- Submit up to 100,000 requests per batchget/2- Retrieve batch statusget_results/2- Download JSONL resultslist/2- List batches with paginationcancel/2- Cancel in-progress batchesdelete/2- Delete batches and resultswait_for_completion/3- Poll with callback support
Error Handling
- Structured
APIErrorexceptions - Error type atoms: :authentication_error, :invalid_request_error, :rate_limit_error, etc.
- Consistent error handling across all API modules
- Preservation of raw error bodies for debugging
Testing
test/request_test.exs- 23 tests for request buildertest/response_test.exs- 13 tests for response parsingtest/tools_test.exs- 10 tests for tool utilitiestest/api_error_test.exs- 6 tests for error handling- Total: 55 tests, all passing
Documentation
- Comprehensive
@moduledocfor all new modules @docwith examples for all public functions@spectype specifications throughout- Updated main
Claudiomodule with usage examples - Updated
CLAUDE.mdwith new architecture details
Changed
HTTP Client Migration
- Migrated from Tesla to Req for better streaming performance and configurability
- Fixed timeout configuration - now properly respects custom settings
- Connection timeout default: 60s (configurable via
:timeout) - Receive timeout default: 120s (configurable via
:recv_timeout) - Streaming responses now complete quickly instead of timing out
- Added retry support for transient failures
Messages Module
- Added new
create/2function alongside legacycreate_message/2 - Both functions now return structured
APIErroron failure create/2returnsResponsestructs for non-streaming requestscount_tokens/2now acceptsRequeststructs in addition to maps- Improved error handling with consistent error types
Dependencies
- Replaced Tesla and Mint with Req ~> 0.5
- Added Bypass ~> 2.1 for testing (replaces Tesla mocks)
- Added Plug Cowboy ~> 2.0 for test server
- Moved Jason from test-only to production dependency
- Poison remains the primary JSON library for production
- Added ex_doc ~> 0.31 for documentation generation
Maintained
Backward Compatibility
- Legacy
create_message/2API fully maintained - Raw map payloads still supported
- Error tuple format
{:ok, result}/{:error, error}preserved - All existing tests continue to pass
Technical Details
Architecture Improvements
- Clear separation between request building, API calls, and response parsing
- Consistent error handling pattern across all modules
- Type safety with extensive
@typeand@specannotations - Support for both streaming and non-streaming in unified API
Code Quality
- All code formatted with
mix format - 55 tests with 100% pass rate
- Async tests where possible for performance
- Comprehensive test coverage of new functionality