Skip to content

Decisions ​

@xsai/decide asks questions about an input and returns typed answers. Use it to classify a message, route a ticket, check a policy, or rate a text. It needs a service with a decisions or systemone endpoint, or any language model.

sh
pnpm add @xsai/decide
ts
import { 
decide
,
decisions
} from '@xsai/decide'
const
model
=
decisions
({
apiKey
:
process
.
env
.
OPENAI_API_KEY
,
baseURL
: 'https://api.openai.com/v1/',
model
: 'gpt-6-luna',
}) const {
answers
} = await
decide
(
model
, {
input
: 'The package arrived with a broken screen.',
questions
: {
damaged
: {
instructions
: 'Does the customer report a damaged item?',
type
: 'boolean' },
}, })
console
.
log
(
answers
.
damaged
.
probability
)

decide(model, { input, questions }) sends one request that holds every question. It returns { answers, usage }. Each key of questions is the ID of one question, and the same key holds its answer in answers. The type of an answer follows the type of its question, so TypeScript knows which fields exist.

Question types ​

A question has instructions that tell the model what to judge, and a type that sets the shape of the answer.

TypeQuestionAnswer
booleanA yes or no question.probability, a number from 0 to 1 for the answer yes.
choicePick one value from choices.choice, the picked value.
scoreRate the input on the scale in levels.score, a number that can have a fraction.

A score of 1.43 on three levels lies between the second level and the third level. The first level has the number 0. The model decides the answer, and you decide what to do with it. For a boolean answer, compare probability with a threshold that suits your use case.

ts
import { 
decide
} from '@xsai/decide'
const {
answers
} = await
decide
(
model
, {
input
: 'I was charged twice. Please fix this today.',
questions
: {
department
: {
choices
: [
{
description
: 'Payments, invoices, and refunds.',
value
: 'billing' },
{
description
: 'Problems using the product.',
value
: 'technical' },
],
instructions
: 'Which department should handle this message?',
type
: 'choice',
},
urgency
: {
instructions
: 'How urgent is this message?',
levels
: [{
label
: 'Can wait' }, {
label
: 'Needs a reply today' }, {
label
: 'Blocks the customer' }],
type
: 'score',
}, }, })
console
.
log
(
answers
.
department
.
choice
,
answers
.
urgency
.
score
)

answers.department.choice has the type 'billing' | 'technical'. To keep these literal values, write choices inline as shown, or declare them with as const.

input and instructions take a string or an object. Adapters serialize an object to JSON when the service expects text.

Use a language model ​

toDecisionModel turns any LanguageModel into a decision model. Use it when your service has no decisions endpoint.

ts
import { 
decide
,
toDecisionModel
} from '@xsai/decide'
const
model
=
toDecisionModel
(
languageModel
, {
temperature
: 0 })
const {
answers
} = await
decide
(
model
, {
input
: 'The package arrived with a broken screen.',
questions
: {
damaged
: {
instructions
: 'Does the customer report a damaged item?',
type
: 'boolean' },
}, })

The adapter makes one request with structured output for all questions. The model writes the numbers itself, so a boolean probability is an estimate and not a token probability. The adapter checks each answer against its question, because some services, such as Ollama, do not enforce the number limits in the schema. Choice answers and score answers have no confidence and no probabilities. Use decisions() or systemone() when you need those fields.

toDecisionModel(model, options?) passes maxOutputTokens, providerOptions, reasoningEffort, temperature, and topP to the language model.

Handle a refusal ​

If the service refuses a question, decide throws DecisionRefusalError and returns no partial answers. Its questionIds field lists the refused IDs.

ts
import { 
decide
,
DecisionRefusalError
} from '@xsai/decide'
try { await
decide
(
model
, {
input
: 'A support message.',
questions
: {
safe
: {
instructions
: 'Is the message safe to publish?',
type
: 'boolean' } },
}) } catch (
error
) {
if (
DecisionRefusalError
.
isInstance
(
error
))
console
.
log
(
error
.
questionIds
)
else throw
error
}

A model that refuses without naming a question refuses all of them. toDecisionModel does the same when the language model refuses.

Reference ​

OptionDescription
inputRequired. The content to judge, as a string or an object.
questionsRequired. An object that maps a question ID to a question.
providerOptionsOptions for one adapter. The sections below list them.
signalAn AbortSignal.
Question fieldUsed byDescription
typeAllboolean, choice, or score.
instructionsAllRequired. A string or an object.
choiceschoiceRequired. A list of { value, description? }.
levelsscoreRequired. A list of { label, description? }, from the lowest level to the highest.
AnswerFields
booleanprobability.
choicechoice, and when the service gives them, confidence and probabilities, a list of { value, probability }.
scorescore, and when the service gives them, confidence and probabilities, an object that maps a level number to its probability.

usage can have inputTokens, outputTokens, and totalTokens. A field is absent when the service reports no value.

Adapters ​

Every adapter takes { baseURL, model, apiKey?, headers?, fetch? } and returns a DecisionModel.

AdapterRequestProvider options
decisions()POST decisionsproviderOptions.decisions.overrideInput
systemone()POST systemoneproviderOptions.systemone.images
toDecisionModel(model, options?)One request to a LanguageModel.None. The adapter ignores the options of other adapters.

systemone() sends each score level as one string: the label, or label: description when the level has a description.

overrideInput replaces input for decisions(). It takes a string, or a list of user messages that can hold input_text and input_image content. An empty string also replaces the input.

images adds pictures to a systemone() request. Each image is a URL, a data URL, or an object { base64, content_type }. The content type is image/jpeg, image/png, or image/webp.

Errors ​

CodeMeaning
decision-refusalThe service refused at least one question. questionIds lists them.
invalid-responsetoDecisionModel got a generation that did not complete, one that has tool calls, or an answer outside its question: a probability outside 0 to 1, a score outside the levels, or a value that is not in choices.
truncated-streamThe text stream of toDecisionModel ended without a terminal event.

HTTP failures throw HttpError, and network failures throw network-error. No adapter retries a request. To write your own decision model, read Write a custom model.

Contributors

Changelog