π―π΅ ζ₯ζ¬θͺη / Japanese version: https://zenn.dev/saks/articles/214917996ad932
TL;DR:
askiflets you branch on fuzzy judgments (ask.if,ask.switch,ask.score) with typed results. It runs on Jev (fast, about $0.013 per 1,000 calls) or on OpenAI-compatible models. β GitHub
Ever wanted to write an if for a condition that's hard to spell out in code?
Say you're triaging support tickets and want to know whether the customer is asking for a human. Here's the hand-written version.
const wantsHuman =
/(human|person|agent|representative)/i.test(text) &&
!/(human resources|secret agent|humans are)/i.test(text) &&
!/(don't need|no thanks|not necessary)/i.test(text);
// "I've asked three times and nobody has helped. Get me someone who can fix this." β false β
// "Humans are amazing" β true β
Painful. Every keyword you add brings new exceptions, and you can't keep up with rephrasing.
Let's try ask.if. One line:
await ask.if(text, "the customer is asking for a human agent", routeToHuman);
This isn't magic. It's an AI call.
"Calling an AI for a single branch? That's absurdly slow and expensive." I thought so too, so I measured it:
- Cost: about $0.013 per 1,000 calls. That's roughly 300 input tokens per call, at $0.042 per million input tokens. Output tokens are free.
- Latency: 72 ms inference + 175 ms network β 250 ms. Measured from Japan in TypeSafe's official playground.
The 175 ms is mostly distance. Jev currently has no Japan or Asia region, and the model's own work is 72 ms.
That's possible because it runs on Jev, a model from TypeSafe AI. Jev is what TypeSafe calls a "System One" model: instead of generating text, it returns probabilities directly. Skipping text generation is what makes it fast and cheap, and you get back numbers like 0.93 instead of prose you'd have to parse.
You don't need to know how Jev works, though. I wrapped it in a small syntax, so you can just use that.
Get started
npm install askif @askif/jev
export TYPESAFE_API_KEY=...
Node 22+, server-side only. TypeSafe's SDK refuses to run in a browser, since it would expose your API key. Browser use, through a server-proxy backend, is planned.
Three verbs
ask.if: yes or no
The model returns a probability, and askif runs the branch that matches. That's why "not sure" can get its own branch:
const ticket = "I've asked three times and nobody has helped. Get me a real person.";
await ask
.if(ticket, "the customer is asking for a human agent", () => console.log("routed to a human agent"))
.else(() => console.log("routed to the bot"))
.unsure(() => console.log("sent for review"));
Without .unsure(), the then handler runs when the probability is above threshold (default 0.5). With .unsure(), anything inside unsureBand (default 0.2β0.8) goes there instead. 0.55 and 0.98 don't have to be the same "yes".
Need a plain boolean or the raw number?
if (await ask.is("penguin", "can fly")) console.log("penguin can fly");
else console.log("penguin can't fly");
const p = await ask.probability("penguin", "can fly"); // 0..1
ask.switch: one of several options
const team = await ask
.switch(ticket, "Which team should handle this?")
.case("returns", "Exchanges, wrong or damaged items", () => console.log("β returns team"))
.case("shipping", "Delivery status, delays, lost packages", () => console.log("β shipping team"))
.case("billing", "Charges, invoices, payment problems", () => console.log("β billing team"));
team.choice; // "returns" | "shipping" | "billing" (narrowed to your keys)
team.ranking; // every option, most likely first
Both the option key and its description are sent to the model. If the key explains itself, skip the description: .case("calm").
ask.score: a position on a scale
const COSMETIC = "Cosmetic; no impact to functionality";
const WORKAROUND = "Broken or degraded feature, but workaround exists";
const BLOCKING = "Blocking issue; no workaround exists";
const result = await ask
.score("The export button crashes the settings page in Safari. Works in Chrome.", "How severe?")
.level(COSMETIC, () => console.log("β backlog"))
.level(WORKAROUND, () => console.log("β this sprint"))
.level(BLOCKING, () => console.log("β page on-call"));
The handler that runs is the one for the most likely level, not a rounded score. The result also carries score (the probability-weighted position, which can fall between levels) and normalized (0..1), for ranking.
The practical bits
Every chain is awaitable. It resolves with what happened: r.level, r.branch, r.confidence. Handlers receive the same result, so they can look at the runner-up.
Batching. Questions about the same state in the same tick go out as one request:
await Promise.all([
ask.if(order, "looks fraudulent", flag),
ask.if(order, "ships internationally", addCustoms),
ask.score(order, "How urgent is delivery?").level("standard").level("express"),
]); // one request
Measured: those three questions as one request took a median of 166 ms. Sent one after another, the same three took 519 ms. As three parallel requests, 177 ms. (Median of 10 runs each, round trip included, measured from outside Japan, so these numbers are separate from the 72 + 175 ms above.) Extra questions barely add time.
Tests run offline. mock answers locally and records every call:
import { createAsk, mock } from "askif";
const backend = mock(() => ({ kind: "yesno", probability: 0.9 }));
const ask = createAsk({ backend });
await ask.is("cat", "is animal");
console.log(backend.calls); // what was asked
Thresholds are configurable globally (ask.configure({ threshold: 0.6 })) or per call.
"We can't use Jev in production"
Jev is fast and cheap, but not everyone can use it. Reasons vary:
- No Japan or Asia region, so latency or data-location requirements aren't met (at the moment)
- Adding a new AI vendor means going through procurement or a security review
- You already have a contract with Azure or AWS and want to stay inside it
- You want to run it on your own infrastructure, or locally
So askif is split in two:
-
askif: the chain logic, theBackendcontract, andmock. It doesn't know Jev exists. -
@askif/jevand@askif/openai: thin adapters, each implementingBackend.
Right now there are two ways out: @askif/openai for general-purpose LLMs, and OpenJev, an open Jev-compatible server, through @askif/jev.
Switching to @askif/openai
It's one import:
// before
import { ask } from "@askif/jev";
// after
import { ask } from "@askif/openai";
Your ask.if / switch / score code doesn't change. @askif/openai speaks Chat Completions, so a baseURL reaches most of the ecosystem:
import { openai } from "@askif/openai";
// OpenRouter
const viaOpenRouter = openai({
apiKey: process.env.OPENROUTER_API_KEY,
baseURL: "https://openrouter.ai/api/v1",
model: "openai/gpt-6-luna",
});
// Local: Ollama (vLLM, LM Studio and llama.cpp work the same way)
const viaOllama = openai({
apiKey: "ollama",
baseURL: "http://localhost:11434/v1",
model: "llama3.3",
});
Azure OpenAI and Amazon Bedrock need their own client classes. They're re-exported, so you don't need a separate npm install openai:
import { AzureOpenAI, openai } from "@askif/openai";
const viaAzure = openai({
client: new AzureOpenAI({
apiKey: process.env.AZURE_OPENAI_API_KEY,
endpoint: process.env.AZURE_OPENAI_ENDPOINT,
deployment: "gpt-6-luna",
apiVersion: "2025-04-01-preview",
}),
});
Bedrock uses BedrockOpenAI. Google's OpenAI-compatible Gemini API and Vertex AI work through baseURL too. See the @askif/openai README for the details.
Tradeoffs, honestly
A general-purpose LLM isn't a System One model, and the adapter doesn't pretend it is:
- Probabilities are the model's own JSON estimates (via Structured Outputs), not logprobs.
-
confidenceis computed by askif from how spread out the probabilities are. The adapter never self-reports one, because models are poor at that. - With Jev, same-state questions are one request. With OpenAI, they're N parallel requests.
- Local servers must honor
response_format: json_schema, or you'll seeBAD_RESPONSEerrors. The README's server table reflects each project's docs only; none of these has been tested live. - Thresholds tuned on one backend may not carry over to another. Test on your own data.
If none of these fit, the Backend contract is small: implement it for whatever model you have.
An open, Jev-compatible server: OpenJev
There's another option: OpenJev, an open server that implements the same wire APIas Jev. It's a separate project, not from TypeSafe AI, and you can run it on your own GPU or Apple silicon. There's a hosted version on Codiv, too. With @askif/jev, only baseURL and model change:
import { jev } from "@askif/jev";
const backend = jev({
apiKey: process.env.OPENJEV_API_KEY, // not needed for a self-hosted server without auth
baseURL: "https://api.codiv.ai", // or your own server, e.g. "http://127.0.0.1:8080"
model: "openjev-latest",
});
OpenJev uses its own model IDs (openjev-latest). Pinned Jev IDs like jev-1.13.0 return a 400.
Writing good questions
From TypeSafe's docs, and they hold for any backend:
- One judgment per question. "is angry and wants a refund" works worse than two questions combined in your code.
- Phrase it so yes is the interesting answer: "contains personal data," not "is free of personal data."
- Describe situations, not degrees. "Broken, but a workaround exists" works; "moderately severe" doesn't.
- Test against your own data, and tune thresholds in code.
What's next
Offline examples live in packages/askif/examples/ in the repo.
More adapters are in progress: Anthropic, plus lighter setups such as Laya on OpenJev and an MLX-based configuration. A server-proxy backend for browser use, a form for declaring several questions up front, and per-call AbortSignal with usage reporting are planned too.
Packages: askif Β· @askif/jev Β· @askif/openai
askif is an unofficial library and is not affiliated with TypeSafe AI.
askif
Let Jev decide.
Typed decisions for your code: ask.if, ask.switch, ask.score.
askif is the provider-neutral toolkit: the chain logic, the Backend contract, and a mock backend for tests. @askif/jev is the batteries-included bundle for TypeSafe's Jev, with a ready-to-use ask:
import { ask } from "@askif/jev";
// packages/jev/examples/basic.ts#L5-L7
await ask.if("cat", "is animal", () => {
console.log("cat is animal");
});
Each call asks Jev, TypeSafe's System One model, a question about some state. Jev returns probabilities instead of text, and askif runs the branch that matches.
Unofficial, not affiliated with TypeSafe AI.
Install
npm install askif @askif/jev
export TYPESAFE_API_KEY=...
Node 22 or newer. Run it on a server: the TypeSafe SDK refuses to run in a browser, where your API key would be exposed. Building your own backend insteadβ¦
Top comments (0)