@anvailabs/sandhi 0.5.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/Cargo.toml ADDED
@@ -0,0 +1,38 @@
1
+ [package]
2
+ name = "sandhi-node"
3
+ description = "napi binding for Sandhi (published to npm as @anvai-labs/sandhi)."
4
+ version = "0.0.0"
5
+ edition = "2021"
6
+ license = "Apache-2.0"
7
+ repository = "https://github.com/anvai-labs/sandhi"
8
+ publish = false
9
+
10
+ [lib]
11
+ crate-type = ["cdylib"]
12
+
13
+ [dependencies]
14
+ # tokio_rt: lets `#[napi] async fn` bridge a Rust future to a JS Promise on napi's own tokio
15
+ # runtime (the napi analogue of PyO3's pyo3-async-runtimes) — powers the transport surface.
16
+ napi = { version = "2", default-features = false, features = ["napi4", "tokio_rt"] }
17
+ napi-derive = "2"
18
+ sandhi-core = { path = "../../crates/sandhi-core" }
19
+ serde_json = "1"
20
+ reqwest = { version = "0.12", default-features = false }
21
+ time = { version = "0.3", features = ["formatting"] }
22
+
23
+ # Transport binding (ADR-0047 D10 step 3c — napi/TS parity): expose sandhi-providers' async provider
24
+ # transport to Node. `tokio` (sync) supplies the mpsc channel that decouples the background stream
25
+ # driver from the JS-side pull; `futures-util` drives the Rust `Stream`.
26
+ sandhi-providers = { path = "../../crates/sandhi-providers" }
27
+ tokio = { version = "1", features = ["sync"] }
28
+ futures-util = "0.3"
29
+
30
+ [build-dependencies]
31
+ napi-build = "2"
32
+
33
+ [profile.release]
34
+ lto = true
35
+ strip = "symbols"
36
+
37
+ # Its OWN workspace root — built via napi, not the root `cargo build --workspace`.
38
+ [workspace]
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @anvailabs/sandhi
2
+
3
+ Node binding for [**Sandhi**](https://github.com/anvai-labs/sandhi) — *the metering layer for
4
+ AI agents*. The Rust core runs in-process through napi-rs. It exposes **metering** (virtual keys,
5
+ budgets, neutral usage-event emission — zero network hop) and a typed persistent provider runtime.
6
+
7
+ ```bash
8
+ # The npm package is not published yet; build the binding from a source checkout.
9
+ cd bindings/node
10
+ npm ci
11
+ npm run build
12
+ npm test
13
+ ```
14
+
15
+ ```js
16
+ import { Gateway, parseUsage } from "./sandhi.js";
17
+
18
+ const gw = new Gateway("usage.jsonl"); // events append as JSONL (+ in-memory)
19
+ gw.addVirtualKey("vk_alice", "alice", "platform", "anthropic");
20
+ gw.setBudget("group:platform", 1_000_000);
21
+
22
+ // ... you make your own provider call and get the raw response JSON ...
23
+ const event = gw.meter("vk_alice", "anthropic", "claude-x", responseJson, "conv_7");
24
+ // event.tokensIn, event.cacheReadTokens, event.subjectId, ...
25
+ gw.spent("group:platform"); // budget recorded
26
+ gw.checkBudget("group:platform", 5000); // true/false
27
+
28
+ parseUsage("openai", responseJson); // { tokensIn, tokensOut, cache* }
29
+ ```
30
+
31
+ ### Custom / unknown providers (host escape hatch)
32
+
33
+ For a provider Sandhi doesn't natively parse, do your own parsing and pass the counts:
34
+
35
+ ```js
36
+ gw.meterTokens("vk_alice", "myprovider", "model", tokensIn, tokensOut);
37
+ ```
38
+
39
+ `meter()` parses usage **at the source** (same Rust parsers as the proxy), attributes it to the
40
+ virtual key's subject/group, records the budget, emits the neutral event, and returns it.
41
+ Unknown key or bad JSON → throws.
42
+
43
+ ### Usage snapshots (in-process aggregation)
44
+
45
+ ```js
46
+ const rows = JSON.parse(gw.usageSnapshotJson("subject")); // busiest subject first
47
+ rows[0].billable_tokens; // what budgets enforce on
48
+ JSON.parse(gw.usageSnapshotJson("total"))[0]; // one grand-total row
49
+ JSON.parse(gw.usageSnapshotJson("session", 256)); // bound distinct keys to 256
50
+ ```
51
+
52
+ Folds the events recorded so far into
53
+ [`usage-aggregate.v1`](https://github.com/anvai-labs/sandhi/blob/main/schemas/usage-aggregate.v1.schema.json)
54
+ rows for one dimension — `subject` (`user`), `group`, `provider`, `model`, `key`
55
+ (`virtual_key`), `session`, or `total` — using the same fold the reverse proxy, the `sandhi` CLI,
56
+ and the dashboard read. Neutral units only, never dollars. The rows are the schema'd contract
57
+ shape (snake_case), not napi camelCase. The optional second argument caps distinct keys (default
58
+ 1024); everything past it folds into a single `"(overflow)"` row, so a long-lived process loses
59
+ per-key detail but never the sum. Unknown dimension → throws.
60
+
61
+ ### Provider transport (in-process)
62
+
63
+ Reuse a provider handle so its HTTP pool, retry policy, timeouts, and circuit breaker survive across
64
+ calls. Inputs and outputs are serialized Sandhi chat-contract v1 documents rather than
65
+ provider-native JSON:
66
+
67
+ ```js
68
+ import { ProviderRuntime } from "./sandhi.js";
69
+
70
+ const runtime = new ProviderRuntime();
71
+ const provider = runtime.provider("openrouter", "openai/gpt-4o", API_KEY);
72
+ const request = JSON.stringify({
73
+ schema_version: "1",
74
+ model: "openai/gpt-4o",
75
+ messages: [{ role: "user", content: "hello" }],
76
+ });
77
+
78
+ const response = JSON.parse(await provider.completeJson(request));
79
+ for await (const eventJson of provider.streamJson(request)) {
80
+ const event = JSON.parse(eventJson); // response_start, text_delta, usage, finish, ...
81
+ }
82
+ ```
83
+
84
+ `completeJson` accepts an optional second JSON-string argument containing per-call wire headers;
85
+ `streamJson` accepts the same argument and returns an async iterable. Transport-owned headers and
86
+ credentials cannot be overridden. Invalid documents fail before network I/O; provider failures
87
+ throw `SandhiProviderError` with a structured `ProviderErrorV1` payload.
88
+
89
+ Apache-2.0. The transport surface links `sandhi-providers` (async HTTP stack) into the addon.