Drex

Ask questions

Use client.models.list() with the same API key and base URL to check which models are available before selecting one.

client.systemOne(request, options?) returns APIPromise<SystemOneResult<Q>>, where Q is the type of your questions.

ParameterTypeRequiredDescription
request.stateEntryTypeYesAny JSON value to evaluate: text, a number, a boolean, an object, an array or null. See State.
request.questionsQuestionsYes1 to 512 questions, each keyed by the name its answer comes back under. Build them with noul(), choice() and score(), or write plain objects. See Questions.
request.modelstringNoThe model for this call. Default: the client's defaultModel.
request.imagesreadonly ImageInput[]NoA served Drex Max model, or a retired id that resolves to Max v1.1; set model to its id: up to 8 images that every question sees, in order. See Images.
optionsRequestOptionsNoThe second argument, not a field of request: signal, timeout, retry and headers for this call. See Per-call options.

request is a SystemOneRequest<Q>. Any other field on it is sent in the body as it is.

decide.ts
import { choice, NaceClient, noul, score } from "nace-sdk";

const client = new NaceClient();

const { answers, usage, request_id } = await client.systemOne({
  state: "I was charged twice for my March invoice.",
  questions: {
    wants_refund: noul("Is the customer asking for a refund?"),
    topic: choice("Which topic is it?", { billing: null, shipping: null, other: null }),
    urgency: score("How urgent is it?", ["low", "medium", "high"]),
  },
});

answers.wants_refund.noul; // number
answers.topic.choice; // "billing" | "shipping" | "other"
answers.urgency.probabilities["2"]; // probability of "high"
console.log(usage.input_tokens, request_id);

The answer types come from your questions, so a misspelled question name or label is a compile error. A decision is billed per input token, and only when it returns 200, so the client's retries never bill twice.

systemOne checks the questions before it sends anything, and throws a NaceError for:

  • no questions, or more than 512;
  • a noul with neither instructions nor a non-empty true or false criterion;
  • a choice whose criteria aren't a non-empty map of labels;
  • a score whose criteria aren't a non-empty list.

An instructions or criteria left null is dropped from the body. Drex reads null there the same as no key.

Question helpers

HelperReturnsDescription
noul(instructions?, criteria?)NoulQuestionA yes/no question. instructions defaults to null. criteria is { true?, false? }, a description of each outcome.
choice(instructions, criteria)ChoiceQuestion<T>Picks one label. criteria maps each label to a description, or to null. Throws a NaceError when criteria is a list.
score(instructions, criteria)ScoreQuestion<T>Rates on an ordered rubric: criteria is a list of descriptions, one per score from zero. Throws a NaceError when criteria is a map.

instructions is a string or null. Plain objects work too, such as { type: "noul", instructions: "Is it urgent?" }.

Answer types

SystemOneResult<Q> is what systemOne resolves to:

FieldTypeDescription
modelstringThe model version that answered.
answers{ [K in keyof Q]: ResultFor<Q[K]> }One answer per question, under the question's name.
usageUsageinput_tokens, which Drex bills, output_tokens, and optional image_tokens (number), already included in input tokens.
evaluation_time_msnumberHow long the model took.
request_idstringDrex's request id. Quote it in support requests.
TypeWhat it holds
NoulResponsetype: "noul", noul, the probability of yes, from 0 to 1, and optional confidence, the probability of the likelier answer (undefined when an older server omits it).
ChoiceResponse<T>type: "choice", choice (one of your labels), confidence and probabilities, keyed by label.
ScoreResponse<T>type: "score", score (the expected score, which can fall between levels), confidence, legend (your rubric, as a list) and probabilities, keyed by score.
ResultFor<T>The answer type for question type T: NoulResponse, ChoiceResponse or ScoreResponse, keeping your labels.
ScoreOf<T>The score keys of rubric T: "0" | "1" | "2" for a three-entry rubric written in the call, number for one whose length isn't known.
Usageinput_tokens, output_tokens, and optional image_tokens (number).

legend is always a list of labels, whichever shape the server sent: a legend keyed by level, which Drex sends when a request asks for X-Nace-Legend: object or comes from a TypeSafe client, is read as the same list. The SDK doesn't send that header.

See Reading answers for what each number means.

Request types

TypeWhat it is
JsonValueAny JSON value.
EntryTypeThe type of state: a JsonValue.
DescriptionA criterion's description: string | null.
NoulQuestion{ type: "noul", instructions?, criteria?: { true?, false? } | null }.
ChoiceCriteriaLabels mapped to a Description.
ChoiceQuestion<T>{ type: "choice", instructions?, criteria: T }.
ScoreCriteriareadonly [string, ...string[]]: at least one description.
ScoreQuestion<T>{ type: "score", instructions?, criteria: T }.
QuestionNoulQuestion | ChoiceQuestion | ScoreQuestion.
QuestionsQuestions keyed by name.
SystemOneRequest<Q>systemOne's first argument: state, questions, optional model and optional images.
SystemOneRequestPayloadThe body sent to POST /v1/systemone: a SystemOneRequest with model set.
ImageInputOne images entry: string | WorkspaceFileSource.

Images

The served Drex Max models read images. Retired ids that resolve to Max v1.1 accept images too. Pass them in images and refer to them as "Image 1", "Image 2" and so on in the state or the instructions; media inside state or questions returns 422. Each entry, an ImageInput, is one of:

  • a WorkspaceFileSource for a file you uploaded with documents.upload (upload without workspace_id). It doesn't count toward the 4 MiB request body, so use it for anything but small images;
  • a standard base64 string, or a data URL such as data:image/jpeg;base64,....

A file in one of your Workspaces or in another account's returns 422, and so does images with any model that does not support images. Images are PNG, JPEG or WebP, up to 8 per request, 16 MiB each and 32 MiB in all. Their tokens are in usage.image_tokens, already counted in input_tokens.

HelperReturnsDescription
imageInput(content)ImageInputcontent is a Uint8Array, an ArrayBuffer or an UploadedFile. An UploadedFile returns its WorkspaceFileSource, as workspaceFile(file) does. Bytes return a data URL typed from their first bytes; they count toward the 4 MiB request body, base64 expansion included. Throws a NaceError for bytes that aren't a PNG, JPEG or WebP image, or are over 16 MiB.
drex-max.ts
import { readFile } from "node:fs/promises";
import { imageInput, NaceClient, noul } from "nace-sdk";

const client = new NaceClient();

const receipt = await client.documents.upload(await readFile("receipt.jpg"), { file_name: "receipt.jpg" });
const { answers, usage } = await client.systemOne({
  model: "drex-max",
  state: "Image 1 is a scanned receipt. Image 2 is the shop's logo.",
  images: [imageInput(receipt), imageInput(await readFile("logo.png"))],
  questions: {
    same_shop: noul("Was the receipt in Image 1 issued by the shop whose logo is Image 2?"),
  },
});

console.log(answers.same_shop.noul, usage.image_tokens);

Read the raw response

Most methods return an APIPromise<T>. Await it for the parsed body, or call one of its methods:

MethodReturnsDescription
withResponse()Promise<WithResponse<T>>{ data, response, requestId }: the parsed body, the Response (its body already read) and the x-request-id header.
asResponse()Promise<Response>The Response with its body unread. Read the body yourself, and don't also await the parsed result. A non-2xx answer still rejects with an APIError.
map(fn)APIPromise<U>A new APIPromise of fn(data), sharing the response and its single parse.
const { data, response, requestId } = await client
  .systemOne({ state: "Refund my order", questions: { refund: noul("Is it a refund request?") } })
  .withResponse();

console.log(response.status, requestId, data.answers.refund.noul);

systemOne, models.list, the document creates, createUploadGrant, createUploadSession, getUploadSession, deleteFile, jobs.get, jobs.list, jobs.getRequest, jobs.rows, jobs.fileLink and the extractionSchemas reads and creates return an APIPromise. documents.upload, uploadPart, completeUploadSession, abortUploadSession, jobs.wait, jobs.delete, jobs.fetchFile, jobs.download and extractionSchemas.delete return a plain Promise, and the iter, iterVersions and events methods are async generators. Every workspaces, workspaces.files and search method returns an APIPromise too, except workspaces.iterAll, an async generator.

OpenAI-compatible decisions

client.decisions.create(params, options?) returns APIPromise<Decision>. It asks the same questions as systemOne does, in the wire shape of OpenAI's Decisions API, which POST /v1/decisions serves. Answers come back in question order rather than by name, and nothing is checked before sending: the server's 422 names the field at fault in error.param, read as the error's param.

ParameterTypeRequiredDescription
params.inputDecisionInputYesA string, or user messages whose content is a string or input_text, input_image and input_json parts. Text parts join into the state; images are data URLs, up to 8, on the served Drex Max models and retired ids that resolve to Max v1.1.
params.questionsDecisionQuestion[]Yes1 to 512 questions, each predicate, choice or score, with an optional name echoed on its answer.
params.modelstringNoThe model for this call. Default: the client's defaultModel.
params.safety_identifierstring | nullNoOpenAI's opaque end-user identifier, up to 128 characters. Accepted for compatibility; Drex neither forwards nor stores it.
params.prompt_cache_keystring | nullNoOpenAI's prompt cache key, up to 64 characters. Accepted for compatibility and ignored: Drex has no prompt cache.
params.prompt_cache_options{ mode?, ttl? } | nullNoOpenAI's prompt cache options: mode implicit or explicit, ttl 30m. Accepted for compatibility and ignored.
optionsRequestOptionsNosignal, timeout, retry and headers for this call. See Per-call options.

params is a DecisionCreateParams.

decisions.ts
const { answers } = await client.decisions.create({
  input: "I was charged twice for my March invoice.",
  questions: [
    { type: "predicate", name: "wants_refund", instructions: "Is the customer asking for a refund?" },
    { type: "choice", name: "refund", instructions: "Refund?", choices: [{ value: true }, { value: false }, { value: "partial" }] },
  ],
});

const refund = answers[1];
if (refund?.type === "choice") refund.choice; // true, false or "partial", as sent
TypeWhat it is
DecisionInputstring | DecisionMessage[].
DecisionMessage{ type?: "message", role: "user", content: string | DecisionInputPart[] }.
DecisionInputPart{ type: "input_text", text } | { type: "input_image", image_url, detail? } | { type: "input_json", data }: text, a data URL image (detail is ignored) or an object or an array of objects.
DecisionQuestion{ type: "predicate", name?, instructions } | { type: "choice", name?, instructions, choices } | { type: "score", name?, instructions, levels }, with 2 to 10 levels.
DecisionChoice{ value: string | boolean, description? }. A boolean is the lowercase label true or false; a string equal to that label ("true") in the same question is a 422 whose param is the later choice, such as questions[0].choices[1].value.
DecisionLevel{ label, description? }. The description is shown to the model with the label.
DecisionCreateParamscreate's argument: input, questions, optional model, safety_identifier, prompt_cache_key and prompt_cache_options.
Decisionmodel, answers (in question order), usage and request_id.
DecisionAnswer{ type: "predicate", name, probability } | { type: "choice", name, choice, confidence, probabilities } | { type: "score", name, score, confidence, probabilities } | { type: "refusal", name }. Choice values come back as sent; a score probability is { value, label, probability } with value the level's index; Drex's models never send a refusal today.
DecisionUsageinput_tokens (billed), output_tokens, total_tokens, and input_tokens_details and output_tokens_details, always zero.

List models

client.models.list(options?) returns APIPromise<ModelCard[]>, the models your key can use. It's free and works at any balance, so it doubles as a key check.

FieldTypeDescription
namestringThe value to pass as model.
descriptionstringWhat the model is.
release_datestringWhen the version was released.
alias_forstring | nullThe version an alias such as drex-latest points to, or the version that now serves a retired one. null for a pinned version.
for (const model of await client.models.list()) {
  console.log(model.name, model.alias_for);
}

See Models.

On this page