@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.lock +1625 -0
- package/Cargo.toml +38 -0
- package/README.md +89 -0
- package/__test__/gateway.test.mjs +860 -0
- package/__test__/typed_error.test.mjs +48 -0
- package/artifacts/sandhi.darwin-arm64.node +0 -0
- package/artifacts/sandhi.linux-x64-gnu.node +0 -0
- package/build.rs +3 -0
- package/contracts.d.ts +63 -0
- package/index.d.ts +143 -0
- package/index.js +326 -0
- package/npm/darwin-arm64/package.json +19 -0
- package/npm/darwin-arm64/sandhi.darwin-arm64.node +0 -0
- package/npm/linux-x64-gnu/package.json +22 -0
- package/npm/linux-x64-gnu/sandhi.linux-x64-gnu.node +0 -0
- package/package.json +45 -0
- package/sandhi.d.ts +22 -0
- package/sandhi.js +113 -0
- package/sandhi.linux-x64-gnu.node +0 -0
- package/src/lib.rs +834 -0
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.
|