All notable changes to this project are documented here. The format follows Keep a Changelog, and the project uses Semantic Versioning.
Unreleased
0.1.0-alpha.3 - 2026-09-17
Fixed
- The Livebook walkthrough shipped with a path dependency that only resolved inside a clone, so the "Run in Livebook" badge failed to install. It now installs the package from Hex and describes the cached question encoding.
0.1.0-alpha.2 - 2026-09-17
Changed
- Breaking: the module namespace is now
TypeSafeAPI(wasTypeSafe) and telemetry events are[:typesafe_api, :request, *](were[:typesafe, :request, *]). The Hex package, app name, config key, andTYPESAFE_*environment variables are unchanged. This avoids colliding with the unrelatedtypesafe_aipackage, which usesTypeSafe.*. instructionsis optional on every question type, matching the OpenAPI spec. Anilvalue is omitted from the request rather than sent asnull. Noul instructions may be a question or a statement to evaluate.- Connection errors are no longer retried for
POST /v1/systemoneby default.retry_connection_errorsnow defaults to:auto:trueforGETrequests,falseotherwise, because a replayed evaluation can be billed twice. Set it totrueexplicitly to restore the old behaviour. Server-signalled retries (429, 503, 529,Retry-After) are unaffected. - The per-call
:timeoutno longer feeds Req'sconnect_options, so different timeout values reuse one Finch pool. A newconnect_timeoutclient option (default 5 000 ms) controls the connection timeout instead. TypeSafeAPI.Question.Choice.new/2rejects maps with anArgumentError. Maps have no option order at any size; the previous "up to 32 entries" claim was wrong. Use a keyword list or a list of{key, description}pairs.DESIGN.mdrewritten for package consumers.
Changed (code review pass)
A file-by-file review of the library produced the following, each verified with a failing test before the fix. Several are breaking for alpha.1 callers.
- Questions. Duplicate question ids, including an atom and a string that collide on the
wire, are a
:validationerror instead of duplicate JSON keys. Anything that passesTypeSafeAPI.Question.validate/1is guaranteed to JSON-encode: structs, tuples, keyword lists and non-string map keys in instructions or descriptions are rejected up front. A map of questions is rejected like a map of Choice options; use a keyword list. Noul criteria acceptnildescriptions, string"true"/"false"keys, and keyword lists, andTypeSafeAPI.noul(true: "...", false: "...")reads the keyword list as criteria. Choice accepts a bare list of option names and its minimum is now one option (the spec has no bound); the maximum stays 255. Score rejects duplicate and empty labels andScore.label/1reads the%{"label" => ...}wire shape. - Answers decode strictly. Score and Choice probabilities must be numbers over exactly the
levels or options sent; missing Choice options are filled with
0.0; an unknown or non-argmaxchoice, an out-of-range Noul value, an empty probability object, or a legend that disagrees with the question is an:unexpectederror rather than a silently wrong answer.TypeSafeAPI.Answer.Choicegainsdescriptionand an orderedoptionslist;TypeSafeAPI.Answer.Noulgains a derivedconfidenceso all three answer structs share a shape.Answer.yes?/2on a non-Noul andAnswer.gate/2with a Noul review threshold at or below 0.5 raiseArgumentErrorwith an explanation. - Results and errors carry what support needs. Decode failures keep the request id
(
Result.decode/4).TypeSafeAPI.Errorgainsheaders,retry_count, a:server_errortype for 5xx other than 503/529, andError.retryable?/1; 408 maps to:timeout; a non-JSON 2xx keeps its status, request id and raw body; an empty 2xx body is aResponsewithbody: nil.ResultandPreparedhaveInspectimplementations that elide the bulk. - Retries.
retry_timeout_errorsdefaults to:autolikeretry_connection_errors, so a timed-outPOST /v1/systemoneis not replayed either. A server-sentRetry-Afteris clamped into the newretry_after_min(100 ms) andretry_after_max(60 s) and otherwise honored, so a zero or past value cannot cause a tight loop while a short explicit delay is respected. Per-callretry:keywords merge onto the client's policy instead of resetting it.TypeSafeAPI.Resultgainsretry_count. A Finch pool checkout timeout surfaces as:timeoutand is always retried.req_optionsrejects:retry,:auth,:base_urland:finch. - Client. New first-class
finch:option for sharing an existing pool (the previousreq_options: [finch: ...]recipe raised). A userconnect_optionskeeps the client'sconnect_timeoutunless it sets its own. Application config values are validated like options. - Fan-out.
evaluate_many/4runs its tasks unlinked under aTask.Supervisorstarted by the newTypeSafeAPI.Application, so a crashing state yields one:unexpectederror instead of killing the caller.:timeoutnow means the per-request timeout exactly as inevaluate/4; the per-state cap is the new:task_timeout;:attempt_timeoutis removed.on_error: :raisealso raises for an invalid question set.questionsaccepts a%TypeSafeAPI.SystemOne.Prepared{}, as doevaluate/4and the newTypeSafeAPI.prepare/1.Preparedcaches the encoded question bytes so a batch encodes the question set once. - State and options. A state that cannot be JSON-encoded, or a charlist, is a
:validationerror.evaluate/4returns{:error, %Error{type: :validation}}for bad per-call options instead of raising;evaluate!/4andevaluate_many/4raise. Models.
Model.release_dateis aDate.t() | nilwithrelease_date_rawandrawalongside; one malformed entry is skipped with a warning instead of failing the list; per-call options are validated;TypeSafeAPI.models!/1added.Usage.total_tokens/1andUsage.add/2added; a missing usage no longer fails decoding.- Telemetry. Failed requests log at
:warning(configurableerror_level);attach_logger/1acceptswarn, rejects unknown levels, and can be re-attached with new levels; the duration metric is adistributionso Prometheus reporters work; token counts are sanitised before they reach handlers;question_countis correct for encoded bodies. - Test stubs.
TypeSafeAPI.Test.stub/2andstub_models/2compose instead of replacing each other;stub_error/4takes options (:headers,:times,:path); confidence is validated so a stub always decodes to the answer you asked for; a missing stub is a 422 error, not a raise. The recorder no longer escapes to the real API on a retried request, records every response with an HTTP status, uses a JSON Lines fixture, and names the closest recording on a mismatch.
Added
- Guides: getting started, System One concepts, configuration and concurrency, errors and
retries, Phoenix LiveDashboard metrics, and Broadway and Oban integration. The README
errors section and the
TypeSafeAPI.Retrymoduledoc now summarise and link to the guides. examples/support_triage, a Mix app that routes support messages with confidence gating. Its test suite runs onTypeSafeAPI.Teststubs with no API key, and CI runs it.TypeSafeAPI.Test.record/2andreplay/2: capture real responses from one live run into a JSON fixture (method, path and body only, never headers) and replay them offline.TypeSafeAPI.Telemetry.metrics/0returningTelemetry.Metricsdefinitions for request count, duration, retries, errors, exceptions and token usage.telemetry_metricsis an optional dependency.- "Why Req" section in
DESIGN.mdwith the runtime dependency table, and a limits table in the cheatsheet and question moduledocs stating which bounds are local policy and which come from the spec. - Documented raw and forward-compatible questions via
TypeSafeAPI.HTTP.post/4for question types this client does not model. - The Livebook walkthrough ships in the package and in the docs, with a "Run in Livebook" badge in the README.
- The Hex description now leads with offline test stubs, concurrent fan-out, and atom-keyed answers.
priv/openapi.json, a vendored snapshot of the upstream OpenAPI spec (API 0.2.0), and a spec-drift test asserting the paths, required fields, and limits the client relies on.DESIGN.mdexplains how to refresh it.TypeSafeAPI.Retry.for_method/2and a "Connection errors and replay" section in theTypeSafeAPI.Retrydocs.- A "Timeouts and connection pools" section in the
TypeSafeAPI.HTTPdocs listing which options select a Finch pool and which do not.
0.1.0-alpha.1 - 2026-09-17
Added
TypeSafe.new/1client struct resolving configuration from options, application config, andTYPESAFE_*environment variables.TypeSafe.evaluate/4andevaluate!/4with typedNoul,Choice, andScorequestions and answers; caller keys (atoms or strings) round-trip withoutString.to_atom/1.TypeSafe.evaluate_many/4concurrent fan-out with ordered results andon_errormodes.TypeSafeAPI.models/1forGET /v1/models.TypeSafe.Retrymirroring the official SDK policy, includingretry-after-ms,Retry-After(seconds or HTTP date), and a total time budget per call.TypeSafe.HTTPraw layer (maps in, maps out) built on Req with the built-inJSONmodule.TypeSafe.Errortyped error values::auth,:validation,:rate_limited,:overloaded,:timeout,:connection,:unexpected.- Telemetry spans
[:typesafe, :request, *]andTypeSafe.Telemetry.attach_logger/1. TypeSafe.Teststubs by question id for downstream test suites.TypeSafe.Answer.gate/2andyes?/2helpers for confidence-gated routing.TypeSafe.FanOut.options_schema/0;TypeSafe.evaluate/4andevaluate_many/4render their full option tables inline in the docs.TypeSafe.Result.request_idandTypeSafe.HTTP.request/5returning aTypeSafe.HTTP.Responsewith status, headers,request_id, and retry count.- Local validation that a Choice has 2 to 255 options (the API's ceiling is 255).
TypeSafe.Model.release_dateparsed as aDateTimewhen the API sends one.- HTTP 400 responses map to
type: :validation; error messages are read from the API'sdetail.messageshape as well as string and list details. TypeSafe.Answer.Score.descriptioncarrying the winning level as written;labelis always a string.TypeSafe.Question.validate!/1for eager, raising validation of questions built at compile time; constructors themselves never validate.TypeSafe.SystemOne.prepare/1returning aTypeSafe.SystemOne.Preparedstruct for reuse across many states.livebooks/live_walkthrough.livemd, a Livebook notebook that runs the README example against the live API and shows the request, raw response, and typed result.