@primeradianthq/obol 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/ffi-bun-C6QZKITS.js +42 -0
- package/dist/ffi-node-27DIUBG7.js +44 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +101 -0
- package/native/darwin-arm64/libobol_ffi.dylib +0 -0
- package/native/darwin-x64/libobol_ffi.dylib +0 -0
- package/native/linux-arm64/libobol_ffi.so +0 -0
- package/native/linux-x64/libobol_ffi.so +0 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# obol — TypeScript binding (Bun + Node)
|
|
2
|
+
|
|
3
|
+
A thin TypeScript binding over obol's C ABI (`obol-ffi`). It runs under **both Bun and Node**:
|
|
4
|
+
Bun uses the built-in `bun:ffi` (zero runtime deps), Node uses [`koffi`](https://koffi.dev). The
|
|
5
|
+
Rust core does all the accounting; this binding only `dlopen`s the cdylib and re-types the JSON.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @primeradianthq/obol # or: bun add @primeradianthq/obol
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The published package **bundles the native library** for macOS (arm64 / x64) and Linux
|
|
14
|
+
(x64 / arm64) — no `cargo build` needed for consumers, no postinstall, no network at install.
|
|
15
|
+
Requires **Node 18+** (or Bun). Other platforms (Windows, musl/Alpine) aren't bundled yet and
|
|
16
|
+
will get a clear "library not found" error.
|
|
17
|
+
|
|
18
|
+
### From source (contributors)
|
|
19
|
+
|
|
20
|
+
Running the binding straight from this repo (not the published package) needs the cdylib built:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
cargo build -p obol-ffi # produces target/{debug,release}/libobol_ffi.{dylib,so}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The library is resolved in order: `$OBOL_LIB` (explicit path) → the package's bundled
|
|
27
|
+
`native/<platform>-<arch>/` (published installs) → `target/{release,debug}` (in-repo dev).
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { estimatePath, estimateBytes, refresh, version, ObolError } from "obol";
|
|
33
|
+
|
|
34
|
+
const est = await estimatePath("session.jsonl", "claude"); // dialect optional → auto-detect
|
|
35
|
+
console.log(est.total_usd, est.pricing_as_of);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await estimateBytes(new Uint8Array(/* … */));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (e instanceof ObolError) console.error(e.code, e.kind, e.message);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The API is async because the FFI backend is loaded lazily (and cached) on first use.
|
|
45
|
+
|
|
46
|
+
Pricing tables must exist before estimating — run `obol refresh` (the CLI) or point
|
|
47
|
+
`OBOL_PRICING_DIR` at a directory containing a `current.json` snapshot.
|
|
48
|
+
|
|
49
|
+
## Ownership
|
|
50
|
+
|
|
51
|
+
You never touch raw pointers. Each call copies obol's returned string into a JS string and then
|
|
52
|
+
frees the obol-owned pointer (via `obol_string_free`) inside the adapter — the single place that
|
|
53
|
+
can get it right.
|
|
54
|
+
|
|
55
|
+
## Bun environment caveat
|
|
56
|
+
|
|
57
|
+
obol's Rust core reads `OBOL_PRICING_DIR` / `OBOL_LIB` from the OS environment via `getenv`, which
|
|
58
|
+
is resolved per call. **Under Bun, mutating `process.env` at runtime does not reach the native
|
|
59
|
+
library** — set these variables in the environment *before launching* the process (the normal
|
|
60
|
+
way). Node propagates `process.env` to `getenv`, so runtime mutation works there; Bun does not.
|
|
61
|
+
(The test suite works around this by calling libc `setenv` directly under Bun — see
|
|
62
|
+
`test/pricing-env.ts`.)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/ffi-bun.ts
|
|
2
|
+
import { dlopen, FFIType, ptr, CString } from "bun:ffi";
|
|
3
|
+
function createBackend(libPath) {
|
|
4
|
+
const { symbols } = dlopen(libPath, {
|
|
5
|
+
obol_version: { args: [], returns: FFIType.ptr },
|
|
6
|
+
obol_string_free: { args: [FFIType.ptr], returns: FFIType.void },
|
|
7
|
+
obol_estimate_path: { args: [FFIType.cstring, FFIType.cstring, FFIType.ptr], returns: FFIType.i32 },
|
|
8
|
+
obol_estimate_bytes: { args: [FFIType.ptr, FFIType.u64, FFIType.cstring, FFIType.ptr], returns: FFIType.i32 },
|
|
9
|
+
obol_refresh_pricing: { args: [FFIType.cstring, FFIType.ptr], returns: FFIType.i32 }
|
|
10
|
+
});
|
|
11
|
+
const cstr = (s) => s === null ? null : Buffer.from(s + "\0");
|
|
12
|
+
const nonEmpty = (data) => data.length === 0 ? new Uint8Array(1) : data;
|
|
13
|
+
const drain = (code, out) => {
|
|
14
|
+
const p = out[0];
|
|
15
|
+
if (p === 0n) return { code, json: null };
|
|
16
|
+
const json = new CString(Number(p)).toString();
|
|
17
|
+
symbols.obol_string_free(Number(p));
|
|
18
|
+
return { code, json };
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
version: () => new CString(symbols.obol_version()).toString(),
|
|
22
|
+
// static; never freed
|
|
23
|
+
estimatePath(path, dialect) {
|
|
24
|
+
const out = new BigUint64Array(1);
|
|
25
|
+
const code = symbols.obol_estimate_path(cstr(path), cstr(dialect), ptr(out));
|
|
26
|
+
return drain(code, out);
|
|
27
|
+
},
|
|
28
|
+
estimateBytes(data, dialect) {
|
|
29
|
+
const out = new BigUint64Array(1);
|
|
30
|
+
const code = symbols.obol_estimate_bytes(ptr(nonEmpty(data)), BigInt(data.length), cstr(dialect), ptr(out));
|
|
31
|
+
return drain(code, out);
|
|
32
|
+
},
|
|
33
|
+
refresh(asOf) {
|
|
34
|
+
const out = new BigUint64Array(1);
|
|
35
|
+
const code = symbols.obol_refresh_pricing(cstr(asOf), ptr(out));
|
|
36
|
+
return drain(code, out);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
createBackend
|
|
42
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/ffi-node.ts
|
|
2
|
+
import koffi from "koffi";
|
|
3
|
+
function createBackend(libPath) {
|
|
4
|
+
const lib = koffi.load(libPath);
|
|
5
|
+
const obol_version = lib.func("const char* obol_version()");
|
|
6
|
+
const obol_string_free = lib.func("void obol_string_free(void* s)");
|
|
7
|
+
const obol_estimate_path = lib.func(
|
|
8
|
+
"int obol_estimate_path(const char* path, const char* dialect, _Out_ void** out)"
|
|
9
|
+
);
|
|
10
|
+
const obol_estimate_bytes = lib.func(
|
|
11
|
+
"int obol_estimate_bytes(const uint8_t* data, size_t len, const char* dialect, _Out_ void** out)"
|
|
12
|
+
);
|
|
13
|
+
const obol_refresh = lib.func("int obol_refresh_pricing(const char* as_of, _Out_ void** out)");
|
|
14
|
+
const nonEmpty = (data) => data.length === 0 ? Buffer.alloc(1) : data;
|
|
15
|
+
const drain = (code, out) => {
|
|
16
|
+
const p = out[0];
|
|
17
|
+
if (p === null || p === void 0) return { code, json: null };
|
|
18
|
+
const json = koffi.decode(p, "char", -1);
|
|
19
|
+
obol_string_free(p);
|
|
20
|
+
return { code, json };
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
version: () => obol_version(),
|
|
24
|
+
// koffi marshals const char* return to a JS string
|
|
25
|
+
estimatePath(path, dialect) {
|
|
26
|
+
const out = [null];
|
|
27
|
+
const code = obol_estimate_path(path, dialect, out);
|
|
28
|
+
return drain(code, out);
|
|
29
|
+
},
|
|
30
|
+
estimateBytes(data, dialect) {
|
|
31
|
+
const out = [null];
|
|
32
|
+
const code = obol_estimate_bytes(nonEmpty(data), data.length, dialect, out);
|
|
33
|
+
return drain(code, out);
|
|
34
|
+
},
|
|
35
|
+
refresh(asOf) {
|
|
36
|
+
const out = [null];
|
|
37
|
+
const code = obol_refresh(asOf, out);
|
|
38
|
+
return drain(code, out);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
createBackend
|
|
44
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
interface TokenBuckets {
|
|
2
|
+
input: number;
|
|
3
|
+
output: number;
|
|
4
|
+
cache_read: number;
|
|
5
|
+
cache_write: number;
|
|
6
|
+
}
|
|
7
|
+
interface ModelCost {
|
|
8
|
+
model: string;
|
|
9
|
+
provider: string;
|
|
10
|
+
tokens: TokenBuckets;
|
|
11
|
+
subtotal_usd: number;
|
|
12
|
+
}
|
|
13
|
+
interface Approximation {
|
|
14
|
+
kind: string;
|
|
15
|
+
detail?: string;
|
|
16
|
+
}
|
|
17
|
+
interface CostEstimate {
|
|
18
|
+
total_usd: number;
|
|
19
|
+
per_model: ModelCost[];
|
|
20
|
+
tokens: TokenBuckets;
|
|
21
|
+
unpriced_models: string[];
|
|
22
|
+
approximations: Approximation[];
|
|
23
|
+
pricing_as_of: string;
|
|
24
|
+
}
|
|
25
|
+
interface RefreshReport {
|
|
26
|
+
models: number;
|
|
27
|
+
as_of: string;
|
|
28
|
+
written_to: string;
|
|
29
|
+
}
|
|
30
|
+
type Dialect = "claude" | "codex" | "pi";
|
|
31
|
+
declare class ObolError extends Error {
|
|
32
|
+
code: number;
|
|
33
|
+
kind: string;
|
|
34
|
+
constructor(code: number, kind: string, message: string);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
declare function version(): Promise<string>;
|
|
38
|
+
declare function estimatePath(path: string, dialect?: Dialect | null): Promise<CostEstimate>;
|
|
39
|
+
declare function estimateBytes(data: Uint8Array, dialect?: Dialect | null): Promise<CostEstimate>;
|
|
40
|
+
declare function refresh(asOf: string): Promise<RefreshReport>;
|
|
41
|
+
|
|
42
|
+
export { type Approximation, type CostEstimate, type Dialect, type ModelCost, ObolError, type RefreshReport, type TokenBuckets, estimateBytes, estimatePath, refresh, version };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/lib-path.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
function libFilename() {
|
|
6
|
+
switch (process.platform) {
|
|
7
|
+
case "darwin":
|
|
8
|
+
return "libobol_ffi.dylib";
|
|
9
|
+
case "win32":
|
|
10
|
+
return "obol_ffi.dll";
|
|
11
|
+
default:
|
|
12
|
+
return "libobol_ffi.so";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function resolveLibPath() {
|
|
16
|
+
const tried = [];
|
|
17
|
+
const env = process.env.OBOL_LIB;
|
|
18
|
+
if (env) {
|
|
19
|
+
tried.push(env);
|
|
20
|
+
if (existsSync(env)) return env;
|
|
21
|
+
}
|
|
22
|
+
const name = libFilename();
|
|
23
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const bundled = join(here, "..", "native", `${process.platform}-${process.arch}`, name);
|
|
25
|
+
tried.push(bundled);
|
|
26
|
+
if (existsSync(bundled)) return bundled;
|
|
27
|
+
const repo = join(here, "..", "..", "..");
|
|
28
|
+
for (const profile of ["release", "debug"]) {
|
|
29
|
+
const p = join(repo, "target", profile, name);
|
|
30
|
+
tried.push(p);
|
|
31
|
+
if (existsSync(p)) return p;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(
|
|
34
|
+
"obol_ffi shared library not found. Set OBOL_LIB or install a platform with a bundled lib. Tried:\n " + tried.join("\n ")
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/ffi.ts
|
|
39
|
+
var cached;
|
|
40
|
+
function backend() {
|
|
41
|
+
return cached ??= load();
|
|
42
|
+
}
|
|
43
|
+
async function load() {
|
|
44
|
+
const isBun = typeof globalThis.Bun !== "undefined";
|
|
45
|
+
const libPath = resolveLibPath();
|
|
46
|
+
const mod = isBun ? await import("./ffi-bun-C6QZKITS.js") : await import("./ffi-node-27DIUBG7.js");
|
|
47
|
+
return mod.createBackend(libPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/types.ts
|
|
51
|
+
var ObolError = class extends Error {
|
|
52
|
+
code;
|
|
53
|
+
kind;
|
|
54
|
+
constructor(code, kind, message) {
|
|
55
|
+
super(`obol: ${kind} (code ${code}): ${message}`);
|
|
56
|
+
this.name = "ObolError";
|
|
57
|
+
this.code = code;
|
|
58
|
+
this.kind = kind;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/index.ts
|
|
63
|
+
function unwrap(r) {
|
|
64
|
+
if (r.code !== 0) {
|
|
65
|
+
let kind = "Unknown";
|
|
66
|
+
let message = "no detail";
|
|
67
|
+
let code = r.code;
|
|
68
|
+
if (r.json) {
|
|
69
|
+
try {
|
|
70
|
+
const e = JSON.parse(r.json).error;
|
|
71
|
+
if (e) {
|
|
72
|
+
kind = e.kind ?? kind;
|
|
73
|
+
message = e.message ?? message;
|
|
74
|
+
code = e.code ?? code;
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw new ObolError(code, kind, message);
|
|
80
|
+
}
|
|
81
|
+
return JSON.parse(r.json);
|
|
82
|
+
}
|
|
83
|
+
async function version() {
|
|
84
|
+
return (await backend()).version();
|
|
85
|
+
}
|
|
86
|
+
async function estimatePath(path, dialect = null) {
|
|
87
|
+
return unwrap((await backend()).estimatePath(path, dialect));
|
|
88
|
+
}
|
|
89
|
+
async function estimateBytes(data, dialect = null) {
|
|
90
|
+
return unwrap((await backend()).estimateBytes(data, dialect));
|
|
91
|
+
}
|
|
92
|
+
async function refresh(asOf) {
|
|
93
|
+
return unwrap((await backend()).refresh(asOf));
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
ObolError,
|
|
97
|
+
estimateBytes,
|
|
98
|
+
estimatePath,
|
|
99
|
+
refresh,
|
|
100
|
+
version
|
|
101
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@primeradianthq/obol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Agent-transcript cost estimation — TypeScript binding over the obol C ABI (Bun + Node).",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"native",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"koffi": "2.16.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"tsup": "^8.5",
|
|
26
|
+
"typescript": "^5.9"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/prime-radiant-inc/obol.git",
|
|
31
|
+
"directory": "bindings/typescript"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|