@primeradianthq/obol
v0.6.0
Published
Agent-transcript cost estimation — TypeScript binding over the obol C ABI (Bun + Node).
Downloads
1,597
Readme
obol — TypeScript binding (Bun + Node)
A thin TypeScript binding over obol's C ABI (obol-ffi). It runs under both Bun and Node:
Bun uses the built-in bun:ffi (zero runtime deps), Node uses koffi. The
Rust core does all the accounting; this binding only dlopens the cdylib and re-types the JSON.
Install
npm install @primeradianthq/obol # or: bun add @primeradianthq/obolThe published package bundles the native library for macOS (arm64 / x64) and Linux
(x64 / arm64) — no cargo build needed for consumers, no postinstall, no network at install.
Requires Node 18+ (or Bun). Other platforms (Windows, musl/Alpine) aren't bundled yet and
will get a clear "library not found" error.
From source (contributors)
Running the binding straight from this repo (not the published package) needs the cdylib built:
cargo build -p obol-ffi # produces target/{debug,release}/libobol_ffi.{dylib,so}The library is resolved in order: $OBOL_LIB (explicit path) → the package's bundled
native/<platform>-<arch>/ (published installs) → target/{release,debug} (in-repo dev).
Usage
import { estimatePath, refresh, version, ObolError } from "obol";
const est = await estimatePath("session.jsonl", "claude");
console.log(est.total_usd, est.pricing_as_of);
try {
await estimatePath("session.jsonl", "gemini");
} catch (e) {
if (e instanceof ObolError) console.error(e.code, e.kind, e.message);
}The API is async because the FFI backend is loaded lazily (and cached) on first use.
Pricing tables must exist before estimating — run obol refresh (the CLI) or point
OBOL_PRICING_DIR at a directory containing a current.json snapshot.
Pinning the pricing dir at runtime
To set OBOL_PRICING_DIR after the process has started, use the exported helpers rather than
writing process.env directly — under Bun, a runtime process.env write does not reach the
native environment the FFI (and Rust's getenv) observes, so the value is silently ignored. The
helpers call libc setenv/unsetenv under Bun (and set process.env for Node), so they work on
both runtimes:
import { setPricingDir, clearPricingDir } from "obol";
await setPricingDir("/path/to/pricing-dir"); // a dir containing current.json
// … estimatePath(...) …
await clearPricingDir();Ownership
You never touch raw pointers. Each call copies obol's returned string into a JS string and then
frees the obol-owned pointer (via obol_string_free) inside the adapter — the single place that
can get it right.
Bun environment caveat
obol's Rust core reads OBOL_PRICING_DIR / OBOL_LIB from the OS environment via getenv, which
is resolved per call. Under Bun, mutating process.env at runtime does not reach the native
library — set these variables in the environment before launching the process (the normal
way). Node propagates process.env to getenv, so runtime mutation works there; Bun does not.
(The test suite works around this by calling libc setenv directly under Bun — see
test/pricing-env.ts.)
