[REL] v0.6.1 — GPT image family support
Other changes:
- Support
gpt-image-2,gpt-image-2.5-sunburst,gpt-image-2.5-flare,gpt-image-1.5,gpt-image-1-mini, their dated snapshots andchatgpt-image-latestinALLM.Providers.OpenAI.Images. Every model butgpt-image-1previously sentresponse_formatand was rejected by OpenAI withUnknown parameter: 'response_format'; anygpt-image-*model now gets the base64-only wire shape, token usage, and:urlpre-flight rejection - Forward
output_compression,moderation(generate) andinput_fidelity(edit) fromImageRequest.optionsfor GPT image models - Accept
:medium,:xhighand:maxinImageRequest.quality - Update the image generation guide for
gpt-image-2, arbitrary sizes, the new options, and OpenAI's 2026-12-01 removal ofgpt-image-1-mini,gpt-image-1.5andchatgpt-image-latest
[REL] v0.6.0 — Content moderation, compact tools, audio, prompt caching, typed classification and transcript timings
Breaking changes:
- An
%ALLM.Image{source: {:base64, _}}whose data will not decode is now rejected locally as%ALLM.Error.ValidationError{reason: :invalid_message}carrying:unresolvable_image, instead of being forwarded for the provider to answer 400. Code matching on the provider's%AdapterError{}for this case now sees a%ValidationError{}from pre-flight ALLM.Error.EngineErrorgains:no_moderation_adapter,:no_speech_adapter,:no_transcription_adapterand:no_classification_adapter, andALLM.Error.ValidationErrorgains:invalid_moderation_request,:invalid_speech_request,:invalid_transcription_requestand:invalid_classification_request. Both are closed enums, so an exhaustivecaseover either reason union needs new clausesALLM.Providers.OpenAI.ImagesandALLM.Providers.Gemini.Imagesno longer put:body_previewin an%ImageAdapterError{}'s:metadata, and redact key material from its:message. Code readingmetadata.body_previewnow gets nothing- The
ALLMfaçade functionimage_variations/3and the:variationimage operation are removed. OpenAI retiredPOST /v1/images/variations(it now 404s for every model) and no other bundled provider supported it.ALLM.ImageRequest.operationis now:generate | :edit;supported_operations/0onALLM.Providers.OpenAI.ImagesandALLM.Providers.FakeImagesreturns[:generate, :edit], andOpenAI.Images.endpoint_for(:variation)no longer exists. A persistedImageRequestwithoperation: :variationno longer loads: its JSON failsALLM.Serializer.from_json/1with a%ValidationError{}({:_unknown, :atom_decode_failed}), and an in-memory or ETF-restored struct failsALLM.Validate.image_request/1with{:operation, :unknown}and is rejected by every bundled image adapter as:unsupported_operation.Validate.image_request/1no longer emits{:prompt, :not_allowed_for_operation}, which only:variationproduced.allm_conformance'sImageAdapterConformancedrops its variation case (8 cases, was 9) ALLM.Providers.Anthropicnow reports%ALLM.Usage{}input_tokensas the TOTAL prompt count, cached reads and cache writes included (input_tokens + cache_read_input_tokens + cache_creation_input_tokens), matching OpenAI and Gemini; the raw provider count moves tousage.extra["uncached_input_tokens"]. On a request with cache activityinput_tokensandtotal_tokensare therefore larger than before, andinput_cost, which prices every input token at the plain input rate, rises with them. A request with no cache activity is unchanged- OpenAI model ids of the
gpt-6family and later (gpt-6-sol,gpt-6-luna,gpt-6-astra, …) now dispatch to the Responses endpoint likegpt-5*, getmax_completion_tokenson Chat Completions, and keep their reasoning controls. They previously went to Chat Completions withmax_tokensand had reasoning controls stripped. The model-family match is now case-insensitive in all three tables - Streaming Chat Completions request bodies now carry
"stream_options" => %{"include_usage" => true}, so OpenAI sends usage on a stream. Astream_optionsyou set inrequest.optionsstill wins
Other changes:
- Add content moderation:
ALLM.moderate(engine, "…user text…")returns{:ok, %ALLM.ModerationResponse{}}via the engine's new:moderation_adapterslot.ModerationResponse.flagged?/1answers the common question in one call;ModerationResult.category_scorescarries the provider's full per-category float map for callers wanting their own threshold - Add
ALLM.Providers.OpenAI.Moderationagainst the freePOST /v1/moderations, targetingomni-moderation-latest.max_batch_size/0is 1000, settled by a live ladder and documented as a demonstrated floor rather than a provider-stated cap - Accept images: an
:inputmay carry%ALLM.ImagePart{}items alongside strings. Any image makes the whole input one multimodal item, so the provider returns exactly one result however long the list —ModerationRequest.multimodal?/1derives that before the call - Keep per-category maps provider-shaped and string-keyed rather than normalizing to a cross-provider atom taxonomy, so provider-controlled keys never grow the atom table
- Add the
ALLM.ModerationAdapterbehaviour with ten numbered invariants, a ten-case published conformance suite, andALLM.Providers.FakeModeration— a scripted adapter whose{:flagged, categories}entry makes "assert the app rejects flagged content" a one-liner - Add the
[:allm, :moderate, :*]telemetry span withresult_countandflagged_counton both success and error paths, andusage: nilon:stopso a handler written for:embeddoes notKeyError - Add
ALLM.Capability.preflight_moderation/2, inert without the optionalllm_dbcatalog, and bind it with a test — deleting the gate previously left the whole suite green - Add
guides/moderation.md— the engine slot,flagged?/1versus a per-category threshold, the caller-side chunk loop (moderation deliberately does NOT chunk transparently the wayembed/3does), image input and the multimodal cardinality rule, the one-row provider matrix and why it has one row, and theFakeModerationscripting grammar. Every runnable example executes as a doctest undermix test - Add
examples/19_moderate_text.exsandexamples/20_moderate_image.exs, plusExamplesHelpers.moderation_engine/1and theALLM_MODERATION_MODELoverride. Both carry a# Provider: openaimarker sorun_all.exsskips them on the arms that have no moderation adapter, and both cost $0.00 - Add compact tools for large tool catalogs:
ALLM.tool(..., compact: true)sends the tool to the model as a one-line stub (a summary, the argument names, and a bare{"type": "object"}schema) and adds one built-intool_helptool the model calls to get full descriptions and schemas on demand. Execution always uses the full tool. The tool list sent to the model is identical on every step of a run, so provider prompt caches keep working. Defaultcompact: falseleaves every existing tool unchanged on the wire - Add
ALLM.Tool's:summaryfield to override a stub's one-line summary.ALLM.Validate.tool/1rejects a non-string summary with{:summary, :not_a_string}, andTool.new/1raisesArgumentErroron a non-boolean:compact - Add
ALLM.ToolHelp:project/2shows exactly what the model receives,check_args/2is the required-argument check, andanswer/2builds thetool_helpresult a caller submits undermode: :manual. A compact tool called without a required argument returns a usage error carrying its full help instead of running the handler, routed through:on_tool_error - Add a "Compact tools" section to
guides/tools.mdandexamples/21_compact_tools.exs, which runs one task with and without compact tools on every provider. Measured step-1 input tokens fell by 54% (OpenAI), 64% (Gemini) and 44% (Anthropic) on its eight-tool fixture (one prompt, one run, default example models, all eight tools compact; compacting only part of a catalog saves less) - Fix unreadable image files crashing every vision adapter. A
%ALLM.Image{source: {:file, path}}whose file could not be read passed both the MIME and the size gate and then raised out ofgenerate/2andstream/2on all four translators; it now returns an error tuple - Fix transport timeouts never reaching Finch, so a slow reasoning turn is no
longer killed at Finch's 15 s default. Add
ALLM.Adapter.transport_opts/0as the neutral key list; transport opts now reach the adapter without leaking onto the request body, which also makes the documentedadapter_opts: [finch_name: …]route work for the first time - Assign the run-level
request_idunconditionally inALLM.Runner, dropping a defensive fallback no current code path could reach, and letALLM.Chatcompile clean under the Elixir 1.19 type checker - Silence every compile, test, docs, and conformance warning across both Mix projects
- Add text-to-speech:
ALLM.synthesize(engine, "Hello.", voice: "coral")returns{:ok, %ALLM.SpeechResponse{}}whose:audioholds the bytes, via the engine's new:speech_adapterslot.:formatis one of:mp3,:opus,:aac,:flac,:wav,:pcm, andresponse.formatreports what actually arrived, read from the response content type - Add speech-to-text:
ALLM.transcribe(engine, ALLM.Audio.from_file("clip.mp3"))returns{:ok, %ALLM.TranscriptionResponse{text: …}}via the new:transcription_adapterslot.response.usageis nevernil; models billed by the second reportresponse.duration_secondsinstead of tokens - Add
ALLM.Audio, one serializable audio value for both directions (file, binary or base64 source; bytes base64-encoded on JSON), plusALLM.SpeechRequest/ALLM.SpeechResponse,ALLM.TranscriptionRequest/ALLM.TranscriptionResponse,ALLM.Error.SpeechAdapterErrorandALLM.Error.TranscriptionAdapterError, and thespeech_request/2/transcription_request/2builders andALLM.Validate.speech_request/1/transcription_request/1 - Add per-slot audio models on the engine:
:speech_modeland:transcription_model. The audio calls never read the chatengine.model; the model isrequest.model, then the slot's field, then the adapter's default - Add
ALLM.Providers.OpenAI.Speech(POST /v1/audio/speech, defaultgpt-4o-mini-tts, voice"alloy"when unset), which rejects input over 4096 code points locally as:context_length_exceededbefore any key lookup; andALLM.Providers.OpenAI.Transcription(POST /v1/audio/transcriptions, defaultgpt-transcribe), whosemax_audio_bytes/0is 26,148,864, settled by a live probe against the 25 MiB body cap - Add
ALLM.Providers.Gemini.Transcription, which transcribes by prompting a chat model (gemini-flash-latestby default) with the audio inline. Itsmax_audio_bytes/0is 15,679,488. Its transcript can paraphrase, and given silence it returned invented speech rather than""; both caveats are in its docs and the guide. A safety or recitation stop is:content_filter, and a truncated transcript comes back as a success withmetadata.finish_reason: :length - Add the
ALLM.SpeechAdapterandALLM.TranscriptionAdapterbehaviours,ALLM.Providers.FakeSpeechandALLM.Providers.FakeTranscription(scripted, with the moderation Fake's run-dry-is-an-error rule), and two published six-case conformance suites inconformance/ - Add the
[:allm, :synthesize, :*]and[:allm, :transcribe, :*]telemetry spans, measuringaudio_bytesandtext_length. The synthesis:stopmetadata carries the whole response, audio bytes included - Add
guides/audio.md(both directions, formats, voices, per-slot models, size limits, Gemini fidelity, pairing providers, and the Fakes), with every runnable example executed as a doctest, andexamples/23_synthesize_speech.exs(# Provider: openai) andexamples/24_transcribe_audio.exs(# Provider: openai, gemini), plusExamplesHelpers.speech_engine/1/transcription_engine/1and theALLM_SPEECH_MODEL/ALLM_TRANSCRIPTION_MODELoverrides - Add streaming speech:
ALLM.stream_synthesize(engine, "Hello.", format: :pcm)returns{:ok, events}, a lazy stream ofALLM.SpeechEvents (:speech_started,:audio_delta,:speech_completed) whose first audio arrives while the provider is still generating.ALLM.stream_synthesize_input/3also takes the text as an enumerable, so an LLM's answer is spoken while it is written - Add realtime transcription:
ALLM.stream_transcribe(engine, pcm_chunks, sample_rate: 16_000)returnsALLM.TranscriptionEvents (:partial_transcript, which replaces the previous partial, and:committed_transcriptsegments) while the audio is still arriving. It never readsengine.transcription_model, because batch and realtime model names differ - Add
ALLM.AudioStream:collect_speech/1andcollect_transcription/1fold a stream into the responsesynthesize/3/transcribe/3return, andtext_deltas/1turns a chat stream into text chunks. An audio stream that fails after opening ends with{:error, err}and the collectors return it, unlike chat's fold intofinish_reason: :error; a chat error insidetext_deltas/1ends the speech stream rather than speaking a truncated answer - Add the
ALLM.SpeechStreamAdapterandALLM.TranscriptionStreamAdapterbehaviours. An adapter opts in on its existing engine slot; a slot adapter that does not stream returnsEngineError :missing_stream_adapter.ALLM.Engineis unchanged.FakeSpeechandFakeTranscriptionimplement both, with a{:events, _}script entry for mid-stream failures, and three six-case conformance suites are published.TranscriptionAdapterConformancegains a:skip_casesoption - Add
ALLM.TranscriptionStreamRequestand:sample_rateonALLM.SpeechRequest/ALLM.SpeechResponse. The PCM default is 24,000 Hz on every bundled adapter, and each stream reports its rate on:speech_started. AddALLM.Validate.speech_request/2andtranscription_stream_request/1 - Add
:unsupported_featuretoSpeechAdapterErrorandTranscriptionAdapterError, returned before any request for a field the provider cannot express - Stream
ALLM.Providers.OpenAI.Speechover chunked HTTP (stream_synthesize/2).synthesize/2now reportssample_rate: 24_000for:pcmand:wav, and refuses any other rate as:unsupported_feature - Add ElevenLabs:
ALLM.Providers.ElevenLabs.Speech(synthesize/2,stream_synthesize/2over/stream, andstream_synthesize_input/3over its/stream-inputWebSocket; defaulteleven_flash_v2_5) andALLM.Providers.ElevenLabs.Transcription(transcribe/2withscribe_v2, andstream_transcribe/3over its realtime WebSocket withscribe_v2_realtime). The key resolves fromELEVENLABS_API_KEYand, on the WebSocket paths, travels in the upgrade headers, never the URL. Text-in streaming turns on ElevenLabs'auto_mode(first audio 238 ms against 563 ms in one measurement) and sends whole words only, so token deltas are not voiced as separate clips. Like Voyage, ElevenLabs has no chat adapter, so an engine pairs it with a chat provider - Add telephony speech formats
:ulawand:alaw(8 kHz G.711) toALLM.SpeechRequest.formats/0:ALLM.Providers.ElevenLabs.Speechsends them asulaw_8000/alaw_8000and refuses any other rate, andALLM.Providers.OpenAI.Speechrefuses both as:unsupported_featurebefore any request. The response MIME types areaudio/basicandaudio/alaw. Additive, but an exhaustivecaseoverSpeechRequest.format()gains two clauses - Add the
[:allm, :stream_synthesize, :*]and[:allm, :stream_transcribe, :*]spans (they stop when the stream is returned, so they carry no audio) and the[:allm, :audio, :first_chunk]event, which reports the time from the call to the first audio or partial transcript - Add the
:mint_web_socketdependency for the WebSocket paths - Extend
guides/audio.mdwith streaming, the voice loop, PCM and sample rates, latency choices, realtime transcription and an ElevenLabs section; addexamples/25_stream_speech.exs,26_stream_transcribe.exsand27_voice_loop.exs, and an audio-onlyALLM_PROVIDER=elevenlabsarm toexamples/run_all.exs - Add provider prompt caching:
prompt_cache: %{key: "recipe-42", retention: :long}(ortrue) as a call option or anengine.paramskey onchat/3,stream/3,step/3,stream_step/3and everyALLM.Sessionoperation. With no:key, the call's:session_id(a session's id) is the key, so a long-lived session caches without further setup; caching stays off unless asked for. It lands on the new typedALLM.Request.prompt_cachefield, whichALLM.Validate.request/1checks ({:prompt_cache, :invalid_shape}) and which survives JSON round-trips. OpenAI (both endpoints) sendsprompt_cache_keyand, for:long,prompt_cache_retention: "24h"; Anthropic sends a top-levelcache_control(ttl: "1h"for:long) and never the key; Gemini caches implicitly and ignores the option. A raw provider option inrequest.optionswins over the translation - Add
ALLM.Usage.cache_write_input_tokens, and report cache reads and writes incached_input_tokens/cache_write_input_tokenson every chat adapter, streaming and non-streaming alike: OpenAI Chat Completions and Responses, Anthropic and Gemini previously left some or all of these counters inusage.extraor dropped them on a stream. A counter the provider does not send staysnil, never0. Streamed usage now equals non-streamed usage field for field, and a streamed Anthropic response now reportsinput_tokens(it wasnil) - Add a "Prompt caching" section to
guides/sessions.mdandexamples/28_prompt_cache.exs, which drives a three-turn session over a ~5k-token prefix on every chat arm and prints each turn's cache reads and writes. Every provider wire field it relies on was confirmed against the live APIs by the prompt-cache recorder script; cache hits and the unknown-field controls are recorded as test fixtures - Add typed classification:
ALLM.classify(engine, ticket_text, questions: %{"team" => ALLM.ClassificationQuestion.choice("Which team?", ["billing", "technical"])})returns{:ok, %ALLM.ClassificationResponse{}}with one typed%ALLM.ClassificationAnswer{}per question, via the engine's new:classification_adapterslot. Three question types —ClassificationQuestion.choice/2,score/2andyes_no/2— mix freely in one call; answers carry the provider's probabilities and confidence, and the library sets no threshold.ALLM.classification_request/2builds the request;ClassificationResponse.answer/2andClassificationAnswer.value/1read it back - Add
ALLM.Providers.TypeSafe.Classificationagainst TypeSafe's Jev model (POST https://api.typesafe.ai/v1/systemone, keyTYPESAFE_API_KEY, default model"jev-latest"). TypeSafe's yes/no typenoulis:yes_noin ALLM. The adapter gates empty questions, more than 255 options and more than 10 score levels before any request, makes one HTTP attempt per call, puts thex-typesafe-request-idheader onClassificationResponse.id(and on an error'smetadata.typesafe_request_id), maps a 400max_tokens_exceededto:context_length_exceededand 529 to:provider_unavailable, and never stores an exception in an error's:cause - Add a per-slot classification model on the engine,
:classification_model;ALLM.classify/3never reads the chatengine.model - Add
ALLM.Validate.classification_request/1,ALLM.Error.ClassificationAdapterError(nine reasons), theALLM.ClassificationAdapterbehaviour with nine numbered invariants,ALLM.Providers.FakeClassificationfor tests, andALLM.Test.ClassificationAdapterConformanceinallm_conformance - Add the
[:allm, :classify, :*]telemetry span withquestion_countmetadata and ananswer_countmeasurement - Add
guides/classification.mdandexamples/22_classify_ticket.exs, which runs on a new TypeSafe-only examples arm (ALLM_PROVIDER=typesafe) - A streamed chat request that the provider answers with a 4xx/5xx
(OpenAI on both endpoints, Anthropic, Gemini) now reads the error body
before ending the stream, so its terminal
%AdapterError{}carries the provider's own:messageand aRetry-Afterheader's:retry_after_msinstead of only"<Provider> HTTP <status>". Streams are still never retried, so:retry_after_msis for the caller to act on. Chat error messages, streamed and non-streamed, now have key material redacted in each provider's own key format, and an"error"field that is a bare string becomes the message instead of raising. A chat stream the consumer halts early now also clears the transport's already-queued messages from the reducing process's mailbox - Add opt-in word timings and confidence to transcription:
ALLM.transcribe(engine, audio, timestamps: true, logprobs: true)fills the newALLM.TranscriptionResponse.spanswith%ALLM.TranscriptSpan{}structs (:text,:kind,:start_seconds,:end_seconds,:logprob), andTranscriptionResponse.mean_logprob/1averages the spoken units.ALLM.TranscriptionRequestandALLM.TranscriptionStreamRequestgain the two boolean flags (defaultfalse), whichALLM.Validate.transcription_request/1andtranscription_stream_request/1check (:invalid_shape). With both flags offspansisniland nothing else changes; an attribute that was not asked for isnilon every span ALLM.Providers.ElevenLabs.Transcriptionhonours both flags, batch and realtime. On the realtime path each committed segment carries its spans, with times measured from the start of the session, and a flag makes the adapter hold each segment up to 1,000 ms for its timing dataALLM.Providers.OpenAI.Transcriptionhonourslogprobs: true(it sendsinclude[]=logprobsand returns one:tokenspan per token) and refusestimestamps: true.ALLM.Providers.Gemini.Transcriptionrefuses both; itslogprobsrefusal rests on one probe ongemini-flash-latest. A refused flag is:unsupported_feature, returned before any upload or key lookup. When a provider accepts a flag but returns no span data, a blank transcript succeeds withspans: []and any other fails with:unsupported_feature,metadata.cause: :absent_from_responseand the transcript onmetadata.text- Add
ALLM.TranscriptionEvent.committed_transcript/3and an optional:spanskey on:committed_transcriptand:transcription_completed, written only when a flag is set;ALLM.AudioStream.collect_transcription/1copies the completed spans onto the response.ALLM.Providers.FakeTranscriptionhonours both flags on both paths, and a scripted real adapter refuses what its provider would ALLM.Providers.FakeTranscriptionnow raisesArgumentErrornaming the entry when a call reaches a{:retry_until_call, n}whosenis not a positive integer, instead of retrying forever or skipping it- Add a "Word timings and confidence" section to
guides/audio.md, with a per-provider support table, and flagged calls toexamples/24_transcribe_audio.exsand26_stream_transcribe.exs [:allm, :audio, :first_chunk]for a transcription stream now also fires at the first non-blank:committed_transcriptwhen no:partial_transcriptcame first; previously a stream the provider committed without a partial reported no time to first transcript
[REL] v0.5.0 — Text embeddings
Breaking changes:
ALLM.Engine.new/1now raisesArgumentErroron a non-module:adapter/:tool_executor/:tool_result_encoder/:image_adaptervalue (e.g. the unsupportedtool_executor: {Mod, tools: %{}}form). Previously it built a booby-trapped engine that crashed later at tool-run timechat/3,stream/3, andSession.*now honormax_tokens/temperaturefromengine.paramsand call opts. Responses that were silently capped at the adapter default (Anthropic's 1024) are no longer capped, so requests that previously truncated will now run to completion — expect longer outputs and higher per-call cost
Other changes:
- Add text embeddings:
ALLM.embed/3turns a string, a list of strings, or an%ALLM.EmbeddingRequest{}into vectors via the engine's new:embed_adapterslot.EmbeddingResponse.vectors/1returns[[float()]]in input order, ready for a vector column - Add
ALLM.Providers.OpenAI.Embeddings(2048 inputs/request),ALLM.Providers.Gemini.Embeddings(100), andALLM.Providers.Voyage.Embeddings(1000). Voyage is the Anthropic track — Anthropic ships no embeddings endpoint and names Voyage as its recommended partner, so the key resolves fromVOYAGE_API_KEYand there is deliberately noALLM.Providers.Anthropic.Embeddings - Chunk oversized input transparently: lists longer than the adapter's
max_batch_size/0are split, dispatched sequentially, and merged with indices rebased across chunk boundaries. A failing chunk fails the whole call, carryingcompleted_chunks/completed_inputson the error - Add the
ALLM.EmbeddingAdapterbehaviour plus a ten-caseALLM.Test.EmbeddingAdapterConformancesuite in theallm_conformancepackage for third-party adapter authors - Add serializable
%ALLM.Embedding{},%ALLM.EmbeddingRequest{}, and%ALLM.EmbeddingResponse{}, all round-tripping through ETF and JSON, plus%ALLM.Error.EmbeddingAdapterError{}and its eleven-member reason enum - Add
ALLM.Providers.FakeEmbeddings, shipped inlib/so downstream apps can script embeddings in their own tests Add the
[:allm, :embed, :start | :stop | :exception]telemetry span, withchunk_counton:stopso one call becoming fifty HTTP requests is visible- Add
guides/embeddings.md— provider matrix, task-type guidance, batching and resumable chunking, and apgvectorworked example — and example scripts 16–18, live-validated on all three providers - Execute bundled
guides/*.mdiex>examples undermix testviadoctest_file, so guide drift is a red test rather than a silent ship - Correct guide drift against the real 0.4.x API: engine-first
Session.*calls, the real status union, the handler-on-tool-executor pattern, theStreamReducerAPI, and theFakeImagesimage_scriptshape
Upgrade notes:
- Google's batch endpoint returns no usage metadata, so
response.usageis an all-nil%ALLM.Usage{}on Gemini. Voyage reportstotal_tokensonly; OpenAI reports both - The Gemini adapter L2-normalizes any response whose
:dimensionsis set to anything other than3072, because Google does not normalize truncated output. Adimensions: 768response therefore differs numerically from the same request issued withcurl— deliberate, so a vector table never holds a mix of normalized and unnormalized rows :task_typeis provider-neutral and lossy by design: Gemini maps all five members, Voyage supports query/document only, and OpenAI drops the field entirely. A dropped task type is never an error- Retry and timeout budgets are per chunk, with no aggregate deadline. A
:timeoutcosts 9 HTTP attempts per chunk rather than 3, because the adapter's retry loop and the facade's widened one both retry it;guides/embeddings.mdcarries the multiplication table
[REL] v0.4.3 — Engine identity
Breaking changes:
ALLM.Engine.new/1now stamps each engine with a unique:id, so two engines constructed from identical opts are no longer equal (Engine.new(o) != Engine.new(o)); code comparing two independently built engines for equality will see them differ
Other changes:
- Add a stable, serializable
:idfield to%ALLM.Engine{}— auto-assigned atnew/1, preserved acrosswith_model/2/merge_opts/2/put_tool/2, and round-tripping through both ETF and JSON (pre-existing serialized engines decode withid: nil) - Fix the
ALLM.Providers.Fake/FakeImagesmulti-call cursor footgun — the cursor is now keyed on engine identity at the façade, so engines built with content-equal scripts no longer share a cursor (each engine's first call reads index 0); direct adapter calls without an engine keep the prior content-hash behavior
[REL] v0.4.2 — Streaming usage fidelity
Other changes:
- Fix OpenAI Responses-API streaming usage landing as an all-nil
%Usage{}forgpt-5*models — streaming now folds input/output/total and reasoning token counts intoResponse.usage, matching the non-streaming Responses path
[REL] v0.4.0 — Integrator ergonomics
Breaking changes:
ALLM.Tool.new/1(and__from_tagged__/1) now recursively normalize atom-keyed:schemamaps to string keys for deterministic adapter wire shape; callers depending on atom keys on%Tool{}.schemawill see strings insteadALLM.Providers.Fakestreaming{:usage, map}script entries now fold onto:message_completed.metadata.usageinstead of emitting a:raw_chunkevent; tests asserting the prior:raw_chunkshape should pass{:raw_chunk, {:usage, _}}directly
Other changes:
- Add
ALLM.Sandbox— Mox-styleset_engine/1/get_engine/0/with_engine/2honouring$callersso engines set in a parent test process are visible toTask.async/1/Task.async_stream/3workers - Add
ALLM.unwrap/1— fold the three-clausegenerate/3return (:stop/:error/{:error, _}) into{:ok, text} | {:error, _} - Add
ALLM.Providers.Fakeadapter_opts[:usage](Usage struct or keyword; populatesresponse.usageon every call) andadapter_opts[:record](sends{:allm_fake_record, request, opts}to a pid before script interpretation) - Add
ALLM.Image.from_data_uri/1— parsedata:<mime>;base64,...strings into a{:base64, _}-source%Image{}that round-trips throughto_data_uri/1 - Add
ALLM.JsonSchema.normalize/1— shared atom-to-string key normalizer called by bothTool.new/1andALLM.json_schema/3 - Carry structured detail for
Validate.message/1's{:content, :invalid_part_type}error in%ValidationError{}.metadata(machine-readable) plus a human-readableException.message/1—errorslist shape unchanged - Document the arity-2
:handlercontext keys (:context | :session_id | :tool_call | :engine | :request_id) inALLM.Tool's@typedoc - Document
chat(engine, thread, response_format: schema, structured_finalize: true)inguides/tools.md— the existing tool-loop + structured coda flow was previously undiscoverable - Add
guides/fakes.md— consolidated Fake testing patterns (script vocabulary, cursor disambiguation,:usage/:record, halt-cleanup observation, retry simulation, Sandbox cross-process injection) - Extend
guides/getting_started.mdwith the three-clause finish-reason fold pattern and the engine-no-:api_keyclarification - Extend
guides/tools.mdwith handler-context keys and an "Adapter-call cadence" subsection (tool-loop turns consume 2 adapter calls each) - Extend
guides/vision.mdto point atImage.from_data_uri/1for data-URI inputs
[REL] v0.4.1 — Streaming HTTP timeout forwarding
Other changes:
- Forward
:receive_timeout,:request_timeout, and:pool_timeoutfromoptstoFinch.async_request/3in the streaming codepath ofALLM.Providers.OpenAI,ALLM.Providers.Anthropic, andALLM.Providers.Gemini. Previously these opts were dropped on the streaming arm — onlygenerate/2's non-streamingReq-based path honoured them — so callers passingrequest_timeout:to long-running streaming requests fell back to Finch's defaults (≈20s receive), causing premature:timeoutfailures on slow-first-token requests. Eachstream/2's@docnow documents the new opts and clarifies that:stream_timeout(inter-message) and:receive_timeout(HTTP-level) are distinct timers - Extend
ALLM.Test.FinchStubwithcaptured_opts/1so tests can assert which Finch-level options the adapter forwarded - Fix
scripts/release.exsmanglingmix.exs@versionwhen the new version starts with a digit (e.g.0.4.0). The replacement used\1back-references which Erlang'sreparsed as\10followed by literal text — producing.4.0"instead of@version "0.4.0". Switch to unambiguous\g{N}back-refs
[REL] v0.3.1 — Documentation rebuild
Other changes:
- Rewrite every
@moduledocand public@docso prose is self-contained — no internal phase, spec-section, or design-decision references - Add eight ExDoc guides under
guides/(Getting Started, Streaming, Tools, Sessions, Vision, Image Generation, Errors & Retries, Multi-Tenant Keys), shipped to both hexdocs and the source tarball - Restructure README around a 5-minute on-ramp and cross-link the guides instead of duplicating their content
- Drop the alpha warning in favor of a concrete stability statement (semver promise within v0.x)
- Add
scripts/audit_user_docs.exs(banned-token gate) andscripts/check_lib_diff_non_doc.exs(docstring-vs-body classifier) - Fix an
async: trueflake inanthropic_stream_wire_testby passing the stub key per-call instead of through the globalALLM.Keys.Storeagent - Clean up the release script — drop the redundant
finalizestep
[REL] v0.3.0 — Initial public release
First public release of ALLM — a provider-neutral, streaming-first LLM execution library for Elixir. The package is alpha: public APIs and on-disk session shapes may shift between releases until v1.0.
Other changes:
- Layer A serializable data:
Message,Thread,ToolCall,Request,Response,Session,StepResult,ChatResult,Event,Usage— round-trip through:erlang.term_to_binary/1and JSON - Stateless execution facade:
ALLM.generate/3,stream_generate/3,step/3,stream_step/3,chat/3,stream/3 - Stateful continuation via
ALLM.Sessionwith auto and per-tool manual orchestration modes and{:ask_user, ...}suspension - Streaming as the primitive — synchronous calls are reducers over a
closed
ALLM.Eventtagged-tuple union viaALLM.StreamCollector - Bundled adapters for OpenAI (Chat Completions + Responses), Anthropic Messages, and Google Gemini, all live-validated
- Vision input across all three providers via
ALLM.TextPart/ALLM.ImagePart - Image generation/edit/variation behaviour with an OpenAI Images adapter
- Telemetry events, retry policy, capability pre-flight, and BYOK key
resolution through
ALLM.Keys - Conformance harnesses (
ALLM.Test.AdapterConformance,ImageAdapterConformance) and a deterministicALLM.Providers.Faketest vehicle - Provider-neutral example scripts under
examples/runnable viaALLM_PROVIDER=<name> mix run examples/run_all.exs