ExTypesafe
Elixir client for the TypeSafe AI API.
TypeSafe evaluates typed questions against a state and returns structured answers your code can branch on directly — no text parsing required. Three question primitives, mixable in a single call:
| Primitive | Ask | Returns |
|---|---|---|
| Noul | Yes/no question | Float 0–1 (probability of "yes") |
| Choice | Pick one from a list you define | Chosen option + full probability distribution |
| Score | Rate on a rubric you define | Probability-weighted float across your levels |
Installation
Add ex_typesafe to your dependencies in mix.exs:
def deps do
[
{:ex_typesafe, "~> 0.1"}
]
end
Configuration
Set your API key via environment variable (recommended):
export TYPESAFE_API_KEY="ts-..."
Or pass it directly when creating a client:
client = ExTypesafe.Client.new(api_key: "ts-...")
All options
| Option | Env Variable | Default | Description |
|---|---|---|---|
:api_key |
TYPESAFE_API_KEY |
— | Required. Your TypeSafe API key. |
:base_url |
TYPESAFE_BASE_URL |
https://api.typesafe.ai |
API root URL. |
:model |
TYPESAFE_DEFAULT_MODEL |
jev-latest |
Default model for requests. |
:max_retries |
— | 3 |
Retry attempts on 429/529 responses. |
:retry_delay_ms |
— | 500 |
Initial retry delay (doubles each attempt). |
Usage
# 1. Create a client (reads TYPESAFE_API_KEY from env)
client = ExTypesafe.Client.new()
# 2. Define typed questions
questions = %{
is_urgent: ExTypesafe.Question.noul("Does this message convey urgency?"),
department: ExTypesafe.Question.choice(
"Which team should handle this?",
%{
billing: "Payments, invoices, and refunds",
technical: "Bugs, outages, and integrations",
sales: "Pricing, upgrades, and new accounts"
}
),
frustration: ExTypesafe.Question.score(
"How frustrated does the customer seem?",
["Calm and polite", "Mildly frustrated", "Very angry"]
)
}
# 3. Evaluate against a state
state = "Hi, my payouts have been failing for 3 days. I'm losing sales. Please help ASAP!"
case ExTypesafe.system_one(client, state, questions) do
{:ok, response} ->
IO.inspect(response.answers["is_urgent"].noul) # => 0.97
IO.inspect(response.answers["department"].choice) # => "billing"
IO.inspect(response.answers["frustration"].score) # => 1.92
IO.inspect(response.usage.input_tokens) # => 418
{:error, %ExTypesafe.Error{status: 429}} ->
# Retried automatically; this means retries were exhausted
:backoff
{:error, error} ->
IO.inspect(error)
end
Structured instructions
Questions can reference data in instructions or state using structured maps:
questions = %{
same_person: ExTypesafe.Question.noul(%{
candidate: %{name: "Jane Doe", location: "Austin, TX"},
question: "Is the resume for the same person as `candidate`?"
})
}
Per-request model override
ExTypesafe.system_one(client, state, questions, model: "jev-latest")
Retries
429 Too Many Requests and 529 Overloaded responses are retried automatically with
exponential backoff. Configure with :max_retries and :retry_delay_ms on the client.
Testing
Use Req.Test to stub HTTP calls without hitting the network:
defmodule MyApp.ClassifierTest do
use ExUnit.Case, async: true
setup do
client = ExTypesafe.Client.new(
api_key: "ts-test",
plug: {Req.Test, __MODULE__}
)
%{client: client}
end
test "classifies urgent tickets", %{client: client} do
Req.Test.stub(__MODULE__, fn conn ->
body = Jason.encode!(%{
"model" => "jev-1.13.0",
"answers" => %{"is_urgent" => %{"type" => "noul", "noul" => 0.95}},
"usage" => %{"input_tokens" => 296, "output_tokens" => 20}
})
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(200, body)
end)
{:ok, response} = ExTypesafe.system_one(client, "Help! Urgent!", %{
is_urgent: ExTypesafe.Question.noul("Urgent?")
})
assert response.answers["is_urgent"].noul > 0.8
end
end
API Reference
ExTypesafe— Main entry point:system_one/4ExTypesafe.Client— Client configurationExTypesafe.Question— Question helpers:noul/2,choice/2,score/2ExTypesafe.Response— Response and answer structsExTypesafe.Error— Error struct
Full TypeSafe API reference: https://docs.typesafe.ai/api
License
MIT