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.system_one(state, questions, **options)
| Parameter | Type | Required | Description |
|---|---|---|---|
state | Any | Yes | Text, a JSON object, or an array to evaluate. See State. |
questions | Mapping[str, Question] | Yes | 1 to 512 questions, keyed by the name used to identify each answer. Each is a Noul, Choice or Score, or a plain dictionary. See Questions. |
model | str | No | Model for this call. Default: the client's model. |
images | Sequence[ImageInput] | No | A served Drex Max model, or a retired id that resolves to Max v1.1; pass its id as model: up to 8 images that every question sees, in order. See Images. |
retry | RetryPolicy | No | Replaces the client's retry policy for this call. |
timeout | float | httpx.Timeout | No | Replaces the client's timeout for this call. |
extra_headers | Mapping[str, str] | No | Extra headers for this call. |
extra_body | Mapping[str, Any] | No | Extra top-level body fields, merged over the body last, so they win. |
response_model | type[BaseModel] | No | Parse the response into your own Pydantic model instead of SystemOneResponse. See Parse into your own model. |
It returns a SystemOneResponse. A decision is billed per input token and only when it returns 200, so the SDK's retries never bill twice.
from nace_sdk import Choice, NaceClient, Noul, Score
with NaceClient() as client:
result = client.system_one(
state="I was charged twice for my March invoice.",
questions={
"wants_refund": Noul(instructions="Is the customer asking for a refund?"),
"topic": Choice(instructions="Which topic is it?", criteria={"billing": None, "shipping": None, "other": None}),
"urgency": Score(instructions="How urgent is it?", criteria=["low", "medium", "high"]),
},
)
print(result.nouls["wants_refund"].noul)
print(result.choices["topic"].choice, result.choices["topic"].confidence)
print(result.scores["urgency"].score, result.usage.input_tokens, result.request_id)Question types
| Type | Fields | Description |
|---|---|---|
Noul | type ("noul", set for you), instructions: str | None = None, criteria: NoulCriteria | None = None | A yes/no question. Needs instructions, or a criteria that describes an outcome. |
Choice | type ("choice", set for you), criteria: Mapping[str, str | None], instructions: str | None = None | Picks one label. criteria maps each label to a description, or None. At least one label. |
Score | type ("score", set for you), criteria: Sequence[str], instructions: str | None = None | Scores on an ordered rubric, one description per score from zero. At least one entry. |
NoulCriteria | true: str | None, false: str | None (both optional) | A TypedDict describing the yes and no outcomes of a Noul. |
NoulModel, ChoiceModel, ScoreModel | type ("noul", "choice", "score"), plus the same fields | The plain-dictionary forms, as TypedDicts. criteria is required for choice and score. |
These names are type aliases, for annotating your own code:
| Alias | What it means |
|---|---|
QuestionModel | A question written as a plain dictionary: a NoulModel, ChoiceModel or ScoreModel. |
Question | One question, in either form: a Noul, Choice or Score object, or a QuestionModel dictionary. |
Questions | The whole questions argument: a dictionary from each question's name to a Question. |
questions = {
"wants_refund": {"type": "noul", "instructions": "Is the customer asking for a refund?"},
"is_spam": Noul(criteria={"true": "Unsolicited advertising", "false": "A real customer message"}),
}The SDK checks the questions before it sends anything and raises NaceError for no questions or more than 512, a Noul with neither instructions nor a described outcome, a Choice whose criteria isn't a nonempty mapping, a Score with no criteria, and a dictionary without a nonempty type (or, for choice and score, without criteria). An unknown field on a question object raises pydantic.ValidationError. Unset instructions and criteria are left out of the request.
Read the response
SystemOneResponse:
| Member | Type | Description |
|---|---|---|
answers | dict[str, Answer] | Every answer, keyed by question name. |
nouls | dict[str, NoulAnswer] | The yes/no answers. |
choices | dict[str, ChoiceAnswer] | The choice answers. |
scores | dict[str, ScoreAnswer] | The score answers. |
model | str | The model that answered. An alias such as drex-latest resolves to the model it serves. |
usage | Usage | input_tokens (billed), output_tokens, and image_tokens, each int | None. Image tokens are already included in input tokens. |
evaluation_time_ms | float | None | How long the model took, in milliseconds. |
request_id | str | The x-request-id header (else the body's request_id). Quote it in support requests. |
raw_http_response | httpx.Response | The HTTP response, for its status, headers and body. |
request_id and raw_http_response aren't fields, so model_dump() leaves them out. Every response type has them, and a from_http_response(response) class method that builds one from an httpx.Response you fetched yourself, raising the same errors as a call.
| Answer | Fields |
|---|---|
NoulAnswer | type ("noul"), noul: float: the probability of yes, from 0 to 1, and confidence: float | None: the probability of the likelier answer, max(noul, 1 - noul), or None when an older server omits it. |
ChoiceAnswer | type ("choice"), choice: str (one of your labels), confidence: float (0 to 1), probabilities: dict[str, float] keyed by label. |
ScoreAnswer | type ("score"), score: float (expected score, can fall between levels), confidence: float, legend: tuple[str, ...] (your rubric), probabilities: dict[int, float] keyed by integer score. |
Answer is the union of the three, told apart by type. Score probabilities are keyed by int, so read them as result.scores["urgency"].probabilities[2]. legend is always a tuple 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 tuple. The SDK doesn't send that header. An answer of a type this SDK version doesn't know is dropped with a warning on the nace_sdk logger; it is still in raw_http_response. See Reading answers.
Parse into your own model
Subclass SystemOneResponse and declare an answer per question name. The SDK copies each declared answer from answers into its field (answers still has it too), and you keep request_id and raw_http_response:
from nace_sdk import Choice, ChoiceAnswer, NaceClient, Noul, NoulAnswer, SystemOneResponse
class Ticket(SystemOneResponse):
wants_refund: NoulAnswer
topic: ChoiceAnswer
with NaceClient() as client:
ticket = client.system_one(
state="I was charged twice for my March invoice.",
questions={
"wants_refund": Noul(instructions="Is the customer asking for a refund?"),
"topic": Choice(criteria={"billing": None, "shipping": None}),
},
response_model=Ticket,
)
print(ticket.wants_refund.noul, ticket.topic.choice, ticket.request_id)Any other pydantic.BaseModel is validated against the raw JSON body and has no request_id or raw_http_response. A body that doesn't match raises NaceAPIResponseValidationError.
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:
- an
UploadedFilethatclient.documents.uploadreturned (upload withoutworkspace_id), or theWorkspaceFileSourcefor one. It is sent as aworkspace_filereference and 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,...; - the wire's own
{"type": "workspace_file", "workspace_id": ..., "file_id": ...}dictionary.
ImageInput is str | UploadedFile | WorkspaceFileSource | Mapping[str, str]. Any other entry, a string that is neither base64 nor a data URL (such as a file path: pass image_input(path) instead), or a single string in place of the list, raises NaceError before sending. 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.
image_input(content), from nace_sdk:
| Parameter | Type | Required | Description |
|---|---|---|---|
content | str | PathLike | bytes | BinaryIO | UploadedFile | WorkspaceFileSource | Yes | A path, the image's bytes or an open binary file, returned as a data URL typed from its first bytes. An UploadedFile or WorkspaceFileSource returns its workspace_file dictionary. |
It returns a str | dict[str, str] for images. Content that isn't a PNG, JPEG or WebP image, or is over 16 MiB, raises NaceError. An inline image counts toward the 4 MiB request body, base64 expansion included.
from nace_sdk import NaceClient, Noul, image_input
with NaceClient() as client:
receipt = client.documents.upload("receipt.jpg")
result = client.system_one(
state="Image 1 is a scanned receipt. Image 2 is the shop's logo.",
questions={"same_shop": Noul(instructions="Was the receipt in Image 1 issued by the shop whose logo is Image 2?")},
model="drex-max",
images=[receipt, image_input("logo.png")],
)
print(result.nouls["same_shop"].noul, result.usage.image_tokens)OpenAI-compatible decisions
client.decisions.create(*, input, questions, model=None, safety_identifier=None, prompt_cache_key=None, prompt_cache_options=None, retry=None, timeout=None, extra_headers=None, extra_body=None, response_model=None)
| Parameter | Type | Required | Description |
|---|---|---|---|
input | DecisionInput | Yes | A 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. |
questions | Sequence[DecisionQuestionParam] | Yes | 1 to 512 questions, each predicate, choice or score, with an optional name echoed on its answer. |
model | str | No | The model that answers. Default: the client's model. |
safety_identifier | str | No | OpenAI's opaque end-user identifier, up to 128 characters. Accepted for compatibility; Drex neither forwards nor stores it. |
prompt_cache_key | str | No | OpenAI's prompt cache key, up to 64 characters. Accepted for compatibility and ignored: Drex has no prompt cache. |
prompt_cache_options | Mapping[str, Any] | No | OpenAI's prompt cache options: mode implicit or explicit, ttl 30m. Accepted for compatibility and ignored. |
retry | RetryPolicy | No | Replaces the client's retry policy for this call. |
timeout | float | httpx.Timeout | No | Replaces the client's timeout for this call. |
extra_headers | Mapping[str, str] | No | Extra headers for this call. |
extra_body | Mapping[str, Any] | No | Extra top-level body fields, merged over the body last, so they win. |
response_model | type[BaseModel] | No | Parse the response into your own Pydantic model instead of Decision. |
It returns a Decision. This is system_one 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, so a bad question is the server's 422, with the field at fault in the error's param. The same models, funding, limits and billing apply.
from nace_sdk import NaceClient
with NaceClient() as client:
decision = 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"}]},
],
)
wants_refund, refund = decision.answers
print(wants_refund.probability, refund.choice, decision.usage.input_tokens, decision.request_id)The request types are TypedDicts, so the dictionaries an OpenAI client builds type-check as they are:
| Type | Fields |
|---|---|
DecisionTextPart | type: Literal["input_text"], text: str. |
DecisionImagePart | type: Literal["input_image"], image_url: str (a data URL), detail: Literal["low", "high", "auto", "original"] | None (optional, ignored). |
DecisionJsonPart | type: Literal["input_json"], data: dict[str, Any] | list[dict[str, Any]]. Alone in the input it is sent as JSON; next to text it is stringified. |
DecisionMessage | type: Literal["message"] (optional), role: Literal["user"], content: str | list[DecisionInputPart]. |
DecisionChoiceParam | value: str | bool, description: str | None (optional). 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. "True" is a different string. |
DecisionLevelParam | label: str, description: str | None (optional). The description is shown to the model with the label. |
DecisionPredicateParam | type: Literal["predicate"], instructions: str, name: str | None (optional). |
DecisionChoiceQuestionParam | type: Literal["choice"], instructions: str, choices: list[DecisionChoiceParam], name: str | None (optional). |
DecisionScoreQuestionParam | type: Literal["score"], instructions: str, levels: list[DecisionLevelParam] (2 to 10), name: str | None (optional). |
| Alias | What it means |
|---|---|
DecisionInputPart | A DecisionTextPart, DecisionImagePart or DecisionJsonPart. |
DecisionInput | The input argument: a str or a list[DecisionMessage]. |
DecisionQuestionParam | One question: a DecisionPredicateParam, DecisionChoiceQuestionParam or DecisionScoreQuestionParam. |
DecisionAnswer | One answer, told apart by type: a PredicateAnswer, DecisionChoiceAnswer, DecisionScoreAnswer or RefusalAnswer. |
| Type | Fields |
|---|---|
Decision | model: str, answers: list[DecisionAnswer] (in question order), usage: DecisionUsage, plus request_id and raw_http_response. |
PredicateAnswer | type ("predicate"), name: str | None, probability: float: the probability of yes. |
DecisionChoiceAnswer | type ("choice"), name: str | None, choice: str | bool (the option as sent), confidence: float, probabilities: list[ChoiceProbability] in the order sent. |
ChoiceProbability | value: str | bool, probability: float. |
DecisionScoreAnswer | type ("score"), name: str | None, score: float (can fall between levels), confidence: float, probabilities: list[ScoreProbability] in the order sent. |
ScoreProbability | value: int (the level's index), label: str, probability: float. |
RefusalAnswer | type ("refusal"), name: str | None. In OpenAI's contract; Drex's models answer every question today. |
DecisionUsage | input_tokens: int (billed), output_tokens: int, total_tokens: int, input_tokens_details: DecisionInputTokensDetails, output_tokens_details: DecisionOutputTokensDetails. |
DecisionInputTokensDetails | cached_tokens: int, cache_write_tokens: int, always zero. |
DecisionOutputTokensDetails | reasoning_tokens: int, always zero. |
AsyncNaceClient.decisions is an AsyncDecisions with the same create, awaited.
List models
client.models.list(**options) takes retry, timeout and extra_headers, and returns a ListModelsResponse. It works at any balance and is free, so it doubles as a key check.
| Type | Fields |
|---|---|
ListModelsResponse | models: tuple[ModelMetadata, ...], plus request_id and raw_http_response. |
ModelMetadata | name (what model takes), description, release_date (YYYY-MM-DD), alias_for (the model an alias such as drex-latest serves; None for a pinned model). |
with NaceClient() as client:
print([model.name for model in client.models.list().models])See Models.
Python SDK
Call Drex's decision model and run Perception's document tools from Python with nace-sdk — client options, calibrated questions, typed answers, sources, parse, split, classify, extract, ground, uploads, jobs, workspaces, workspace files, storage connections, search, saved schemas, errors, retries and logging.
Sources
Next Page