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:

PrimitiveAskReturns
NoulYes/no questionFloat 0–1 (probability of "yes")
ChoicePick one from a list you defineChosen option + full probability distribution
ScoreRate on a rubric you defineProbability-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

OptionEnv VariableDefaultDescription
:api_keyTYPESAFE_API_KEY—Required. Your TypeSafe API key.
:base_urlTYPESAFE_BASE_URLhttps://api.typesafe.aiAPI root URL.
:modelTYPESAFE_DEFAULT_MODELjev-latestDefault model for requests.
:max_retries—3Retry attempts on 429/529 responses.
:retry_delay_ms—500Initial 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