TypeSafe for Elixir
An Elixir client for the TypeSafe AI System One API.
Evaluate one shared state with Choice, Score, and Noul questions, and receive
typed answers. Uses Mint directly, native JSON, and a supervised connection
owner with bounded concurrency, queueing, deadlines, and retries.
Requires Elixir 1.18+ and Erlang/OTP 27+. Package/application: typesafe_ai;
module namespace: TypeSafe. This independently maintained client is licensed
under MIT.
Start with Getting started, then see configuration and concurrency, errors and retries, telemetry, and the support triage example and evaluation.
Installation and supervision
Add typesafe_ai to your application's dependencies in mix.exs:
{:typesafe_ai, "~> 0.1.0"}
To use the development version from GitHub instead:
{:typesafe_ai, git: "https://github.com/hfiguera/typesafe_ai.git", branch: "main"}
Run mix deps.get after adding the dependency. A TypeSafe API key is required
for live evaluations; supply it through the TYPESAFE_API_KEY environment variable.
Start a client under your application's supervisor:
children = [
{TypeSafe.Client,
name: MyApp.TypeSafe,
api_key: System.fetch_env!("TYPESAFE_API_KEY"),
model: "jev-latest"}
]
Supervisor.start_link(children, strategy: :one_for_one)
The application retrieves the key and passes it explicitly. The library does
not read environment variables or Keychain. Separate clients can use different
keys; use Supervisor.child_spec/2 with distinct :id values when starting
multiple clients under one supervisor. Connections are established lazily.
Evaluate state
{:ok, response} =
TypeSafe.system_one(MyApp.TypeSafe,
state: %{message: "I was charged twice. Please refund me."},
questions: %{
"team" =>
TypeSafe.choice("Which team should handle this?", %{
"billing" => "Payments, invoices, and refunds",
"technical" => "Bugs and integration problems"
}),
"urgency" =>
TypeSafe.score("How urgently should we respond?", [
"Can wait until next week",
"Needs attention today"
]),
"refund" => TypeSafe.noul("Does the customer request a refund?")
},
timeout: 15_000
)
%TypeSafe.Answer.Choice{choice: team, confidence: confidence} = response.answers["team"]
%TypeSafe.Answer.Score{score: urgency} = response.answers["urgency"]
%TypeSafe.Answer.Noul{noul: refund_probability} = response.answers["refund"]
response.model
response.usage # %{input_tokens: ..., output_tokens: ...}
Question IDs, Choice labels, probability keys, and Score legend keys stay strings. No atoms are created from server input. Choice includes a full probability map. Score includes probabilities and a legend; its value ranges from zero to the last level's index and can fall between levels. Noul is a probability from 0 to 1 and has no separate confidence field. Choose application thresholds yourself.
State accepts a string, map, or list of JSON-compatible values. Instructions and
criteria descriptions may be structured maps or lists. Choice permits 1–255
options, including nil descriptions; Score requires 2–10 ordered levels. Noul
optionally accepts %{"true" => "Yes means …", "false" => "No means …"} as its
second argument. Helpers construct questions; system_one/2 validates them.
Custom structs need conversion or a native JSON.Encoder implementation;
Jason.Encoder does not apply.
Batch independent questions about the same state in one evaluation. For different
states, use Task.async_stream/3 with bounded concurrency. Calls return
{:ok, %TypeSafe.Response{}} or {:error, %TypeSafe.Error{}}.
Configuration
| Client option | Default | Meaning |
|---|---|---|
api_key |
Required | Non-empty Bearer credential |
name |
Unnamed | GenServer registration name |
base_url |
https://api.typesafe.ai |
Endpoint root; optional path prefix |
model |
jev-latest |
Model for evaluations |
timeout |
30_000 |
Overall request deadline in milliseconds |
connect_timeout |
5_000 |
Connection establishment timeout in milliseconds |
max_concurrency |
10 |
Maximum concurrent HTTP/2 streams |
max_queue |
100 |
Additional outstanding request slots |
max_response_bytes |
8_388_608 |
Maximum body bytes per response |
protocols |
[:http1, :http2] |
Protocols Mint may negotiate |
transport_opts |
[] |
cacerts, cacertfile, or TLS versions |
retry |
See below | Retry policy struct or keyword list |
Per-request options are state, questions, model, timeout, and retry.
Unknown options are rejected. Endpoint, authentication, and transport limits
belong to the client. TLS verifies certificates and hostnames using OTP's system
CA store by default; there is no option to disable verification. Plain HTTP is
supported for local fixtures or explicitly configured endpoints.
Each client owns one reusable connection. HTTP/1 runs one request at a time;
HTTP/2 respects both the client limit and the peer's stream limit. Outstanding
work, including retry waits, is bounded by the current protocol capacity plus
max_queue. Before negotiation, capacity is conservatively one. Excess work
returns :overloaded immediately.
The deadline starts after input validation/encoding and includes queueing, connection establishment, uploads, response collection, and retry waits. Socket sends have a one-second upper timeout; scheduling or a blocked send can delay delivery of a timeout result. A dead caller releases its slot. HTTP/1 cancellation closes the connection; HTTP/2 cancellation resets only that stream. Subsequent work reconnects as needed. There is no connection pool or WebSocket transport.
Retries and errors
The default policy is:
retry_policy = [
max_attempts: 3,
statuses: [429, 529],
base_delay: 250,
max_delay: 5_000,
retry_transport: false
]
Pass a policy as retry: retry_policy when starting a client or making a call.
Attempts include the initial request and are limited to 1–10. Delays use capped
exponential backoff with full jitter. A valid Retry-After value (seconds or HTTP
date) takes precedence, even above max_delay. If the delay cannot fit within
the remaining deadline, the client returns :timeout immediately.
Use retry: [max_attempts: 1] to disable retries. A per-request policy replaces
the client policy; omitted fields use the policy defaults. Transport failures
after submission are ambiguous: the server may already have processed and billed
the evaluation. Replay requires explicit retry_transport: true. Failure to
establish a connection returns a transport error directly. Permanent HTTP errors
are returned without replay unless their status was explicitly added to the policy.
case TypeSafe.system_one(MyApp.TypeSafe, state: "Hello", questions: %{
"greeting" => TypeSafe.noul("Is this a greeting?")
}) do
{:ok, response} -> response.answers["greeting"].noul
{:error, %TypeSafe.Error{kind: :http, status: 401}} -> :invalid_credentials
{:error, %TypeSafe.Error{kind: :timeout}} -> :deadline_exceeded
{:error, %TypeSafe.Error{kind: kind}} -> {:failed, kind}
end
Error kinds are :configuration, :validation, :transport, :timeout,
:overloaded, :http, :invalid_response, and :unavailable. Errors retain HTTP
status where applicable, but omit server bodies and low-level exception details.
The library does not log keys, request state, or response bodies. Formatted
GenServer status is redacted. As with any BEAM process, privileged debugging
such as :sys.get_state/1 can access its actual memory.
Telemetry
Events use the prefix [:typesafe, :request]:
| Suffix | Measurements | Metadata |
|---|---|---|
:start |
system_time in native units |
request_id reference |
:retry |
delay in ms, attempt |
request_id, HTTP status or nil |
:stop |
duration in ms, attempts; token counts on success |
request_id, outcome |
Accepted logical requests normally have one start and stop, including handled
caller cancellation (:cancelled) and orderly client shutdown (:unavailable).
Abrupt process termination may prevent a stop event. Rejected input and queue
overflow do not emit lifecycle events. Other stop outcomes are :ok or an error
kind. Metadata contains no prompts, bodies, headers, or credentials. Handlers
execute synchronously; keep them fast and nonblocking.
Development
The support triage application
demonstrates a Jido support decision agent using this library as a path dependency.
It offers interactive tickets,
follow-up evaluations, probability distributions, routing history, and batch
reporting. Run mix triage from that directory with TYPESAFE_API_KEY configured.
Only the TypeSafe key is needed; see its
README
for setup and the local Keychain invocation.
The same app includes mix triage.eval for labeled workflow evaluations with
separate development/held-out datasets and saved quality, latency, and token
reports. See the
evaluation guide
for dataset labels, metric definitions, and limitations.
Clone the repository to work on the library or run the example:
git clone https://github.com/hfiguera/typesafe_ai.git
cd typesafe_ai
The pinned development runtime is in .tool-versions. mise install can install
it. Run commands sharing a build directory sequentially:
mix deps.get
mix test
mix quality
mix docs --warnings-as-errors
mix quality runs formatting, compilation with warnings as errors, Credo with
ExSlop, ExDNA, Credence with strict Unicode assumptions, and Dialyzer. All tests
are offline, using local TCP/TLS and HTTP/2 fixtures. Neither Bypass nor Mimic is
needed. CI tests the minimum runtime on Linux and the development runtime on
Linux and macOS. Quality checks run on the development runtime.
An opt-in smoke script makes one real, billable evaluation with all three question types and no retries:
# macOS Keychain generic password: service typesafe_ai, account api_key
mix run scripts/smoke.exs --keychain
# Or a key provided by the environment
mix run scripts/smoke.exs --env
The Keychain command's complete stdout is captured in memory and only its final newline is removed, so long keys are not truncated. The script prints counts, never the key. Do not run the retrieval command on its own in a recorded terminal.
The source checkout contains
design decisions and the
upstream API reference, also downloaded in
docs/api.md.
Report bugs and request features through GitHub Issues. Include the Elixir and OTP versions, a minimal reproduction, and the error kind/status when relevant. Omit API keys and private request data.
License
MIT. See the license for the full text.