ailogic
v0.1.1
Published
Dynamically test whether any value evaluates to truthy or falsy by asking a configured AI model.
Maintainers
Readme
ailogic
Test whether a value is truthy or falsy by asking an AI model at runtime.
import { configure, isTruthy } from 'ailogic';
configure({ provider: 'claude-code' }); // uses your Claude.ai login, no API key
await isTruthy('yes, ship it'); // true
await isTruthy(''); // false
await isTruthy('0'); // true — a non-empty string
await isTruthy([]); // true — an object
await isTruthy(Number.NaN); // falseThe default question is JavaScript's own rule: is Boolean(value) true?
Exactly eight values are falsy — false, 0, -0, 0n, "", null,
undefined, NaN — and everything else, including "0", "false", [] and
{}, is truthy.
Supply a question and the model answers that instead, which is where the
runtime part earns its keep:
await isTruthy('[email protected]', { question: 'Is this a valid email address?' });
await isTruthy(order, { question: 'Has this order been fully delivered?' });
await isTruthy(message, { question: 'Is this message spam?' });Install
yarn add ailogic # or: npm install ailogicNode 18+ (needs a global fetch). Ships CommonJS, ES modules and TypeScript
declarations; browser bundles work for every provider except claude-code,
which spawns a local process.
Two ways to authenticate
1. Your Claude.ai subscription — no API key
The claude-code provider shells out to the Claude Code
CLI in headless mode, so it reuses whatever
claude auth login already stored. Nothing to configure, nothing to bill.
configure({
provider: 'claude-code',
model: 'claude-haiku-4-5', // cheaper than the claude-opus-5 default
providerOptions: { forceSubscription: true }, // ignore any ANTHROPIC_API_KEY
});Each call spawns a process, so expect a few seconds of startup per evaluation. Ideal for tests, scripts and local development; use an API-key provider for request-path work.
2. An API key
configure({ provider: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY });Or set nothing at all: with no provider, ailogic picks the first backend whose
API-key environment variable is populated (Anthropic → OpenAI → Google → …).
The claude-code provider is never auto-selected, because it starts a process.
Supported providers
| Name | Aliases | Key from | Default model |
| -------------- | ------------------------------------------------ | ---------------------------------- | ----------------------------------------- |
| anthropic | claude, claude-api | ANTHROPIC_API_KEY | claude-opus-5 |
| claude-code | claude.ai, claude-cli, claude-subscription | — (local login) | claude-opus-5 |
| openai | | OPENAI_API_KEY | gpt-4.1-mini |
| azure-openai | azure | AZURE_OPENAI_API_KEY | your deployment name |
| google | gemini | GOOGLE_API_KEY, GEMINI_API_KEY | gemini-2.5-flash |
| xai | grok | XAI_API_KEY | grok-4-fast |
| mistral | | MISTRAL_API_KEY | mistral-small-latest |
| deepseek | | DEEPSEEK_API_KEY | deepseek-chat |
| groq | | GROQ_API_KEY | llama-3.3-70b-versatile |
| cohere | | COHERE_API_KEY | command-a-03-2025 |
| openrouter | | OPENROUTER_API_KEY | openai/gpt-4.1-mini |
| together | | TOGETHER_API_KEY | meta-llama/Llama-3.3-70B-Instruct-Turbo |
| fireworks | | FIREWORKS_API_KEY | llama-v3p3-70b-instruct |
| perplexity | | PERPLEXITY_API_KEY | sonar |
| ollama | local | — (local server) | llama3.1 |
| lmstudio | | — (local server) | local-model |
listProviders() returns the live list. Anything else that speaks the OpenAI
Chat Completions shape takes one call to add — see
Custom providers.
Choosing a model
Defaults favour capability, not price. A truthiness check is a small task, so for volume, override the model:
configure({ provider: 'anthropic', model: 'claude-haiku-4-5' });
configure({ provider: 'openai', model: 'gpt-4.1-mini' });
configure({ provider: 'ollama', model: 'llama3.1' }); // free, fully localAPI
Every function exists both as a module-level export (backed by the process-wide
config) and as a method on a createClient() instance.
| Function | Returns | Notes |
| ------------------------------- | ----------------------- | -------------------------------------------- |
| isTruthy(value, options?) | Promise<boolean> | |
| isFalsy(value, options?) | Promise<boolean> | |
| evaluate(value, options?) | Promise<Evaluation> | verdict, confidence, reason, usage, timing |
| assertTruthy(value, options?) | Promise<Evaluation> | throws AssertionError when falsy |
| assertFalsy(value, options?) | Promise<Evaluation> | throws AssertionError when truthy |
| evaluateAll(values, options?) | Promise<Evaluation[]> | bounded concurrency, input order preserved |
| configure(patch) | config | merges; also getConfig(), resetConfig() |
| createClient(config?) | AiLogicClient | isolated config and cache |
| clearCache() | void | |
| registerProvider(adapter) | void | also listProviders(), getProvider() |
Evaluation
{
truthy: boolean; // the answer
verdict: 'truthy' | 'falsy' | 'unknown';
confidence: number; // 0..1, self-reported
reason: string; // one sentence from the model
question: string; // what it was actually asked
provider: string;
model: string;
cached: boolean;
durationMs: number;
raw: string; // unparsed model output
usage?: { inputTokens?, outputTokens?, totalTokens?, costUsd? };
}Options
Everything below works per call, on a client, or in configure().
| Option | Default | Meaning |
| ----------------- | ---------------- | --------------------------------------------------------- |
| question | Boolean(value) | The yes/no question to answer about the value |
| context | — | Extra domain rules appended to the prompt |
| provider | auto-detected | Provider name or alias |
| model | provider default | |
| apiKey | from environment | |
| baseUrl | provider default | Proxies, gateways, self-hosted endpoints |
| headers | {} | Extra HTTP headers |
| temperature | 0 | Not sent to Anthropic, which rejects it on current models |
| maxTokens | 512 | |
| timeoutMs | 60000 | Per attempt |
| retries | 2 | Transport errors and 429/5xx, with jittered backoff |
| cache | true | In-process LRU |
| cacheTtlMs | 300000 | |
| maxValueLength | 2000 | Characters of the serialized value sent to the model |
| minConfidence | 0 | Below this, a verdict is downgraded to unknown |
| onUnknown | 'throw' | Or 'truthy' / 'falsy' to coerce instead |
| signal | — | AbortSignal for cancellation |
| providerOptions | {} | Merged into the provider request body |
Handling "I can't tell"
The model may answer unknown. By default that throws IndeterminateError;
pick a side instead if your call site needs a boolean no matter what:
await isTruthy(mystery, { onUnknown: 'falsy' });
// Or demand certainty:
await isTruthy(value, { minConfidence: 0.9, onUnknown: 'falsy' });Errors
All extend AiLogicError and carry a stable .code.
| Class | .code | When |
| -------------------- | ----------------------- | ---------------------------------------------- |
| ConfigurationError | AILOGIC_CONFIGURATION | No provider, no API key, unknown provider name |
| ProviderError | AILOGIC_PROVIDER | Non-2xx, refusal, malformed provider response |
| TimeoutError | AILOGIC_TIMEOUT | timeoutMs elapsed, or the caller aborted |
| ParseError | AILOGIC_PARSE | The model answered unreadably (.raw has it) |
| IndeterminateError | AILOGIC_INDETERMINATE | Verdict unknown under onUnknown: 'throw' |
| AssertionError | AILOGIC_ASSERTION | assertTruthy / assertFalsy failed |
Isolated clients
configure() is process-wide. Inside a library, or when talking to two
providers at once, use a client instead — it owns its own config and cache:
import { createClient } from 'ailogic';
const fast = createClient({ provider: 'groq', model: 'llama-3.3-70b-versatile' });
const careful = createClient({ provider: 'anthropic', model: 'claude-opus-5' });
const quick = await fast.isTruthy(value);
if (!quick) await careful.assertTruthy(value);Custom providers
An OpenAI-compatible endpoint needs only a preset:
import { createOpenAiCompatibleProvider, registerProvider } from 'ailogic';
registerProvider(
createOpenAiCompatibleProvider({
name: 'my-gateway',
label: 'Internal Gateway',
defaultModel: 'internal-small',
defaultBaseUrl: 'https://llm.internal.example/v1',
apiKeyEnv: ['INTERNAL_LLM_TOKEN'],
structuredOutput: 'json_object', // or 'json_schema' | 'none'
}),
);Anything else implements complete(request):
registerProvider({
name: 'my-backend',
label: 'My Backend',
defaultModel: 'v1',
apiKeyEnv: ['MY_TOKEN'],
requiresApiKey: true,
async complete(request) {
// request: { system, prompt, model, apiKey, baseUrl, schema, timeoutMs, ... }
return { text: '{"verdict":"truthy","confidence":1,"reason":"…"}' };
},
});The same interface makes an excellent test double — register a deterministic
provider and your suite never touches the network. See
examples/custom-provider.mjs.
Behaviour worth knowing
Caching. Identical (provider, model, base URL, prompt, temperature,
maxTokens) evaluations are served from a bounded in-process LRU for
cacheTtlMs. It is per process and never persisted. Disable with
cache: false.
Value serialization. Values are rendered with their runtime type, bounded
depth and size, cycles collapsed to [Circular], and hard-capped at
maxValueLength. A throwing getter degrades to a note, never an exception.
Prompt injection. The value is always fenced and the system prompt tells the
model to treat it as inert data. That raises the bar; it does not make hostile
input safe to act on unreviewed. Do not wire assertTruthy straight into an
authorization decision over attacker-controlled text.
Determinism. temperature: 0 and a schema-constrained response make results
stable in practice, not guaranteed. Treat a verdict as a judgement, not a proof —
including for the default question, where a plain Boolean(value) is exact,
free and instant. Reach for ailogic when you want the question to be dynamic.
Structured output. Where a provider supports JSON Schema responses it is used; where it only supports "some JSON" that is used; otherwise the parser falls back to extracting JSON from prose, and finally to a keyword scan.
Development
yarn install --frozen-lockfile
yarn run lint # eslint + prettier, must pass before staging
yarn run test # jest
yarn run typecheck # tsc --noEmit
yarn run build # dist/cjs, dist/esm, dist/typesSources are TypeScript, transpiled by Babel against the browserslist in
package.json, with polyfills pulled from @babel/runtime-corejs3 rather than
installed on globals. Tests are plain .test.js next to the code they cover.
The repository's working rules live in AGENTS.md.
Releases
Pushing code to main publishes to npm automatically
(.github/workflows/release.yml): resolve
version → lint → typecheck → test → build → publish → tag.
Version numbers are split between a human and CI:
major.minoris yours. It lives in theversionfield ofpackage.jsonand only changes when you change it.- The revision is CI's. Each release takes one past the highest
major.minor.<n>the registry has ever seen, starting at0for a new series. Gaps are skipped rather than reused, so the number only counts up. Bump the minor and the next release starts again at.0.
So the revision committed in package.json is a placeholder — 0.4.99 in the
repo still publishes 0.4.0 if no 0.4.x exists yet. Nothing is committed back
to main; each release is recorded as a v<version> tag and GitHub Release.
Publishing needs the NPM_TOKEN repository secret. It must be a token that can
publish while two-factor auth is enforced — a Granular Access Token with
Bypass two-factor authentication enabled, or a classic Automation token.
Docs, examples and devcontainer changes do not trigger a release, because they
cannot change the published tarball.
License
MIT
