npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sage-sdk

v0.1.0

Published

TypeScript client for the Levanto Sage decision API.

Readme

sage-sdk

TypeScript client for Levanto Sage. One decision in, a typed answer or null out.

Three classes. SageSDK holds the client. sage(context) returns a Sage: the value, how sure that value is, and the rest of the decision. sageAnswer(context) returns a SageAnswer: the value only. SageApi is the HTTP client underneath: decide, batch, estimate, and ready.

import { SageSDK } from "sage-sdk";

const sdk = new SageSDK({ apiKey: process.env.SAGE_API_KEY });
const refund = "Refund request: order #4411, $180, bought 41 days ago.";
const options = {
  refund: "Cash refund within policy",
  credit: "Store credit only",
  deny: "Outside policy",
};

const reading = await sdk.sage(refund).choose("What should support do?", options);
reading.value; // "refund" | "credit" | "deny" | null
reading.confidence; // number when value is set, otherwise null

const chosen = await sdk.sageAnswer(refund).choose("What should support do?", options);
chosen; // "refund" | "credit" | "deny" | null

The document is the argument to sage and sageAnswer. new Sage(document, options) and new SageAnswer(document, options) are those same classes. options is client options, or an existing SageApi. SageSDK takes them once. Sort does not use that document. It ranks the items you pass.

| Method | Value | Confidence | | --- | --- | --- | | yesOrNo(instructions) | "yes" \| "no" \| null | how sure that answer is; probability stays P(yes) | | choose(instructions, options) | one option, or null | probability of that option, or null | | scale(instructions, levels) | expectation, in level units | the scale confidence, always a number | | gauge(instructions, start, end, step?) | expectation on that integer range | same as scale | | sort(instructions, items) | the items, ranked | number or null | | tag(instructions, tags) | applies per tag | probability per tag |

choose and tag take a record ({ refund: "Cash refund…" }, { spam: "bulk or unsolicited" }) or an array ({ option, description }[], { tag, description }[]). Record keys come out in ordinary property order: integer-like keys first, ascending, then the rest in insertion order. Arrays keep the order you wrote.

gauge is a scale. rangeEnd is inclusive. rangeStep defaults to 1, or to -1 when the range decreases, and rangeEnd has to fall on a step. A gauge has 2 to 26 levels.

Sort items are either { id, content }[] or an array of id → content maps. An object with a string id and a content field is a list item, so a map cannot hold two items named id and content.

On an image, each tag applies as boolean. On text, boolean | null. Pass the image inline with kind: "image" as const.

An options object may carry reasoning, latency_mode, grounding, and signal. Sort cannot be grounded. An image cannot be grounded.

null is Sage saying it is not sure. It is HTTP 200. Do not retry it. On yes/no, confidence is null together with value, and probability is still P(yes). On choice, confidence is null together with value. Scale and gauge always return a number; a weak score is a low confidence, not null.

Pass the object inline (or as const on a variable). A widened string stays string.

import { SageApi } from "sage-sdk";

const api = new SageApi({ apiKey: process.env.SAGE_API_KEY });

const decision = await api.decide({
  content: refund,
  question: {
    kind: "choice",
    id: "refund",
    instructions: "What should support do?",
    options: [
      { option: "refund", description: "Cash refund within policy" },
      { option: "credit", description: "Store credit only" },
      { option: "deny", description: "Outside policy" },
    ],
  },
});

decision.result.chosen; // "refund" | "credit" | "deny" | null

Tag rows on SageApi stay in request order (result.tags[0].id is the first id you sent). indexTags turns that array into a record keyed by id.

| Call | Endpoint | | --- | --- | | Sage / SageAnswer methods, from SageSDK or directly | POST /decide | | decide | POST /decide | | batch | POST /decide/batch | | estimate | POST /usage/estimate | | ready | GET /ready (no key) |

Auth is Authorization: Bearer. apiKey wins, otherwise SAGE_API_KEY when process exists. The key is not logged. Default timeout is 30s, one shared deadline for the call and a single 503 retry. retry-after is honored up to 2s. A blank retry-after waits 200ms. 400, 401, and 402 are not retried.

Errors are one SageError. kind is "http" | "protocol" | "timeout" | "network" | "invalid". status is the HTTP code, or null.

const sdk = new SageSDK({ stub: true });
const offline = sdk.sage("contract");
const api = new SageApi({ stub: true });

Stub mode does not use the network. The seed is FNV-1a over an allowlist of the request, not JSON.stringify: object keys are sorted, array order is kept, unknown keys and the deprecated tag threshold are ignored. Omitted grounding and grounding: null match. A null option description, level description, tag name, or image text matches omitting it. Explicit reasoning: "off" and latency_mode: "fast" do not match omitting those fields, so a future server default cannot move old seeds. Strings, including image media, are hashed as UTF-16 code units with no Unicode normalization. About one request in five comes back unsure (null), from a fixed bucket of that hash. Image tags are true or false, matching the live API. Choice, scale, sort, and tags stay inside the options, levels, ids, and tags you sent. Each batch answer is seeded from that question, its content, and the group and question indexes, not from its siblings. meta.model is "levanto-sage-stub" and latency_ms is 0.

Scale expectation is a number in your level units, not a level literal. The level literals are on result.probabilities[i].level. Sort sorted is a permutation of your item ids; each index is the union of those ids.

POST /v1/systemone (the Jev-compatible body) is not wrapped. Confidence on that route is Sage's max probability, not TypeSafe's formula. Call it with fetch if you are moving an existing Jev client.