@niscorp/signal
v0.1.3
Published
Universal LLM abstraction — stateless, immutable, provider-agnostic
Maintainers
Readme
@niscorp/signal
Universal LLM abstraction. Stateless, immutable, provider-agnostic. Structured output with Zod, tool calling, zero hard dependencies.
Install
pnpm add @niscorp/signal zod
# Plus the provider SDK you want to use:
pnpm add openai # for OpenAI, Groq, OpenRouter, or any OpenAI-compatible APIQuick Example
import { createSignal } from '@niscorp/signal';
import { z } from 'zod';
const signal = createSignal('groq');
// Simple text completion
const { response } = await signal.complete('What is 2+2?');
// Structured output — response is typed
const { response: user } = await signal
.schema(z.object({ name: z.string(), age: z.number() }))
.complete('Extract: Alice is 30 years old');
// user.name === 'Alice', user.age === 30Documentation
- DOCS.md — Full API reference with examples
- DESIGN.md — Architecture, design decisions, and trade-offs
API
Every builder method returns a new immutable instance. complete()
and stream() run the full Signal pipeline (schema, retries, tool
loop). step() and stepStream() are the low-level primitives — one
adapter call, no auto tool execution — used by orchestrators like
@niscorp/cortex that own their own tool loop.
// Create
const signal = createSignal('groq'); // known provider
const signal = createSignal('groq', { apiKey, model, systemPrompt, retries });
// Configure (each returns a new instance)
signal.model('qwen/qwen3.8-27b')
signal.systemPrompt('You are helpful.')
signal.schema(zodSchema) // typed structured output
signal.tools([myTool]) // tool calling
signal.history(messages) // multi-turn
signal.retries(3) // validation retries
signal.options({ temperature: 0 }) // sampling / generation options
signal.apiKey(key) // override the env key
signal.describe() // provider, model, capabilities — from the registry
signal.onRetry(handler) // retry hook
signal.onToolCall(handler) // tool call hook
// Execute — high level
const { response, history, meta } = await signal.complete('user message');
// Execute — streaming
for await (const event of signal.stream('user message')) {
if (event.type === 'text') process.stdout.write(event.text);
if (event.type === 'done') console.log(event.meta.usage);
}
// Execute — low level (single adapter call, no tool execution)
const { content, toolCalls, usage, finishReason } = await signal.step({
messages: [...], tools: [{ name, description, parameters }],
});
// Execute — streaming low level (symmetric with step())
for await (const event of signal.stepStream({ messages, tools })) {
if (event.type === 'text') process.stdout.write(event.text);
if (event.type === 'done') {
// event.result is the aggregated StepResult — same shape as step()
}
}
// Execute — embedding (separate client, embedding model)
const embedder = createSignal('openai').model('text-embedding-3-small');
const vector = await embedder.embed('wireless headphones'); // number[]
const vectors = await embedder.embed(['shoes', 'boots', 'hat']); // number[][]
const small = await embedder.embed('text', { dimensions: 256 }); // truncated
// Execute — decisions (typed questions about a state; no text is generated)
const jev = createSignal('typesafe');
const { decisions } = await jev.decide({
state: { message: 'I was charged twice' },
questions: {
intent: { type: 'choice', instructions: 'What do they want?', criteria: { refund: 'Money back', other: 'Anything else' } },
needsHuman: { type: 'noul', instructions: 'Does this need a person?' },
},
});
decisions.intent.choice; // 'refund' | 'other'Providers
| Provider | String | Kind | SDK | Embedding |
|----------|--------|------|-----|-----------|
| Groq | 'groq' | chat | openai | No |
| OpenAI | 'openai' | chat | openai | Yes |
| OpenRouter | 'openrouter' | chat | openai | No |
| Anthropic | 'anthropic' | chat | stub (use OpenRouter) | No |
| Google | 'google' | chat | stub (use OpenRouter) | No |
| TypeSafe (Jev) | 'typesafe' | decisions | none — one fetch | — |
A chat provider has every verb; decide() runs there by emulation, uncalibrated. A decision provider has decide() only.
API keys are read from environment variables (GROQ_API_KEY, OPENAI_API_KEY, TYPESAFE_API_KEY, etc.) or passed via .apiKey() / options.
License
Apache-2.0
