modelrig 0.8.0 → 0.8.1

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/CHANGELOG.md CHANGED
@@ -10,6 +10,17 @@ published package diverge.
10
10
  This project adheres to [Semantic Versioning](https://semver.org/). Dates are
11
11
  UTC.
12
12
 
13
+ ## [0.8.1] — 2026-08-24
14
+
15
+ Docs — deploy guidance for the native `better-sqlite3` dependency. The SDK loads
16
+ a native SQLite buffer on import (the local telemetry row written before each run
17
+ returns), so the README Install section and `quickstart.md` now tell customers to
18
+ run on a Node runtime (not Edge/Workers), mark `modelrig` external when bundling
19
+ server code (Next.js `serverExternalPackages`, esbuild `--external:better-sqlite3`,
20
+ webpack `externals`), and set `MODELRIG_API_KEY` on every service/worker/cron;
21
+ plus a short monorepo workspace-source caveat. Docs/packaging only — no SDK
22
+ behaviour change.
23
+
13
24
  ## [0.8.0] — 2026-08-24
14
25
 
15
26
  Packaging (GS-3) — the tarball now ships the customer-facing runs docs
package/README.md CHANGED
@@ -22,6 +22,29 @@ npm i modelrig
22
22
  > macOS and Linux on x64/arm64 (nearly everyone); an unusual platform will
23
23
  > compile it at install time and needs a C++ toolchain. Node ≥ 20 is assumed.
24
24
 
25
+ `modelrig` is a normal dependency: your existing `npm install` (or `pnpm` /
26
+ `yarn`) step installs it **prebuilt** — there is no separate "build modelrig"
27
+ step. Three things to get right when you deploy:
28
+
29
+ - **Run it on a Node runtime, not an Edge/Workers runtime.** `better-sqlite3`
30
+ is a native module and loads on import, so an Edge runtime can't run it. A
31
+ standard Node host — server, container, or a Node serverless function — works
32
+ after `npm install`.
33
+ - **If your deploy _bundles_ server code, mark modelrig external** so the native
34
+ binary is loaded at runtime instead of bundled:
35
+ - **Next.js** — `serverExternalPackages: ['modelrig']` in `next.config.js`
36
+ (Next 13/14: `experimental.serverComponentsExternalPackages`)
37
+ - **esbuild** — `--external:better-sqlite3` (and `--external:modelrig`)
38
+ - **webpack** — add `better-sqlite3` to `externals`
39
+ - **Set `MODELRIG_API_KEY` on every service, worker, and cron** that runs a rig
40
+ — each process reports its own telemetry, so the key belongs on all of them,
41
+ not just your web service.
42
+
43
+ (In a monorepo that vendors ModelRig as a workspace package — the SDK source
44
+ rather than the published tarball — build that package before the app that
45
+ imports it, so its `dist/` types resolve. Consuming the published package needs
46
+ none of this.)
47
+
25
48
  ## Quickstart
26
49
 
27
50
  This runs with **only a provider key** — no account, no signup.
package/dist/cli.js CHANGED
File without changes
package/docs/llms.txt CHANGED
@@ -4,7 +4,7 @@
4
4
  > grounding, and caching measured empirically, with a capability-aware
5
5
  > gateway that routes by them. Registry data CC BY 4.0; code Apache-2.0.
6
6
 
7
- _This reference is as of modelrig v0.8.0 (the published npm package). Any surface below that a newer workspace adds is marked "unreleased — workspace only" until its release; nothing is so marked as of this version._
7
+ _This reference is as of modelrig v0.8.1 (the published npm package). Any surface below that a newer workspace adds is marked "unreleased — workspace only" until its release; nothing is so marked as of this version._
8
8
 
9
9
  ## Docs
10
10
 
@@ -87,8 +87,14 @@ reviewed. The full autonomy ladder and same-model guarantees are in the
87
87
  pnpm add modelrig # or npm i modelrig — Node >= 20
88
88
  ```
89
89
 
90
- It builds a native module (`better-sqlite3`) on install, so the machine needs a
91
- toolchain.
90
+ It installs a native module (`better-sqlite3`) — prebuilt for macOS/Linux on
91
+ x64/arm64; an unusual platform compiles it and needs a C++ toolchain.
92
+
93
+ **Deploying:** `modelrig` installs prebuilt (no separate build step), but it
94
+ needs a **Node** runtime (not Edge/Workers), and if your deploy bundles server
95
+ code you must mark it external — Next.js `serverExternalPackages: ['modelrig']`,
96
+ esbuild `--external:better-sqlite3`, webpack `externals`. Set `MODELRIG_API_KEY`
97
+ on every service, worker, and cron that runs a rig.
92
98
 
93
99
  ```ts
94
100
  import { createRig, loadConfigFromEnv } from "modelrig";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelrig",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "description": "ModelRig — capability-aware AI gateway: routes-as-code, provider adapters, typed failures, budget envelopes, and local-first telemetry.",
6
6
  "license": "Apache-2.0",
@@ -1,64 +0,0 @@
1
- /**
2
- * Async batch exporter: SQLite telemetry → ModelRig Supabase project.
3
- * Spec §5 (cloud sink) + §6 criteria 8–9. Fire-and-forget from the run loop:
4
- * the inference path NEVER blocks on, or fails because of, this sink.
5
- * At-least-once delivery with idempotent ids; unexported rows queue locally in
6
- * SQLite (the inferences table itself, behind a rowid watermark) and flush on
7
- * recovery; watermark bookkeeping per source instance.
8
- *
9
- * Transport is raw PostgREST over fetch (dependency-light, service-role key).
10
- */
11
- import type { RigManifest } from "../routes/manifest";
12
- import type { ServeabilityWarning } from "../routes/serveability";
13
- import type { ExportableTelemetryStore } from "./store";
14
- export interface ExportedRouteMirror {
15
- readonly route: string;
16
- readonly version: number;
17
- readonly candidates: ReadonlyArray<{
18
- readonly provider: string;
19
- readonly model: string;
20
- }>;
21
- /** Capture opt-in FLAG (Phase 3 WS4) — status only; capture content never
22
- * has a cloud surface. */
23
- readonly capture?: boolean;
24
- }
25
- export interface ExporterConfig {
26
- readonly supabaseUrl: string;
27
- /** Service-role key — server-side only; never reaches the console. */
28
- readonly serviceRoleKey: string;
29
- readonly sourceId: string;
30
- /**
31
- * Tenant for every row this exporter writes (Phase 6 WS1). Defaults to the
32
- * house org, which is the right answer for a self-hosted control plane —
33
- * there is exactly one org there and everything belongs to it.
34
- *
35
- * This is DIRECT mode only. In api mode the server resolves org and project
36
- * from the `rig_sk_` key and stamps them itself: a client that could choose
37
- * its own tenant would not be a tenant boundary (T3).
38
- */
39
- readonly orgId?: string;
40
- /** Finer scope inside the org. Null is legitimate — not every rig has one. */
41
- readonly projectId?: string | null;
42
- readonly batchSize?: number;
43
- readonly intervalMs?: number;
44
- /** Loaded route bundles, mirrored to the `routes` table for the console. */
45
- readonly routes?: readonly ExportedRouteMirror[];
46
- /** Rig manifest, mirrored to the `rig_manifests` table for the console
47
- * (dimension NAMES only — tag values never leave the telemetry rows). */
48
- readonly manifest?: RigManifest | null;
49
- /** Inert-candidate warnings for THIS deployment (F1) — mirrored onto the
50
- * manifest row so console /setup can say "2 candidates inert here, set
51
- * XAI_API_KEY" instead of the fact living only in a boot log. Names only:
52
- * route, provider, model, env var. */
53
- readonly serveability?: readonly ServeabilityWarning[];
54
- /** Injectable for tests; defaults to global fetch. */
55
- readonly fetchFn?: typeof fetch;
56
- }
57
- export interface Exporter {
58
- start(): void;
59
- /** Flush pending rows now (used by tests and process-exit hooks). */
60
- flush(): Promise<void>;
61
- stop(): Promise<void>;
62
- }
63
- export declare function createSupabaseExporter(store: ExportableTelemetryStore, config: ExporterConfig): Exporter;
64
- //# sourceMappingURL=supabase-exporter.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"supabase-exporter.d.ts","sourceRoot":"","sources":["../../src/telemetry/supabase-exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAIlE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1F;8BAC0B;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACjD;6EACyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACvC;;;0CAGsC;IACtC,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACvD,sDAAsD;IACtD,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,IAAI,IAAI,CAAC;IACd,qEAAqE;IACrE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAiFD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,wBAAwB,EAC/B,MAAM,EAAE,cAAc,GACrB,QAAQ,CA4KV"}
@@ -1,211 +0,0 @@
1
- "use strict";
2
- /**
3
- * Async batch exporter: SQLite telemetry → ModelRig Supabase project.
4
- * Spec §5 (cloud sink) + §6 criteria 8–9. Fire-and-forget from the run loop:
5
- * the inference path NEVER blocks on, or fails because of, this sink.
6
- * At-least-once delivery with idempotent ids; unexported rows queue locally in
7
- * SQLite (the inferences table itself, behind a rowid watermark) and flush on
8
- * recovery; watermark bookkeeping per source instance.
9
- *
10
- * Transport is raw PostgREST over fetch (dependency-light, service-role key).
11
- */
12
- Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.createSupabaseExporter = createSupabaseExporter;
14
- const constants_1 = require("../tenancy/constants");
15
- const DEFAULT_BATCH_SIZE = 200;
16
- const DEFAULT_INTERVAL_MS = 10000;
17
- /** After a sink failure, skip this many intervals, doubling per consecutive
18
- * failure (simple exponential backoff, capped). */
19
- const BACKOFF_MAX_SKIPS = 32;
20
- function toRemote(row, tenancy) {
21
- return {
22
- ...tenancy,
23
- id: row.id,
24
- ts: row.ts,
25
- route: row.route,
26
- route_version: row.routeVersion,
27
- provider: row.provider,
28
- model: row.model,
29
- requested_tier: row.requestedTier,
30
- served_tier: row.servedTier,
31
- tokens_in: row.tokensIn,
32
- tokens_out: row.tokensOut,
33
- tokens_cached: row.tokensCached,
34
- cost_estimate_usd: row.costEstimateUsd,
35
- latency_ms: row.latencyMs,
36
- ttfb_ms: row.ttfbMs,
37
- failure_class: row.failureClass,
38
- attempts: JSON.parse(row.attemptsJson),
39
- tags: JSON.parse(row.tagsJson),
40
- fingerprint_cell: row.fingerprintCell,
41
- served_variant: row.servedVariant,
42
- repaired_by: row.repairedBy,
43
- pricing_missing: row.pricingMissing,
44
- };
45
- }
46
- function feedbackToRemote(row, tenancy) {
47
- return {
48
- org_id: tenancy.org_id,
49
- project_id: tenancy.project_id,
50
- id: row.id,
51
- ts: row.ts,
52
- inference_id: row.inferenceId,
53
- episode_key: row.episodeKey,
54
- verdict: row.verdict,
55
- score: row.score,
56
- note: row.note,
57
- source: row.source,
58
- };
59
- }
60
- function createSupabaseExporter(store, config) {
61
- const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
62
- const intervalMs = config.intervalMs ?? DEFAULT_INTERVAL_MS;
63
- const fetchFn = config.fetchFn ?? fetch;
64
- const base = config.supabaseUrl.replace(/\/$/, "");
65
- const tenancy = {
66
- org_id: config.orgId ?? constants_1.HOUSE_ORG_ID,
67
- project_id: config.projectId ?? null,
68
- source_id: config.sourceId,
69
- };
70
- let timer = null;
71
- let flushing = false;
72
- let skipsRemaining = 0;
73
- let consecutiveFailures = 0;
74
- let routesMirrored = false;
75
- let manifestMirrored = false;
76
- async function post(table, body, upsert) {
77
- const response = await fetchFn(`${base}/rest/v1/${table}`, {
78
- method: "POST",
79
- headers: {
80
- "content-type": "application/json",
81
- apikey: config.serviceRoleKey,
82
- authorization: `Bearer ${config.serviceRoleKey}`,
83
- prefer: upsert
84
- ? "resolution=merge-duplicates,return=minimal"
85
- : "resolution=ignore-duplicates,return=minimal",
86
- },
87
- body: JSON.stringify(body),
88
- });
89
- if (!response.ok) {
90
- throw new Error(`sink POST ${table} → ${response.status} ${await response.text().catch(() => "")}`);
91
- }
92
- }
93
- async function flushOnce() {
94
- // Routes mirror first (idempotent upsert; retried until it succeeds once).
95
- if (!routesMirrored && config.routes !== undefined && config.routes.length > 0) {
96
- await post("routes", config.routes.map((r) => ({
97
- org_id: tenancy.org_id,
98
- route: r.route,
99
- version: r.version,
100
- candidates: r.candidates,
101
- capture: r.capture ?? false,
102
- updated_at: new Date().toISOString(),
103
- })), true);
104
- routesMirrored = true;
105
- }
106
- // Manifest mirror (idempotent upsert, once per process — like routes).
107
- if (!manifestMirrored && config.manifest !== undefined && config.manifest !== null) {
108
- await post("rig_manifests", [
109
- {
110
- org_id: tenancy.org_id,
111
- source_id: config.sourceId,
112
- name: config.manifest.name,
113
- dimensions: config.manifest.dimensions,
114
- contribution: config.manifest.contribution,
115
- // F1: names only — route, provider/model, and the env var to set.
116
- serveability: (config.serveability ?? []).map((w) => ({
117
- route: w.route,
118
- inert: w.inert.map((c) => ({ provider: c.provider, model: c.model })),
119
- serveable_count: w.serveableCount,
120
- fix_hint: w.fixHint,
121
- })),
122
- updated_at: new Date().toISOString(),
123
- },
124
- ], true);
125
- manifestMirrored = true;
126
- }
127
- // Inferences past the watermark, in batches. Watermark advances ONLY
128
- // after a confirmed write (at-least-once; remote id conflicts ignored).
129
- for (;;) {
130
- const watermark = store.getExportWatermark(config.sourceId);
131
- const pending = store.readInferencesAfter(watermark, batchSize);
132
- if (pending.length === 0)
133
- break;
134
- await post("inferences", pending.map(({ row }) => toRemote(row, tenancy)), false);
135
- store.setExportWatermark(config.sourceId, pending[pending.length - 1].rowid);
136
- await post("export_watermarks", [{ org_id: tenancy.org_id, source_id: config.sourceId, last_exported_ts: new Date().toISOString() }], true);
137
- if (pending.length < batchSize)
138
- break;
139
- }
140
- // Feedback mirror (Phase 4 WS2): same at-least-once watermark dance as
141
- // inferences, behind its own export_state key so the two streams never
142
- // block each other.
143
- const feedbackSource = `${config.sourceId}:feedback`;
144
- for (;;) {
145
- const watermark = store.getExportWatermark(feedbackSource);
146
- const pending = store.readFeedbackAfter(watermark, batchSize);
147
- if (pending.length === 0)
148
- break;
149
- await post("feedback", pending.map(({ row }) => feedbackToRemote(row, tenancy)), false);
150
- store.setExportWatermark(feedbackSource, pending[pending.length - 1].rowid);
151
- if (pending.length < batchSize)
152
- break;
153
- }
154
- // Envelopes: small table, full upsert each flush.
155
- const envelopes = store.listEnvelopes();
156
- if (envelopes.length > 0) {
157
- await post("envelopes", envelopes.map((e) => ({
158
- org_id: tenancy.org_id,
159
- envelope_key: e.envelopeKey,
160
- budget_usd: e.budgetUsd,
161
- spent_usd: e.spentUsd,
162
- status: e.status,
163
- exported_at: new Date().toISOString(),
164
- })), true);
165
- }
166
- }
167
- async function guardedFlush() {
168
- if (flushing)
169
- return;
170
- flushing = true;
171
- try {
172
- await flushOnce();
173
- consecutiveFailures = 0;
174
- }
175
- catch (err) {
176
- // Swallow-and-log, ALWAYS — sink failures must never reach the run loop.
177
- consecutiveFailures += 1;
178
- skipsRemaining = Math.min(2 ** consecutiveFailures, BACKOFF_MAX_SKIPS);
179
- console.warn(`[modelrig exporter] sink flush failed (attempt ${consecutiveFailures}, backing off ` +
180
- `${skipsRemaining} intervals): ${err instanceof Error ? err.message : String(err)}`);
181
- }
182
- finally {
183
- flushing = false;
184
- }
185
- }
186
- return {
187
- start() {
188
- if (timer !== null)
189
- return;
190
- timer = setInterval(() => {
191
- if (skipsRemaining > 0) {
192
- skipsRemaining -= 1;
193
- return;
194
- }
195
- void guardedFlush();
196
- }, intervalMs);
197
- // Never keep the process alive for telemetry export.
198
- timer.unref?.();
199
- },
200
- async flush() {
201
- await guardedFlush();
202
- },
203
- async stop() {
204
- if (timer !== null) {
205
- clearInterval(timer);
206
- timer = null;
207
- }
208
- await guardedFlush();
209
- },
210
- };
211
- }