Elixir client for System One decision models: TypeSafe Jev, Cloudflare Clef, OpenRouter's decisions endpoint, and anything self-hosted that speaks the /v1/systemone wire format (for example CLM).

You send a state (any JSON) and a map of typed questions; you get back typed answers with their full probability distributions, never collapsed to a single pick. That is what a router or a gate needs: top-k over a Choice, a threshold on a Noul, a level cut on a Score.

  • One behaviour, SystemOneClient, with evaluate(state, questions, opts).
  • SystemOneClient.HTTP for real calls; SystemOneClient.Stub for tests, no network.
  • Four providers behind one option; url: and model: override any of them.
  • Retries on 429, 529, 502, 503, 504 and transport errors, honouring Retry-After.
  • Never raises for network conditions; tagged error tuples at the boundary.
  • Telemetry without leaking state, questions, or keys.
  • Depends on req, jason, telemetry only. Apache 2.0.

Install

def deps do
  [{:system_one_client, "~> 0.1"}]
end

Use

questions = %{
  "needs_tool" => %{
    "type" => "noul",
    "instructions" => "Does the next step need one of the actions in `available_actions`?"
  },
  "tool" => %{
    "type" => "choice",
    "instructions" => "Which action should run next to make progress on `request`?",
    "criteria" => %{"add" => "Adds two numbers", "gcd" => "Greatest common divisor", "none" => "No action fits"}
  },
  "depth" => %{
    "type" => "score",
    "instructions" => "How much reasoning does `request` need?",
    "criteria" => ["One direct step", "A few dependent steps", "A long chain or a known trap"]
  }
}

state = %{request: "what is the gcd of 1071 and 462", available_actions: ["add", "gcd"]}

{:ok, answers, meta} = SystemOneClient.evaluate(state, questions)

answers["tool"]
#=> %SystemOneClient.Answer.Choice{choice: "gcd", confidence: 0.97,
#     probabilities: %{"gcd" => 0.97, "add" => 0.02, "none" => 0.01}}
answers["needs_tool"]   #=> %SystemOneClient.Answer.Noul{noul: 0.96}
answers["depth"]        #=> %SystemOneClient.Answer.Score{score: 0.1, probabilities: %{...}, confidence: 0.9, legend: ...}
meta                    #=> %{model: "jev-1.13.0", usage: %{...}, latency_ms: 134, provider: :typesafe}

Question maps are the API's wire format verbatim; this library adds no schema of its own. See TypeSafe's primitives for noul, choice and score.

Providers

provider:EndpointKeyDefault model
:typesafe (default)https://api.typesafe.ai/v1/systemoneapi_key: or TYPESAFE_API_KEYjev-latest
:cloudflarehttps://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/@cf/cloudflare/clefapi_key: or CLOUDFLARE_API_TOKEN; account_id: or CLOUDFLARE_ACCOUNT_IDclef (model: "clef-flash" for the 9B)
:openrouterhttps://openrouter.ai/api/alpha/decisionsapi_key: or OPENROUTER_API_KEYtypesafe/jev-1.13
:compatibleurl: (required)optionalnone
SystemOneClient.evaluate(state, questions, provider: :cloudflare, model: "clef-flash")
SystemOneClient.evaluate(state, questions, provider: :compatible, url: "http://localhost:8700/v1/systemone")

Cloudflare's result envelope is unwrapped for you; a failed envelope returns {:error, {:provider_error, errors}}.

Options

OptionDefaultMeaning
provider:typesafesee above
api_key, account_id, url, modelprovider defaultsexplicit values always win
receive_timeout2_000 msper attempt
max_retries2on 429, 529, 502, 503, 504 and transport errors
retry_delay_ms250base of the exponential backoff; Retry-After / retry-after-ms win when present
clientapp env :client, else HTTPimplementation module (use SystemOneClient.Stub in tests)
plugnoneReq plug, for Req.Test in your own tests
envSystem.get_env()replace the environment (tests)

Errors are tagged tuples:

{:error, :missing_api_key | :missing_account_id | :missing_url | {:unknown_provider, p}}
{:error, {:invalid_question, id, reason}}
{:error, {:http, status, body} | {:transport, reason}}
{:error, {:malformed_answers, reason} | {:provider_error, errors}}

Testing your code

# config/test.exs
config :system_one_client, client: SystemOneClient.Stub

# in a test
alias SystemOneClient.Answer.{Choice, Noul}

{:ok, answers, _} =
  SystemOneClient.evaluate(state, questions,
    answers: %{
      "needs_tool" => %Noul{noul: 0.9},
      "tool" => %Choice{choice: "gcd", probabilities: %{"gcd" => 0.9, "add" => 0.1}, confidence: 0.9}
    })

answers: takes a map (structs or raw API maps) or a fn state, questions -> map | {:error, _} end. For HTTP-level tests pass plug: {Req.Test, MyTest} and stub with Req.Test.stub/2.

Telemetry

[:system_one_client, :request, :start | :stop | :exception] via :telemetry.span/3. Metadata: provider, model, question_count; on :stop also status (:ok | :error), latency_ms, and error (a kind such as {:http, 429} or :transport). State, question text, bodies and keys are never included.

Origin

Extracted from an experiment that used Jev as a tool and model router inside a Jido AI agent; the experiment, its benchmarks and the transformer that consumes this client live on schainks/jido, branch experiment/jev-tool-routing.

Running the tests

With Elixir installed: mix test. Without: ./run.sh test runs them in the hexpm/elixir:1.20.4 Docker image.