Drex guides
Migrate from TypeSafe
Point the TypeSafe SDK at Drex, then fix the requests Drex rejects on purpose.
Drex is wire-compatible with TypeSafe's Jev. The @typesafe-ai/sdk package works against https://console.nace.ai without a fork. This guide covers the configuration change, the requests that Drex rejects, and a check that the switch worked.
Or switch to the Nace SDK
Moving to the TypeScript or Python Nace SDK is mostly a rename. systemOne / system_one, the question helpers, the answer types, RetryPolicy, the logging options and the error classes keep their arguments:
| TypeSafe | Drex |
|---|---|
@typesafe-ai/sdk, typesafe-sdk | nace-sdk on npm and PyPI |
TypeSafeClient, AsyncTypeSafeClient | NaceClient, AsyncNaceClient |
TYPESAFE_API_KEY, TYPESAFE_BASE_URL, TYPESAFE_DEFAULT_MODEL, TYPESAFE_LOG_LEVEL | NACE_API_KEY, NACE_BASE_URL, NACE_DEFAULT_DECISION_MODEL, NACE_LOG_LEVEL |
TypeSafeError | NaceError |
TypeSafe*Error in Python, such as TypeSafeRateLimitError | Drex*Error, such as NaceRateLimitError |
What changes besides the names:
- Defaults:
https://console.nace.ai,drex-latestand a 60-second timeout, so none needs setting. - Errors: the Nace SDKs add classes for the statuses Drex adds. See how errors work in the Python and TypeScript SDKs.
- Retries: The total retry budget is disabled by default:
timeoutMs: nullin TypeScript andRetryPolicy.timeout=Nonein Python. Set an explicit budget to limit when retries may start. See Errors and retries. - Perception: the Nace SDKs also cover the Perception routes.
- Workspaces: the Nace SDKs also cover workspaces: create them, upload files, and search or ask questions about them.
The rest of this guide is for code that keeps the TypeSafe SDK. Tools that already integrate with TypeSafe work the same way, with a base URL, your Drex key and an explicit model: see DSPy, MLflow, Opik and Weave.
Change three environment variables
The SDK reads its base URL, key, and default model from the environment when the constructor does not set them.
TYPESAFE_BASE_URL=https://console.nace.ai
TYPESAFE_API_KEY=nace_sk_...
TYPESAFE_DEFAULT_MODEL=drex-v2.0If your code sets these in the constructor instead, change them there. Explicit options take precedence over environment variables.
import { TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient({
apiKey: process.env.TYPESAFE_API_KEY,
});import { TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient({
baseURL: "https://console.nace.ai",
apiKey: process.env.NACE_API_KEY,
defaultModel: "drex-latest",
timeout: 60_000,
});Set defaultModel in one of the two places. Without it, the SDK sends jev-latest, and Drex returns 422. The timeout line is explained under Raise the SDK timeout.
Verify the switch
List the models. The SDK unwraps the { "models": [...] } envelope and returns the array.
const models = await client.models.list();
console.log(models.map((m) => m.name));The list shows the versions, aliases and retired ids available to your key in the target environment. See Models for the current catalog and retirement mappings. GET /v1/models needs a valid key but works with zero balance, so this check does not spend anything. A 401 here means the key is wrong. See Authentication.
What changes
Work through this list before you send production traffic. Each item is a request that TypeSafe accepted and Drex rejects, or a behavior that differs.
Send a drex-* model name
Drex accepts pinned versions such as drex-v2.0, the drex-latest alias, retired ids, and the legacy nacedm-latest name. Anything else returns 422, including a drex-* name that isn't listed in Models, so a half-migrated client fails on the first call instead of silently using the wrong model. A request with "model": "jev-latest" returns an issue whose message starts with unknown model "jev-latest"; use and then suggests the public active model ids and aliases in that environment. These suggestions omit retired ids and models offered only through staff preview. GET /v1/models returns the full catalog available to your key, including authorized previews and retired ids, so the two lists can differ. This illustrative example shows the suffix when Drex 2.0 is the only publicly available active version; the suggestions can differ by environment:
{
"path": "model",
"message": "unknown model \"jev-latest\"; use \"drex-v2.0\" or \"drex-latest\""
}Search your code for hard-coded jev- model names and for calls that pass model per request.
Don't send box questions
Drex doesn't support box questions: it answers 422 and says so. Remove them before you migrate, or keep them on TypeSafe.
Pass criteria descriptions as text
Every description inside criteria must be text. The TypeSafe SDK's types also allow objects and arrays there, but Drex rejects them. instructions is different: a string or null (read as omitted) is used as is, and any other JSON value is rendered to text the way state is, nested at most 8 levels and at most 262,144 characters once rendered (see Questions).
// Rejected: the description is an object
choice("Which team should handle this?", {
payments: { topics: ["payouts", "refunds"] },
});
// Accepted: the description is text
choice("Which team should handle this?", {
payments: "Payouts, charges, or refunds",
});choiceandnoul: a description can also benull, for a label with no description.score:criteriais a list of text labels, one per level, with nonullentries.
A wrong description returns 422 with must be a string, and path names the label or level, such as questions.team.criteria.payments.
Send images to Drex Max
Images work with the TypeSafe clients as they are. @ai-sdk/typesafe-ai, @typesafe-ai/sdk and typesafe-sdk send images as a multipart/form-data upload, and Drex reads that form. The served Drex Max models read images, as do retired ids that resolve to Max v1.1; a model that does not support images returns 422 with path images. Images must be PNG or JPEG, and the whole request, image bytes included, must be at most 4 MiB. See Multipart upload.
The AI SDK needs its baseURL to end in /v1:
import { createTypeSafeAi } from "@ai-sdk/typesafe-ai";
import { experimental_decide as decide } from "ai";
const drex = createTypeSafeAi({
baseURL: "https://console.nace.ai/v1",
apiKey: process.env.NACE_API_KEY,
});
const { answers } = await decide({
model: drex.decisionModel("drex-max"),
state: [
{ type: "text", text: "Inspect this receipt." },
{ type: "file", mediaType: "image/png", data: receiptPng },
],
questions: {
isReceipt: { type: "boolean", instructions: "Is this a purchase receipt?" },
},
});The AI SDK's boolean question needs no change: its provider sends it to Drex as noul, and the answer's noul field is the probability it reads back. Its box question is not supported. Drex returns 422 with the message "box" questions are not supported; use "noul", "choice", or "score".
The TypeSafe-Danger-Zone-Images: true header that the TypeSafe clients send for images is accepted, and Drex does not require it.
Raise the SDK timeout
The SDK's per-attempt timeout defaults to 10,000 ms. Drex waits up to 55 s for the model on a heavy request, so a large state with many questions can exceed the default and surface as an APITimeoutError. Set timeout: 60_000 on the client, or per call in the second argument to systemOne. The timeout applies per attempt, and the SDK retries APITimeoutError by default. Errors and retries covers retries in detail.
Handle 402 and 529
Drex adds 402 to TypeSafe's statuses. Handle it next to 529, which Drex returns when it cannot serve a request right now.
402with typeinsufficient_creditis new in Drex. It means the account has no spendable balance. The SDK raises this as a plainAPIErrorwithstatus402. It is not retried. Top up or add a card on Billing.402with typepayment_requiredmeans an unpaid invoice; it is not retried either, and the invoice is paid on Billing.529with typeoverloadedmeans Drex is at capacity or the model did not respond in time. It carriesretry-afterandretry-after-ms. The SDK treats it as a5xx, raisesInternalServerErrorif retries run out, and honors theretry-afterheaders while retrying.
Errors lists every type with its status.
legend is an object for the TypeSafe SDK
By default Drex returns the score answer's legend as an array of level labels. The SDK types describe it as an object keyed by score, and the Python typesafe-sdk validates the response against that, so an array raises a validation error on answers.<name>.legend even though the request succeeded with 200. DSPy's TypeSafe client failed the same way for a Score output, and Opik and Weave wrap the Python client.
Drex now answers the TypeSafe SDK with an object keyed by level index as a string, {"0": "Calm", "1": "Annoyed", "2": "Angry"}, without any change on your side: it recognizes a TypeSafe client by an X-TypeSafe-SDK header with any non-empty value, or by a User-Agent that starts with typesafe-sdk/ or langchainjs-typesafe/. langchain-typesafe sends no X-TypeSafe-SDK, so its User-Agent is what identifies it (read from its source, not tried against Drex). Any other client keeps the array and can send X-Nace-Legend: object. Only the shape of legend changes. To choose the shape yourself, send X-Nace-Legend: object or X-Nace-Legend: array; the header wins over detection in both directions and any other value is ignored. See legend as an array or an object.
This applies to the TypeScript @typesafe-ai/sdk too, which sends the same header. Until now Drex returned it an array, so code that already reads legend with array methods (map, length, Array.isArray, spreading) will see an object once this is deployed. Index access such as legend[2] keeps working. To keep the array, send X-Nace-Legend: array.
What was checked: an object legend validates against the real typesafe-sdk 0.7.5 ScoreAnswer model, and a live Score call through that SDK and through DSPy's TypeSafe client against https://console.nace.ai returned a valid answer. A live call through Opik or Weave, which wrap the same Python client, has not been run.
What stays the same
- The endpoints,
POST /v1/systemoneandGET /v1/models. - The request shape:
state,questions, and optionalmodel. Each question'stypeisnoul,choice, orscore; Questions shows the JSON for each, and the API reference has the full body. - The response shape:
model,answerskeyed by question name, andusagewithinput_tokensandoutput_tokens. Drex's body also carriesevaluation_time_msandrequest_id, which the SDK'sSystemOneResulttype does not declare. - The statuses
401,422,429, and5xx, which the SDK maps toAuthenticationError,UnprocessableEntityError,RateLimitError, andInternalServerError. - The
x-typesafe-request-idheader. The SDK reads it intoAPIError.requestIdandwithResponse().requestId. Drex sends the same value asx-request-id. - The SDK's retry behavior: 2 retries by default, backoff from 500 ms doubling to 5 s, on
408,429, and500to599, honoringretry-afterandretry-after-ms. - The
noul,choice, andscorehelpers, and the answer types they infer. Ascoreanswer'slegendreaches the TypeSafe SDK as an object, as it expects; seelegendis an object for the TypeSafe SDK. - The multipart upload that the clients use for images, on the served Drex Max models and retired ids that resolve to Max v1.1.
Checklist
- Set
TYPESAFE_BASE_URL,TYPESAFE_API_KEY, andTYPESAFE_DEFAULT_MODEL, or the matching constructor options. - Remove hard-coded
jev-*model names. - Remove
boxquestions. - Make sure that every criteria description is a string, and that
scorelevels contain nonull. - Raise
timeoutfor large requests. - Handle
402and529where you handle otherAPIErrorcases. - Send images to a served Drex Max model or a retired id that resolves to Max v1.1, as PNG or JPEG.
- Run
client.models.list()and confirm that it includesdrex-v2.0. - If you ask
scorequestions, nothing to change: Drex sends the TypeSafe SDK an objectlegend. SendX-Nace-Legend: arrayorobjectonly to override that.