@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.
@@ -0,0 +1,48 @@
1
+ // SandhiProviderError rewrap contract (TD-0008 P2/C): typed payloads become the
2
+ // class; binding-internal failures pass through untouched.
3
+ import assert from "node:assert/strict";
4
+ import { test } from "node:test";
5
+ import { createRequire } from "node:module";
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const sandhi = require("../sandhi.js");
9
+
10
+ test("SandhiProviderError exposes typed fields from a ProviderErrorV1 payload", () => {
11
+ const payload = {
12
+ code: "upstream_error",
13
+ message: "upstream status 400: bad tool pairing",
14
+ retryable: false,
15
+ http_status: 400,
16
+ provider: "moonshot",
17
+ request_id: "req_abc",
18
+ details: { upstream_body: "{\"error\":\"bad tool pairing\"}" },
19
+ };
20
+ const err = new sandhi.SandhiProviderError(payload, JSON.stringify(payload));
21
+ assert.equal(err.name, "SandhiProviderError");
22
+ assert.equal(err.code, "upstream_error");
23
+ assert.equal(err.httpStatus, 400);
24
+ assert.equal(err.retryable, false);
25
+ assert.equal(err.requestId, "req_abc");
26
+ assert.ok(err instanceof Error);
27
+ assert.equal(err.details.upstream_body, "{\"error\":\"bad tool pairing\"}");
28
+ });
29
+
30
+ test("async iterator rewraps typed error messages and passes others through", async () => {
31
+ const typedJson = JSON.stringify({ code: "rate_limited", message: "rate limited (429)", retryable: true, http_status: 429 });
32
+ const makeStream = (error) => {
33
+ const stream = Object.create(sandhi.TypedEventStream.prototype);
34
+ stream.read = async () => { throw error; };
35
+ return stream;
36
+ };
37
+
38
+ await assert.rejects(
39
+ async () => { for await (const _ of makeStream(new Error(typedJson))) { /* drain */ } },
40
+ (err) => err instanceof sandhi.SandhiProviderError && err.code === "rate_limited" && err.retryable === true
41
+ );
42
+
43
+ const plain = new Error("segfault in binding");
44
+ await assert.rejects(
45
+ async () => { for await (const _ of makeStream(plain)) { /* drain */ } },
46
+ (err) => err === plain && !(err instanceof sandhi.SandhiProviderError)
47
+ );
48
+ });
package/build.rs ADDED
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ napi_build::setup();
3
+ }
package/contracts.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ // Generated by scripts/gen-binding-contract-facades.py; DO NOT EDIT.
2
+ // schema-sha256: 0eff7885fe2a40495027f8ae846d4acef93c0b76cf712b6c16f72fa4c97bebe7
3
+ export type JsonValue = unknown
4
+ export type Role = "developer" | "system" | "user" | "assistant" | "tool" | "function"
5
+ export type FinishReasonV1 = "stop" | "length" | "tool_calls" | "content_filter" | "function_call" | "unknown"
6
+ export type UsageCompleteness = "final" | "partial" | "unavailable"
7
+ export type UsageBasis = "provider_reported" | "estimated"
8
+ export type ContentPart =
9
+ | { type: "text"; text: string }
10
+ | { type: "image_url"; image_url: string; detail?: string }
11
+ | { type: "input_audio"; data: string; format: string }
12
+ | { type: "file"; file_id?: string; file_data?: string; filename?: string }
13
+ export type MessageContent = string | ContentPart[]
14
+ export interface ToolCallV1 { id: string; name: string; arguments: string; extensions?: Record<string, JsonValue> }
15
+ export type ChatMessageV1 =
16
+ | { role: "developer" | "system" | "user"; content: MessageContent; name?: string }
17
+ | { role: "assistant"; content?: MessageContent; name?: string; tool_calls?: ToolCallV1[]; refusal?: string }
18
+ | { role: "tool"; content: MessageContent; tool_call_id: string }
19
+ | { role: "function"; content: MessageContent; name: string }
20
+ export interface ToolDefinitionV1 { name: string; parameters: JsonValue; description?: string; strict?: boolean }
21
+ export interface RequestMetadataV1 { session_id?: string; virtual_key_id?: string; subject_id?: string; group_id?: string; route?: string }
22
+ export interface ThinkingV1 { enabled: boolean; budget_tokens?: number }
23
+ export interface ChatRequestV1 {
24
+ schema_version?: "1"; model: string; messages: ChatMessageV1[]; tools?: ToolDefinitionV1[]
25
+ tool_choice?: "none" | "auto" | "required" | { name: string }; temperature?: number
26
+ max_output_tokens?: number; stop?: string[]; response_format?: JsonValue; seed?: number
27
+ metadata?: RequestMetadataV1; reasoning_effort?: string; thinking?: ThinkingV1
28
+ include_native_response?: boolean
29
+ extensions?: Record<string, JsonValue>
30
+ }
31
+ export interface UsageV2 {
32
+ tokens_in: number; tokens_out: number; cache_creation_tokens: number; cache_read_tokens: number
33
+ completeness?: UsageCompleteness; basis?: UsageBasis; attempts?: number; outcome?: string
34
+ upstream_request_id?: string
35
+ duration_ms?: number; time_to_first_token_ms?: number
36
+ audio_input_tokens?: number; audio_output_tokens?: number; reasoning_tokens?: number
37
+ accepted_prediction_tokens?: number; rejected_prediction_tokens?: number
38
+ }
39
+ export interface LatencySummary { samples: number; p50_ms: number; p95_ms: number; ttft_p50_ms?: number }
40
+ export interface UsageAggregateV1 {
41
+ key: string; calls: number; tokens_in: number; tokens_out: number;
42
+ cache_creation_tokens: number; cache_read_tokens: number; reasoning_tokens: number;
43
+ billable_tokens: number; latency?: LatencySummary
44
+ }
45
+ export interface AssistantOutputV1 { content?: MessageContent; tool_calls?: ToolCallV1[]; refusal?: string }
46
+ export interface ChatResponseV1 {
47
+ schema_version: "1"; id?: string; model: string; output: AssistantOutputV1
48
+ finish_reason?: FinishReasonV1; usage: UsageV2; extensions?: Record<string, JsonValue>
49
+ }
50
+ export interface ProviderErrorV1 { code: string; message: string; retryable: boolean; http_status?: number; provider?: string; request_id?: string; details?: Record<string, JsonValue> }
51
+ export type ChatStreamEventV1 =
52
+ | { event: "response_start"; id?: string; model: string }
53
+ | { event: "text_delta" | "reasoning_delta" | "refusal_delta"; delta: string }
54
+ | { event: "tool_call_start"; index: number; id: string; name: string }
55
+ | { event: "tool_call_arguments_delta"; index: number; delta: string }
56
+ | { event: "tool_call_end"; index: number }
57
+ | { event: "usage"; usage: UsageV2 }
58
+ | { event: "finish"; reason: FinishReasonV1 }
59
+ | { event: "error"; error: ProviderErrorV1 }
60
+ export interface ProviderDescriptorV1 {
61
+ schema_version: "1"; slug: string; aliases?: string[]; endpoint_family: string; base_url: string
62
+ capabilities: Record<string, boolean>; models?: JsonValue[]; extensions?: Record<string, JsonValue>
63
+ }
package/index.d.ts ADDED
@@ -0,0 +1,143 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /* auto-generated by NAPI-RS */
5
+
6
+ /** The neutral token breakdown parsed from a provider response. */
7
+ export interface UsageBreakdown {
8
+ tokensIn: number
9
+ tokensOut: number
10
+ cacheCreationTokens: number
11
+ cacheReadTokens: number
12
+ }
13
+ /** A neutral usage event (mirrors `usage-event.v1.schema.json`). */
14
+ export interface Event {
15
+ schemaVersion: string
16
+ requestId: string
17
+ occurredAt: string
18
+ provider: string
19
+ model: string
20
+ backend: string
21
+ virtualKeyId?: string
22
+ subjectId?: string
23
+ groupId?: string
24
+ route?: string
25
+ sessionId?: string
26
+ tokensIn: number
27
+ tokensOut: number
28
+ cacheCreationTokens: number
29
+ cacheReadTokens: number
30
+ usageCompleteness: string
31
+ attempts: number
32
+ outcome?: string
33
+ upstreamRequestId?: string
34
+ gpuSeconds?: number
35
+ }
36
+ /** The usage-event wire-contract major version this build targets. */
37
+ export declare function wireContractVersion(): string
38
+ /**
39
+ * The neutral chat-contract (ChatRequestV1/ChatStreamEventV1) major version this
40
+ * build targets. Today equal to `wire_contract_version()` (pinned by a
41
+ * sandhi-core test); exported separately so a future split cannot silently
42
+ * invalidate consumer handshakes that validate the chat contract.
43
+ */
44
+ export declare function chatContractVersion(): string
45
+ /**
46
+ * Additive growth counter within the v1 chat contract (W3c). Consumers
47
+ * feature-detect this export — an older binding without it reads as minor 0
48
+ * — and gate trust in newer additive fields on a minimum minor.
49
+ */
50
+ export declare function chatContractMinor(): number
51
+ /**
52
+ * Resolve an OpenAI-compatible provider spec (slug, aliases, base_url) as JSON.
53
+ * Parity with the Python binding's `provider_spec` (TD-0008 P4); JSON here
54
+ * because napi objects would otherwise diverge from the schema'd facades.
55
+ */
56
+ export declare function providerSpecJson(provider: string, model?: string | undefined | null): string
57
+ /**
58
+ * Return a checked chat-contract JSON Schema document by name
59
+ * (e.g. "chat-stream-event.v1"). Parity with the Python binding.
60
+ */
61
+ export declare function chatContractSchemaJson(name: string): string
62
+ /** Return the versioned typed descriptor for a known provider as JSON. */
63
+ export declare function providerDescriptorJson(provider: string): string
64
+ /**
65
+ * Return the curated model descriptors for a known provider as a JSON list (TD-0004 catalog DATA).
66
+ * Carries facts only (id, context window, max output, capabilities); no pricing.
67
+ */
68
+ export declare function providerModelsJson(provider: string): string
69
+ /**
70
+ * Parse a provider response body (JSON string) into the neutral token breakdown. `provider`
71
+ * selects the parser: `anthropic` → the Anthropic Messages shape; anything else → OpenAI-compat.
72
+ */
73
+ export declare function parseUsage(provider: string, responseJson: string): UsageBreakdown
74
+ export type JsProviderRuntime = ProviderRuntime
75
+ /**
76
+ * Persistent factory for typed provider handles. The HTTP pool, retry policy, and circuit
77
+ * breaker belong to each returned handle rather than being rebuilt for every request.
78
+ */
79
+ export declare class ProviderRuntime {
80
+ constructor()
81
+ openaiCompat(provider: string, baseUrl: string, apiKey: string, headersJson?: string | undefined | null, maxRetries?: number | undefined | null, timeoutSecs?: number | undefined | null, streamIdleTimeoutSecs?: number | undefined | null): TypedProvider
82
+ openaiResponses(provider: string, baseUrl: string, bearerToken: string, headersJson?: string | undefined | null, maxRetries?: number | undefined | null, timeoutSecs?: number | undefined | null, streamIdleTimeoutSecs?: number | undefined | null): TypedProvider
83
+ provider(provider: string, model: string, apiKey: string, baseUrl?: string | undefined | null, headersJson?: string | undefined | null, maxRetries?: number | undefined | null, timeoutSecs?: number | undefined | null, streamIdleTimeoutSecs?: number | undefined | null, authScheme?: string | undefined | null, protocol?: string | undefined | null): TypedProvider
84
+ }
85
+ /** A persistent provider handle accepting Sandhi chat-contract v1 JSON documents. */
86
+ export declare class TypedProvider {
87
+ get provider(): string
88
+ /**
89
+ * `wireHeadersJson` (optional, TD-0022 D1): per-call wire headers — a string map sent
90
+ * with THIS call only, for gateway-path metadata that changes per turn (e.g.
91
+ * `x-sandhi-step-id`). Transport-owned names are stripped and can never override the
92
+ * credential.
93
+ */
94
+ completeJson(requestJson: string, wireHeadersJson?: string | undefined | null): Promise<string>
95
+ /** `wireHeadersJson` (optional, TD-0022 D1): per-call wire headers, as `completeJson`. */
96
+ streamJson(requestJson: string, wireHeadersJson?: string | undefined | null): TypedEventStream
97
+ }
98
+ /** Pull-based stream of serialized `ChatStreamEventV1` documents. */
99
+ export declare class TypedEventStream {
100
+ read(): Promise<string | null>
101
+ }
102
+ /** In-process metering middleware: virtual keys, budgets, and usage-event emission. */
103
+ export declare class Gateway {
104
+ /** `sinkPath` — append emitted events as JSONL to this file (plus an in-memory buffer). */
105
+ constructor(sinkPath?: string | undefined | null)
106
+ /** Register a virtual key: `id` → subject/group attribution + an opaque `upstream` ref. */
107
+ addVirtualKey(id: string, subject?: string | undefined | null, group?: string | undefined | null, upstream?: string | undefined | null): void
108
+ /**
109
+ * Set a token budget on a scope (e.g. `group:platform`). Exposed as a 64-bit `bigint` (napi
110
+ * has no bare `u64`); negatives clamp to 0 — a budget is a non-negative token count.
111
+ */
112
+ setBudget(scope: string, tokens: number): void
113
+ /** Would `add` more tokens be within the scope's budget? */
114
+ checkBudget(scope: string, add: number): boolean
115
+ /** Tokens spent so far on a scope. */
116
+ spent(scope: string): number
117
+ /**
118
+ * Meter one completed call: parse usage from `responseJson` (built-in parser for `provider`),
119
+ * attribute it to `virtualKey`, emit the neutral event + record the budget, and return the
120
+ * event. Throws on an unknown virtual key or bad JSON.
121
+ */
122
+ meter(virtualKey: string, provider: string, model: string, responseJson: string, sessionId?: string | undefined | null, route?: string | undefined | null): Event
123
+ /**
124
+ * Meter from token counts you supply directly (bypass parsing) — the escape hatch for any
125
+ * provider Sandhi doesn't natively parse: do your own parsing and pass the counts. Same
126
+ * attribution + budget + emit as `meter()`.
127
+ */
128
+ meterTokens(virtualKey: string, provider: string, model: string, tokensIn: number, tokensOut: number, cacheCreationTokens?: number | undefined | null, cacheReadTokens?: number | undefined | null, sessionId?: string | undefined | null, route?: string | undefined | null): Event
129
+ /** All events emitted so far (in-memory). */
130
+ events(): Array<Event>
131
+ /**
132
+ * Fold the events recorded so far into `UsageAggregateV1` rows for one attribution
133
+ * `dimension`, serialized as a JSON array (TD-0009 P2). `dimension` is one of `subject`
134
+ * (`user`), `group`, `provider`, `model`, `key` (`virtual_key`), `session`, or `total`.
135
+ *
136
+ * The fold is `sandhi_core::UsageAggregator` — the same definition the proxy, the CLI, and
137
+ * the dashboard read, so the in-process path cannot disagree with them. `cap` bounds the
138
+ * number of distinct keys before the rest fold into `"(overflow)"` (default 1024): a
139
+ * long-lived process metering unbounded `sessionId`s loses per-key detail, never the sum —
140
+ * the rows always add up to the exact total. Throws on an unknown dimension.
141
+ */
142
+ usageSnapshotJson(dimension: string, cap?: number | undefined | null): string
143
+ }
package/index.js ADDED
@@ -0,0 +1,326 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ /* prettier-ignore */
4
+
5
+ /* auto-generated by NAPI-RS */
6
+
7
+ const { existsSync, readFileSync } = require('fs')
8
+ const { join } = require('path')
9
+
10
+ const { platform, arch } = process
11
+
12
+ let nativeBinding = null
13
+ let localFileExisted = false
14
+ let loadError = null
15
+
16
+ function isMusl() {
17
+ // For Node 10
18
+ if (!process.report || typeof process.report.getReport !== 'function') {
19
+ try {
20
+ const lddPath = require('child_process').execSync('which ldd').toString().trim()
21
+ return readFileSync(lddPath, 'utf8').includes('musl')
22
+ } catch (e) {
23
+ return true
24
+ }
25
+ } else {
26
+ const { glibcVersionRuntime } = process.report.getReport().header
27
+ return !glibcVersionRuntime
28
+ }
29
+ }
30
+
31
+ switch (platform) {
32
+ case 'android':
33
+ switch (arch) {
34
+ case 'arm64':
35
+ localFileExisted = existsSync(join(__dirname, 'sandhi.android-arm64.node'))
36
+ try {
37
+ if (localFileExisted) {
38
+ nativeBinding = require('./sandhi.android-arm64.node')
39
+ } else {
40
+ nativeBinding = require('@anvailabs/sandhi-android-arm64')
41
+ }
42
+ } catch (e) {
43
+ loadError = e
44
+ }
45
+ break
46
+ case 'arm':
47
+ localFileExisted = existsSync(join(__dirname, 'sandhi.android-arm-eabi.node'))
48
+ try {
49
+ if (localFileExisted) {
50
+ nativeBinding = require('./sandhi.android-arm-eabi.node')
51
+ } else {
52
+ nativeBinding = require('@anvailabs/sandhi-android-arm-eabi')
53
+ }
54
+ } catch (e) {
55
+ loadError = e
56
+ }
57
+ break
58
+ default:
59
+ throw new Error(`Unsupported architecture on Android ${arch}`)
60
+ }
61
+ break
62
+ case 'win32':
63
+ switch (arch) {
64
+ case 'x64':
65
+ localFileExisted = existsSync(
66
+ join(__dirname, 'sandhi.win32-x64-msvc.node')
67
+ )
68
+ try {
69
+ if (localFileExisted) {
70
+ nativeBinding = require('./sandhi.win32-x64-msvc.node')
71
+ } else {
72
+ nativeBinding = require('@anvailabs/sandhi-win32-x64-msvc')
73
+ }
74
+ } catch (e) {
75
+ loadError = e
76
+ }
77
+ break
78
+ case 'ia32':
79
+ localFileExisted = existsSync(
80
+ join(__dirname, 'sandhi.win32-ia32-msvc.node')
81
+ )
82
+ try {
83
+ if (localFileExisted) {
84
+ nativeBinding = require('./sandhi.win32-ia32-msvc.node')
85
+ } else {
86
+ nativeBinding = require('@anvailabs/sandhi-win32-ia32-msvc')
87
+ }
88
+ } catch (e) {
89
+ loadError = e
90
+ }
91
+ break
92
+ case 'arm64':
93
+ localFileExisted = existsSync(
94
+ join(__dirname, 'sandhi.win32-arm64-msvc.node')
95
+ )
96
+ try {
97
+ if (localFileExisted) {
98
+ nativeBinding = require('./sandhi.win32-arm64-msvc.node')
99
+ } else {
100
+ nativeBinding = require('@anvailabs/sandhi-win32-arm64-msvc')
101
+ }
102
+ } catch (e) {
103
+ loadError = e
104
+ }
105
+ break
106
+ default:
107
+ throw new Error(`Unsupported architecture on Windows: ${arch}`)
108
+ }
109
+ break
110
+ case 'darwin':
111
+ localFileExisted = existsSync(join(__dirname, 'sandhi.darwin-universal.node'))
112
+ try {
113
+ if (localFileExisted) {
114
+ nativeBinding = require('./sandhi.darwin-universal.node')
115
+ } else {
116
+ nativeBinding = require('@anvailabs/sandhi-darwin-universal')
117
+ }
118
+ break
119
+ } catch {}
120
+ switch (arch) {
121
+ case 'x64':
122
+ localFileExisted = existsSync(join(__dirname, 'sandhi.darwin-x64.node'))
123
+ try {
124
+ if (localFileExisted) {
125
+ nativeBinding = require('./sandhi.darwin-x64.node')
126
+ } else {
127
+ nativeBinding = require('@anvailabs/sandhi-darwin-x64')
128
+ }
129
+ } catch (e) {
130
+ loadError = e
131
+ }
132
+ break
133
+ case 'arm64':
134
+ localFileExisted = existsSync(
135
+ join(__dirname, 'sandhi.darwin-arm64.node')
136
+ )
137
+ try {
138
+ if (localFileExisted) {
139
+ nativeBinding = require('./sandhi.darwin-arm64.node')
140
+ } else {
141
+ nativeBinding = require('@anvailabs/sandhi-darwin-arm64')
142
+ }
143
+ } catch (e) {
144
+ loadError = e
145
+ }
146
+ break
147
+ default:
148
+ throw new Error(`Unsupported architecture on macOS: ${arch}`)
149
+ }
150
+ break
151
+ case 'freebsd':
152
+ if (arch !== 'x64') {
153
+ throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
154
+ }
155
+ localFileExisted = existsSync(join(__dirname, 'sandhi.freebsd-x64.node'))
156
+ try {
157
+ if (localFileExisted) {
158
+ nativeBinding = require('./sandhi.freebsd-x64.node')
159
+ } else {
160
+ nativeBinding = require('@anvailabs/sandhi-freebsd-x64')
161
+ }
162
+ } catch (e) {
163
+ loadError = e
164
+ }
165
+ break
166
+ case 'linux':
167
+ switch (arch) {
168
+ case 'x64':
169
+ if (isMusl()) {
170
+ localFileExisted = existsSync(
171
+ join(__dirname, 'sandhi.linux-x64-musl.node')
172
+ )
173
+ try {
174
+ if (localFileExisted) {
175
+ nativeBinding = require('./sandhi.linux-x64-musl.node')
176
+ } else {
177
+ nativeBinding = require('@anvailabs/sandhi-linux-x64-musl')
178
+ }
179
+ } catch (e) {
180
+ loadError = e
181
+ }
182
+ } else {
183
+ localFileExisted = existsSync(
184
+ join(__dirname, 'sandhi.linux-x64-gnu.node')
185
+ )
186
+ try {
187
+ if (localFileExisted) {
188
+ nativeBinding = require('./sandhi.linux-x64-gnu.node')
189
+ } else {
190
+ nativeBinding = require('@anvailabs/sandhi-linux-x64-gnu')
191
+ }
192
+ } catch (e) {
193
+ loadError = e
194
+ }
195
+ }
196
+ break
197
+ case 'arm64':
198
+ if (isMusl()) {
199
+ localFileExisted = existsSync(
200
+ join(__dirname, 'sandhi.linux-arm64-musl.node')
201
+ )
202
+ try {
203
+ if (localFileExisted) {
204
+ nativeBinding = require('./sandhi.linux-arm64-musl.node')
205
+ } else {
206
+ nativeBinding = require('@anvailabs/sandhi-linux-arm64-musl')
207
+ }
208
+ } catch (e) {
209
+ loadError = e
210
+ }
211
+ } else {
212
+ localFileExisted = existsSync(
213
+ join(__dirname, 'sandhi.linux-arm64-gnu.node')
214
+ )
215
+ try {
216
+ if (localFileExisted) {
217
+ nativeBinding = require('./sandhi.linux-arm64-gnu.node')
218
+ } else {
219
+ nativeBinding = require('@anvailabs/sandhi-linux-arm64-gnu')
220
+ }
221
+ } catch (e) {
222
+ loadError = e
223
+ }
224
+ }
225
+ break
226
+ case 'arm':
227
+ if (isMusl()) {
228
+ localFileExisted = existsSync(
229
+ join(__dirname, 'sandhi.linux-arm-musleabihf.node')
230
+ )
231
+ try {
232
+ if (localFileExisted) {
233
+ nativeBinding = require('./sandhi.linux-arm-musleabihf.node')
234
+ } else {
235
+ nativeBinding = require('@anvailabs/sandhi-linux-arm-musleabihf')
236
+ }
237
+ } catch (e) {
238
+ loadError = e
239
+ }
240
+ } else {
241
+ localFileExisted = existsSync(
242
+ join(__dirname, 'sandhi.linux-arm-gnueabihf.node')
243
+ )
244
+ try {
245
+ if (localFileExisted) {
246
+ nativeBinding = require('./sandhi.linux-arm-gnueabihf.node')
247
+ } else {
248
+ nativeBinding = require('@anvailabs/sandhi-linux-arm-gnueabihf')
249
+ }
250
+ } catch (e) {
251
+ loadError = e
252
+ }
253
+ }
254
+ break
255
+ case 'riscv64':
256
+ if (isMusl()) {
257
+ localFileExisted = existsSync(
258
+ join(__dirname, 'sandhi.linux-riscv64-musl.node')
259
+ )
260
+ try {
261
+ if (localFileExisted) {
262
+ nativeBinding = require('./sandhi.linux-riscv64-musl.node')
263
+ } else {
264
+ nativeBinding = require('@anvailabs/sandhi-linux-riscv64-musl')
265
+ }
266
+ } catch (e) {
267
+ loadError = e
268
+ }
269
+ } else {
270
+ localFileExisted = existsSync(
271
+ join(__dirname, 'sandhi.linux-riscv64-gnu.node')
272
+ )
273
+ try {
274
+ if (localFileExisted) {
275
+ nativeBinding = require('./sandhi.linux-riscv64-gnu.node')
276
+ } else {
277
+ nativeBinding = require('@anvailabs/sandhi-linux-riscv64-gnu')
278
+ }
279
+ } catch (e) {
280
+ loadError = e
281
+ }
282
+ }
283
+ break
284
+ case 's390x':
285
+ localFileExisted = existsSync(
286
+ join(__dirname, 'sandhi.linux-s390x-gnu.node')
287
+ )
288
+ try {
289
+ if (localFileExisted) {
290
+ nativeBinding = require('./sandhi.linux-s390x-gnu.node')
291
+ } else {
292
+ nativeBinding = require('@anvailabs/sandhi-linux-s390x-gnu')
293
+ }
294
+ } catch (e) {
295
+ loadError = e
296
+ }
297
+ break
298
+ default:
299
+ throw new Error(`Unsupported architecture on Linux: ${arch}`)
300
+ }
301
+ break
302
+ default:
303
+ throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
304
+ }
305
+
306
+ if (!nativeBinding) {
307
+ if (loadError) {
308
+ throw loadError
309
+ }
310
+ throw new Error(`Failed to load native binding`)
311
+ }
312
+
313
+ const { ProviderRuntime, TypedProvider, TypedEventStream, wireContractVersion, chatContractVersion, chatContractMinor, providerSpecJson, chatContractSchemaJson, providerDescriptorJson, providerModelsJson, parseUsage, Gateway } = nativeBinding
314
+
315
+ module.exports.ProviderRuntime = ProviderRuntime
316
+ module.exports.TypedProvider = TypedProvider
317
+ module.exports.TypedEventStream = TypedEventStream
318
+ module.exports.wireContractVersion = wireContractVersion
319
+ module.exports.chatContractVersion = chatContractVersion
320
+ module.exports.chatContractMinor = chatContractMinor
321
+ module.exports.providerSpecJson = providerSpecJson
322
+ module.exports.chatContractSchemaJson = chatContractSchemaJson
323
+ module.exports.providerDescriptorJson = providerDescriptorJson
324
+ module.exports.providerModelsJson = providerModelsJson
325
+ module.exports.parseUsage = parseUsage
326
+ module.exports.Gateway = Gateway
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@anvailabs/sandhi-darwin-arm64",
3
+ "version": "0.5.0",
4
+ "main": "sandhi.darwin-arm64.node",
5
+ "files": [
6
+ "sandhi.darwin-arm64.node"
7
+ ],
8
+ "description": "Platform binary for @anvailabs/sandhi (darwin-arm64)",
9
+ "license": "Apache-2.0",
10
+ "engines": {
11
+ "node": ">= 10"
12
+ },
13
+ "cpu": [
14
+ "arm64"
15
+ ],
16
+ "os": [
17
+ "darwin"
18
+ ]
19
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@anvailabs/sandhi-linux-x64-gnu",
3
+ "version": "0.5.0",
4
+ "main": "sandhi.linux-x64-gnu.node",
5
+ "files": [
6
+ "sandhi.linux-x64-gnu.node"
7
+ ],
8
+ "description": "Platform binary for @anvailabs/sandhi (linux-x64-gnu)",
9
+ "license": "Apache-2.0",
10
+ "engines": {
11
+ "node": ">= 10"
12
+ },
13
+ "cpu": [
14
+ "x64"
15
+ ],
16
+ "os": [
17
+ "linux"
18
+ ],
19
+ "libc": [
20
+ "glibc"
21
+ ]
22
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@anvailabs/sandhi",
3
+ "version": "0.5.0",
4
+ "description": "Sandhi — the metering layer for AI agents (Node binding): in-process virtual keys, budgets, and neutral usage-event metering.",
5
+ "license": "Apache-2.0",
6
+ "repository": "https://github.com/anvai-labs/sandhi",
7
+ "keywords": [
8
+ "llm",
9
+ "gateway",
10
+ "usage",
11
+ "metering",
12
+ "attribution",
13
+ "ai"
14
+ ],
15
+ "main": "sandhi.js",
16
+ "types": "sandhi.d.ts",
17
+ "napi": {
18
+ "name": "sandhi",
19
+ "triples": {
20
+ "defaults": true,
21
+ "additional": [
22
+ "aarch64-apple-darwin",
23
+ "aarch64-unknown-linux-gnu"
24
+ ]
25
+ }
26
+ },
27
+ "engines": {
28
+ "node": ">= 16"
29
+ },
30
+ "scripts": {
31
+ "build": "napi build --platform --release",
32
+ "build:debug": "napi build --platform",
33
+ "test": "node --test __test__/gateway.test.mjs"
34
+ },
35
+ "devDependencies": {
36
+ "@napi-rs/cli": "^2.18.4"
37
+ },
38
+ "optionalDependencies": {
39
+ "@anvailabs/sandhi-win32-x64-msvc": "0.5.0",
40
+ "@anvailabs/sandhi-darwin-x64": "0.5.0",
41
+ "@anvailabs/sandhi-linux-x64-gnu": "0.5.0",
42
+ "@anvailabs/sandhi-darwin-arm64": "0.5.0",
43
+ "@anvailabs/sandhi-linux-arm64-gnu": "0.5.0"
44
+ }
45
+ }