The call to a model.
Signal is the client Nisc calls a model with. You give it a Zod schema and get back an object of that shape. Signal sends the schema the way that model takes one, reads the JSON out of whatever comes back, and checks it against the schema before you get it.
pnpm add @niscorp/signal zod openai · 0.1.5 · Apache-2.0
import { z } from 'zod';
import { createSignal } from '@niscorp/signal';
const Booking = z.object({
show: z.string(),
seats: z.number(),
});
const signal = createSignal('groq');
const { response } = await signal
.schema(Booking)
.complete(
'Two for The Tempest on Saturday, please.',
);Here is the booking:
```json
{ "show": "The Tempest", "seats": 2 }
```{"show":"The Tempest","seats":2}The model’s replies on this page are written by hand, and no model is called. What Signal did with each reply is what the installed package returned.
First steps →Every model takes a schema its own way.
One model follows a JSON Schema sent with the request. Another only promises to return JSON. qwen/qwen3.8-27b does the first on OpenRouter and the second on Groq. Signal knows this, so no other Nisc package has to: Cortex calls every model through Signal.
Read out of whatever the model wrote.
A model that was asked for JSON often wraps it in words, escapes its quotes, or stops before the end. Signal tries the reply as it stands, and then looks for the JSON in it. Each of these four replies gave {"show":"The Tempest","seats":2}.
Here is the booking:
```json
{ "show": "The Tempest", "seats": 2 }
```Sure. { "show": "The Tempest", "seats": 2 } Anything else?{\"show\": \"The Tempest\", \"seats\": 2}{ "show": "The Tempest", "seats": 2, "note": "by the aiWhat Signal finds counts only if it passes your schema. From a reply that stops early, Signal drops the field that was being written, and this one passed because the schema does not ask for that field. A reply that is valid JSON and fails the schema is retried: the model is sent what failed and asked again, twice by default.
{ "show": "The Tempest", "seats": "two" }The call throws
Four providers, and any endpoint of your own.
Signal ships 14 rows of what a model can do: one for each model on each provider it was measured on, each written by a probe script and dated the day it ran. describe() reads the row and makes no request. A model with no row still runs, and is taken not to follow a JSON Schema sent with the request.
Groq, OpenAI and OpenRouter are all called with the openai package, which is why it is in the install line.
Signal runs the loop, or you do.
A tool is a function of yours with a schema for its arguments. With complete and stream, Signal runs the loop: it runs each tool the model asks for and retries an answer that fails your schema. After twelve calls to the model, ten plus the retries, it throws. step is one call to the model and runs nothing. Use it when the messages and the tools are yours to manage, as they are in an agent runtime. Cortex is built on it.
A model that decides.
A decision model writes no text. You give it something to judge and questions with fixed options, and it returns a probability for each option. typesafe is the provider that has one, and decide is the call.
const decider = createSignal('typesafe');
const result = await decider.decide({
state: 'Two for The Tempest on Saturday, please.',
questions: {
wants: {
type: 'choice',
instructions: 'What does the member want?',
criteria: {
seats: 'Seats for a night',
refund: 'Money back',
other: 'Anything else',
},
},
},
});{
"calibrated": true,
"decisions": {
"wants": {
"choice": "seats",
"probabilities": {
"seats": 0.94,
"refund": 0.04,
"other": 0.02
},
"confidence": 0.94
}
}
}A chat model can answer the same call. Then calibrated is false, and there are no probabilities.
What an engineer asks.
SignalError with the original inside. The openai package makes its own retries.openrouter. Neither has a measured row yet, so a schema goes to them in JSON mode and a message.createSignal a client of your own and any key, and it answers in place of the provider.stream is the same run as events, and takes an AbortSignal.