Getting started

September 18, 2026 · View on GitHub

TypeSafe evaluates shared context against your questions and returns typed decisions. The Elixir client uses Mint over HTTP/1 or HTTP/2. It requires Elixir 1.18+ and Erlang/OTP 27+, and uses native JSON for encoding.

Install

Add typesafe_ai to your application's dependencies in mix.exs:

defp deps do
  [{:typesafe_ai, "~> 0.1.1"}]
end

To use the development version from the GitHub repository instead:

{:typesafe_ai, git: "https://github.com/hfiguera/typesafe_ai.git", branch: "main"}

Then run mix deps.get. The package and OTP application are named typesafe_ai; the public module namespace is TypeSafe.

When working on the library alongside another application, you can instead use {:typesafe_ai, path: "../typesafe_ai"}, adjusting the path to your local checkout.

Start a supervised client

Set TYPESAFE_API_KEY in the environment using your usual secret provisioning. Read it in your application and pass it explicitly to the client:

defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      {TypeSafe.Client,
       name: MyApp.TypeSafe,
       api_key: System.fetch_env!("TYPESAFE_API_KEY")}
    ]

    Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
  end
end

Add this child to your existing supervision tree if the application already has one. A new application also needs mod: {MyApp.Application, []} in the list returned by its application/0 function in mix.exs.

The client validates its configuration at startup, but connects on the first evaluation. Successful startup does not verify the key with the service. The library never reads environment variables or macOS Keychain itself. See configuration for release runtime configuration and multiple clients.

Evaluate shared context

With your application running, this makes one live evaluation:

result =
  TypeSafe.system_one(MyApp.TypeSafe,
    state: %{message: "I was charged twice. Please refund the duplicate payment."},
    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?", [
          "Routine: can wait",
          "Needs attention today",
          "Immediate: work is blocked"
        ]),
      "refund" => TypeSafe.noul("Is a refund requested?")
    },
    timeout: 15_000,
    retry: [max_attempts: 1]
  )

case result do
  {:ok, %TypeSafe.Response{answers: answers, model: model, usage: usage}} ->
    %TypeSafe.Answer.Choice{choice: team} = answers["team"]
    %TypeSafe.Answer.Score{score: urgency} = answers["urgency"]
    %TypeSafe.Answer.Noul{noul: refund_probability} = answers["refund"]

    %{team: team, urgency: urgency, refund_probability: refund_probability,
      model: model, usage: usage}

  {:error, %TypeSafe.Error{kind: kind, status: status}} ->
    {:evaluation_failed, kind, status}
end

Results vary with the state, questions, and model. This example disables retries to make the number of attempts explicit; normal clients default to at most three attempts for HTTP 429 and 529 responses. Live evaluations use service quota and may be billable.

Choose the right question type

HelperInputResult
TypeSafe.choice/21–255 named optionsSelected label, full probabilities, confidence
TypeSafe.score/22–10 ordered levelsWeighted zero-based level index, probabilities, legend, confidence
TypeSafe.noul/2A yes/no question, optionally with explicit criteriaProbability of yes from 0 to 1

A three-level Score ranges from 0 to 2 and may fall between levels. Noul returns a probability, not a Boolean, and has no separate confidence field. Confidence is supplied by the service; it is not a measured guarantee of correctness. Set application thresholds using representative evaluation data.

Question IDs, Choice labels, and probability/legend keys remain strings. Use answers["team"], not answers[:team]. Token usage is a map with atom keys :input_tokens and :output_tokens, reported for the successful attempt only.

Supply JSON-compatible input

State must be a string, map, or list. Nested values must be encodable with native JSON. Instructions and criterion descriptions can also be maps or lists; they do not need to be flattened into a text prompt. For example:

TypeSafe.choice(
  %{task: "Choose a team", language: "Use the labels exactly as supplied"},
  %{"billing" => %{handles: ["payments", "refunds"]}, "technical" => nil}
)

Helpers only construct questions. TypeSafe.system_one/2 validates the question set and encodes input before contacting the client. Custom structs need conversion to plain data or a native JSON.Encoder implementation; Jason.Encoder does not apply. Missing inputs and invalid questions return a validation error.

Batch independent questions about the same state in one call, including questions whose answers you might use only after choosing a route. For different states, share a client across bounded tasks; see configuration. For an end-to-end application, see the support triage example.