ExTypesafe

Hex.pm Documentation

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

Full TypeSafe API reference: https://docs.typesafe.ai/api

License

MIT