jev_api

A sans-io client for TypeSafe AI’s System One API and its Jev model.

Jev is a decision model: it evaluates one piece of state against typed questions and returns probabilities, choices and scores instead of generated text. See jev_api/question for the question types.

“Sans-io” means this library never performs network I/O itself. evaluate_request builds a gleam/http/request.Request(String) that you send with whichever HTTP client fits your target (gleam_httpc on Erlang, gleam_fetch on JavaScript, or anything else), and evaluate_response decodes the gleam/http/response.Response(String) that comes back.

import gleam/httpc
import gleam/json
import jev_api
import jev_api/question

let client = jev_api.new(api_key)
let req =
  jev_api.evaluate_request(
    client,
    state: json.string("My card was charged twice. Fix it now."),
    questions: [
      #("urgent", question.noul("Does this message express urgency?")),
      #("team", question.choice("Which team should handle this?", ["billing", "technical", "other"])),
    ],
  )
let assert Ok(res) = httpc.send(req)
let assert Ok(evaluation) = jev_api.evaluate_response(res)

Types

The answer to one question, matching the question’s type.

pub type Answer {
  NoulAnswer(probability: Float)
  ChoiceAnswer(
    choice: String,
    probabilities: dict.Dict(String, Float),
    confidence: Float,
  )
  ScoreAnswer(
    score: Float,
    levels: List(Level),
    confidence: Float,
  )
}

Constructors

  • NoulAnswer(probability: Float)

    The probability, from 0.0 to 1.0, that the answer to a Noul question is “yes”. Values near 0.5 mean Jev is unsure.

  • ChoiceAnswer(
      choice: String,
      probabilities: dict.Dict(String, Float),
      confidence: Float,
    )

    The option key Jev picked for a Choice question, the probability of every option, and how peaked that distribution is.

  • ScoreAnswer(score: Float, levels: List(Level), confidence: Float)

    Where on the scale of a Score question the state falls. score is a position on the scale and may sit between two levels; levels carries the probability of each level in order.

What every request needs: the API key, the model to ask, and the base URL.

pub opaque type Client

Why a response could not be turned into a result.

pub type Error {
  AuthenticationError(message: String)
  ValidationError(problems: List(ValidationProblem))
  RateLimited(message: String, retry_after: option.Option(Int))
  Overloaded(message: String)
  ApiError(status: Int, error_type: String, message: String)
  UnexpectedResponse(status: Int, body: String)
  MalformedResponse(
    status: Int,
    body: String,
    error: json.DecodeError,
  )
}

Constructors

  • AuthenticationError(message: String)

    The API key is missing, invalid or not allowed to do this (HTTP 401 or 403, or the API’s authentication_error).

  • ValidationError(problems: List(ValidationProblem))

    The request body failed validation (HTTP 422): a null state, no questions, an unknown question type, and so on.

  • RateLimited(message: String, retry_after: option.Option(Int))

    Too many requests (HTTP 429). Back off, honouring retry_after seconds when the API sends a retry-after header.

  • Overloaded(message: String)

    The service is temporarily overloaded (HTTP 529). Retry after a delay.

  • ApiError(status: Int, error_type: String, message: String)

    Any other error the API described with its error_type/message envelope, for example api_usage_error for an unknown model.

  • UnexpectedResponse(status: Int, body: String)

    A non-success status with a body this library does not recognise, such as a 404 or an HTML page from a proxy.

  • MalformedResponse(
      status: Int,
      body: String,
      error: json.DecodeError,
    )

    A success status whose body did not decode as expected.

Everything Jev said about one request.

pub type Evaluation {
  Evaluation(
    model: String,
    answers: dict.Dict(String, Answer),
    usage: Usage,
    request_id: option.Option(String),
  )
}

Constructors

  • Evaluation(
      model: String,
      answers: dict.Dict(String, Answer),
      usage: Usage,
      request_id: option.Option(String),
    )

    Arguments

    model

    The concrete model that answered, e.g. "jev-1.13.0" for the jev-latest alias.

    answers

    One answer per question, under the key the question was asked with.

    request_id

    TypeSafe’s id for the request, from the x-typesafe-request-id header.

One level of a Score question’s scale as reported in an answer.

pub type Level {
  Level(index: Int, label: dynamic.Dynamic, probability: Float)
}

Constructors

  • Level(index: Int, label: dynamic.Dynamic, probability: Float)

    Arguments

    index

    The level’s position on the scale, starting at 0.

    label

    The level as the API echoed it back from the question’s criteria: a string for a string level, or the object you sent for a structured one. Decode it with gleam/dynamic/decode if you need it.

A model the account can ask for by name.

pub type ModelCard {
  ModelCard(
    name: String,
    description: String,
    release_date: option.Option(String),
  )
}

Constructors

  • ModelCard(
      name: String,
      description: String,
      release_date: option.Option(String),
    )

Token accounting for a request. Only input tokens are billed.

pub type Usage {
  Usage(input_tokens: Int, output_tokens: Int)
}

Constructors

  • Usage(input_tokens: Int, output_tokens: Int)

One thing wrong with a request body, as reported in a ValidationError.

pub type ValidationProblem {
  ValidationProblem(
    location: List(String),
    message: String,
    kind: String,
  )
}

Constructors

  • ValidationProblem(
      location: List(String),
      message: String,
      kind: String,
    )

    Arguments

    location

    Path to the offending field, e.g. ["body", "questions", "urgent"].

    message

    Human-readable description.

    kind

    The validator’s tag for the problem, e.g. "missing".

Values

pub fn answer(
  evaluation: Evaluation,
  key: String,
) -> Result(Answer, Nil)

Looks up the answer to the question asked under key.

pub const default_base_url: String

Where the hosted API lives. Override it with with_base_url to go through a proxy or a mock server.

pub const default_model: String

The model alias every request uses unless with_model says otherwise.

pub fn evaluate_request(
  client: Client,
  state state: json.Json,
  questions questions: List(#(String, question.Question)),
) -> request.Request(String)

Builds the request that evaluates questions against state. Send it with any HTTP client and hand the response to evaluate_response.

state is what Jev reasons about: a string for a single piece of text, an object so each part of the context has a name (recommended), or an array of messages or records. The API rejects null and an empty set of questions with a ValidationError.

pub fn evaluate_response(
  response res: response.Response(String),
) -> Result(Evaluation, Error)

Decodes the response to an evaluate_request.

pub fn model(client: Client) -> String

The model this client asks.

pub fn models_request(client: Client) -> request.Request(String)

Builds the request that lists the models available to the account.

pub fn models_response(
  response res: response.Response(String),
) -> Result(List(ModelCard), Error)

Decodes the response to a models_request.

pub fn new(api_key: String) -> Client

A client for the hosted API using default_model.

pub fn request_id(
  response res: response.Response(body),
) -> option.Option(String)

TypeSafe’s id for the request that produced a response, if present. Available for error responses too, unlike Evaluation.request_id.

pub const request_id_header: String

The response header that carries TypeSafe’s id for a request. Quote it when reporting a problem to TypeSafe.

pub fn with_base_url(
  client: Client,
  url: String,
) -> Result(Client, Nil)

Sends requests somewhere other than default_base_url. The URL needs a scheme and a host; a path prefix is kept (https://proxy.internal/typesafe yields .../typesafe/v1/systemone). Fails if the URL cannot be parsed.

pub fn with_model(client: Client, model: String) -> Client

Asks a different model, for example "jev-preview". models_request lists what your account can use.

Search Document