modelrig 0.1.0 → 0.2.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.
Files changed (309) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +41 -11
  3. package/data/PROVENANCE.md +19 -0
  4. package/data/pricing-manual.json +22 -0
  5. package/data/pricing-map.json +46362 -0
  6. package/data/pricing-overlay.json +419 -0
  7. package/dist/adapters/anthropic.d.ts +50 -6
  8. package/dist/adapters/anthropic.d.ts.map +1 -1
  9. package/dist/adapters/anthropic.js +123 -29
  10. package/dist/adapters/build-adapters.d.ts +11 -0
  11. package/dist/adapters/build-adapters.d.ts.map +1 -0
  12. package/dist/adapters/build-adapters.js +45 -0
  13. package/dist/adapters/deepseek.d.ts +6 -0
  14. package/dist/adapters/deepseek.d.ts.map +1 -1
  15. package/dist/adapters/deepseek.js +54 -1
  16. package/dist/adapters/gemini.d.ts.map +1 -1
  17. package/dist/adapters/gemini.js +46 -6
  18. package/dist/adapters/grok.d.ts +9 -0
  19. package/dist/adapters/grok.d.ts.map +1 -1
  20. package/dist/adapters/grok.js +67 -3
  21. package/dist/adapters/openai-compat.d.ts +6 -0
  22. package/dist/adapters/openai-compat.d.ts.map +1 -1
  23. package/dist/adapters/openai-compat.js +58 -1
  24. package/dist/adapters/openai-stream.d.ts +58 -0
  25. package/dist/adapters/openai-stream.d.ts.map +1 -0
  26. package/dist/adapters/openai-stream.js +120 -0
  27. package/dist/adapters/openai.d.ts +6 -0
  28. package/dist/adapters/openai.d.ts.map +1 -1
  29. package/dist/adapters/openai.js +60 -1
  30. package/dist/adapters/sampling.d.ts +28 -0
  31. package/dist/adapters/sampling.d.ts.map +1 -0
  32. package/dist/adapters/sampling.js +34 -0
  33. package/dist/adapters/tool-mapping.d.ts +92 -0
  34. package/dist/adapters/tool-mapping.d.ts.map +1 -0
  35. package/dist/adapters/tool-mapping.js +143 -0
  36. package/dist/artifacts/artifact.d.ts +137 -0
  37. package/dist/artifacts/artifact.d.ts.map +1 -0
  38. package/dist/artifacts/artifact.js +377 -0
  39. package/dist/artifacts/content-uploader.d.ts +87 -0
  40. package/dist/artifacts/content-uploader.d.ts.map +1 -0
  41. package/dist/artifacts/content-uploader.js +124 -0
  42. package/dist/artifacts/index.d.ts +63 -0
  43. package/dist/artifacts/index.d.ts.map +1 -0
  44. package/dist/artifacts/index.js +80 -0
  45. package/dist/artifacts/run-context.d.ts +129 -0
  46. package/dist/artifacts/run-context.d.ts.map +1 -0
  47. package/dist/artifacts/run-context.js +208 -0
  48. package/dist/artifacts/scrub.d.ts +60 -0
  49. package/dist/artifacts/scrub.d.ts.map +1 -0
  50. package/dist/artifacts/scrub.js +139 -0
  51. package/dist/artifacts/step.d.ts +26 -0
  52. package/dist/artifacts/step.d.ts.map +1 -0
  53. package/dist/artifacts/step.js +37 -0
  54. package/dist/artifacts/taint.d.ts +63 -0
  55. package/dist/artifacts/taint.d.ts.map +1 -0
  56. package/dist/artifacts/taint.js +76 -0
  57. package/dist/bakeoff/cache-sim.d.ts +99 -0
  58. package/dist/bakeoff/cache-sim.d.ts.map +1 -0
  59. package/dist/bakeoff/cache-sim.js +126 -0
  60. package/dist/bakeoff/eval-cases.d.ts +96 -0
  61. package/dist/bakeoff/eval-cases.d.ts.map +1 -0
  62. package/dist/bakeoff/eval-cases.js +105 -0
  63. package/dist/bakeoff/report.d.ts +41 -0
  64. package/dist/bakeoff/report.d.ts.map +1 -0
  65. package/dist/bakeoff/report.js +142 -0
  66. package/dist/bakeoff/samples.d.ts +15 -0
  67. package/dist/bakeoff/samples.d.ts.map +1 -1
  68. package/dist/bakeoff/samples.js +20 -0
  69. package/dist/cli.d.ts +1 -3
  70. package/dist/cli.d.ts.map +1 -1
  71. package/dist/cli.js +134 -116
  72. package/dist/config.d.ts +100 -0
  73. package/dist/config.d.ts.map +1 -1
  74. package/dist/config.js +104 -0
  75. package/dist/deprecations.d.ts +32 -0
  76. package/dist/deprecations.d.ts.map +1 -0
  77. package/dist/deprecations.js +59 -0
  78. package/dist/gateway/http-protocol.d.ts +175 -0
  79. package/dist/gateway/http-protocol.d.ts.map +1 -0
  80. package/dist/gateway/http-protocol.js +105 -0
  81. package/dist/index.d.ts +105 -18
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +266 -138
  84. package/dist/init/finder.d.ts +15 -0
  85. package/dist/init/finder.d.ts.map +1 -1
  86. package/dist/init/finder.js +46 -0
  87. package/dist/init/init-cli.d.ts +10 -0
  88. package/dist/init/init-cli.d.ts.map +1 -0
  89. package/dist/init/init-cli.js +86 -0
  90. package/dist/init/observe-cli.d.ts +19 -0
  91. package/dist/init/observe-cli.d.ts.map +1 -0
  92. package/dist/init/observe-cli.js +73 -0
  93. package/dist/init/observe.d.ts +81 -0
  94. package/dist/init/observe.d.ts.map +1 -0
  95. package/dist/init/observe.js +245 -0
  96. package/dist/registry/build.d.ts +94 -1
  97. package/dist/registry/build.d.ts.map +1 -1
  98. package/dist/registry/build.js +125 -4
  99. package/dist/registry/cache-policy.d.ts +94 -0
  100. package/dist/registry/cache-policy.d.ts.map +1 -0
  101. package/dist/registry/cache-policy.js +238 -0
  102. package/dist/registry/call-notes.d.ts.map +1 -1
  103. package/dist/registry/call-notes.js +40 -2
  104. package/dist/registry/effective-rate.d.ts +151 -0
  105. package/dist/registry/effective-rate.d.ts.map +1 -0
  106. package/dist/registry/effective-rate.js +156 -0
  107. package/dist/registry/facts.d.ts.map +1 -1
  108. package/dist/registry/facts.js +16 -0
  109. package/dist/registry/parity.d.ts +26 -0
  110. package/dist/registry/parity.d.ts.map +1 -1
  111. package/dist/registry/parity.js +43 -2
  112. package/dist/registry/pricing-shape.d.ts +113 -0
  113. package/dist/registry/pricing-shape.d.ts.map +1 -0
  114. package/dist/registry/pricing-shape.js +123 -0
  115. package/dist/registry/pricing.d.ts +25 -1
  116. package/dist/registry/pricing.d.ts.map +1 -1
  117. package/dist/registry/pricing.js +64 -6
  118. package/dist/rigindex/bootstrap.d.ts +36 -0
  119. package/dist/rigindex/bootstrap.d.ts.map +1 -0
  120. package/dist/rigindex/bootstrap.js +97 -0
  121. package/dist/rigindex/fit.d.ts +24 -0
  122. package/dist/rigindex/fit.d.ts.map +1 -0
  123. package/dist/rigindex/fit.js +76 -0
  124. package/dist/rigindex/gates.d.ts +52 -0
  125. package/dist/rigindex/gates.d.ts.map +1 -0
  126. package/dist/rigindex/gates.js +109 -0
  127. package/dist/rigindex/graph.d.ts +31 -0
  128. package/dist/rigindex/graph.d.ts.map +1 -0
  129. package/dist/rigindex/graph.js +120 -0
  130. package/dist/rigindex/index.d.ts +23 -0
  131. package/dist/rigindex/index.d.ts.map +1 -0
  132. package/dist/rigindex/index.js +112 -0
  133. package/dist/rigindex/types.d.ts +76 -0
  134. package/dist/rigindex/types.d.ts.map +1 -0
  135. package/dist/rigindex/types.js +8 -0
  136. package/dist/routes/cache-lint.d.ts +41 -0
  137. package/dist/routes/cache-lint.d.ts.map +1 -0
  138. package/dist/routes/cache-lint.js +81 -0
  139. package/dist/routes/loader.d.ts +37 -0
  140. package/dist/routes/loader.d.ts.map +1 -1
  141. package/dist/routes/loader.js +138 -0
  142. package/dist/routes/resolved-bundle.d.ts +79 -0
  143. package/dist/routes/resolved-bundle.d.ts.map +1 -0
  144. package/dist/routes/resolved-bundle.js +148 -0
  145. package/dist/routes/sampling-config.d.ts +32 -0
  146. package/dist/routes/sampling-config.d.ts.map +1 -0
  147. package/dist/routes/sampling-config.js +91 -0
  148. package/dist/run/ajv.d.ts +22 -0
  149. package/dist/run/ajv.d.ts.map +1 -0
  150. package/dist/run/ajv.js +32 -0
  151. package/dist/run/cache-handle.d.ts +58 -0
  152. package/dist/run/cache-handle.d.ts.map +1 -0
  153. package/dist/run/cache-handle.js +54 -0
  154. package/dist/run/cache-stamp.d.ts +65 -0
  155. package/dist/run/cache-stamp.d.ts.map +1 -0
  156. package/dist/run/cache-stamp.js +0 -0
  157. package/dist/run/capture-write.d.ts +33 -0
  158. package/dist/run/capture-write.d.ts.map +1 -0
  159. package/dist/run/capture-write.js +55 -0
  160. package/dist/run/cost.d.ts.map +1 -1
  161. package/dist/run/cost.js +1 -1
  162. package/dist/run/dispatch.d.ts +99 -0
  163. package/dist/run/dispatch.d.ts.map +1 -0
  164. package/dist/run/dispatch.js +191 -0
  165. package/dist/run/raw.d.ts +88 -0
  166. package/dist/run/raw.d.ts.map +1 -0
  167. package/dist/run/raw.js +286 -0
  168. package/dist/run/record-attempt.d.ts +40 -0
  169. package/dist/run/record-attempt.d.ts.map +1 -0
  170. package/dist/run/record-attempt.js +99 -0
  171. package/dist/run/resolve.d.ts +75 -2
  172. package/dist/run/resolve.d.ts.map +1 -1
  173. package/dist/run/resolve.js +126 -4
  174. package/dist/run/run.d.ts +14 -0
  175. package/dist/run/run.d.ts.map +1 -1
  176. package/dist/run/run.js +0 -0
  177. package/dist/run/stream.d.ts +49 -0
  178. package/dist/run/stream.d.ts.map +1 -0
  179. package/dist/run/stream.js +87 -0
  180. package/dist/run/validate.d.ts.map +1 -1
  181. package/dist/run/validate.js +6 -7
  182. package/dist/savings/cache-ledger.d.ts +108 -0
  183. package/dist/savings/cache-ledger.d.ts.map +1 -0
  184. package/dist/savings/cache-ledger.js +174 -0
  185. package/dist/savings/cache-reconcile.d.ts +64 -0
  186. package/dist/savings/cache-reconcile.d.ts.map +1 -0
  187. package/dist/savings/cache-reconcile.js +59 -0
  188. package/dist/savings/cache-stats.d.ts +63 -0
  189. package/dist/savings/cache-stats.d.ts.map +1 -0
  190. package/dist/savings/cache-stats.js +108 -0
  191. package/dist/savings/emit.d.ts +13 -0
  192. package/dist/savings/emit.d.ts.map +1 -1
  193. package/dist/savings/emit.js +25 -0
  194. package/dist/savings/swap-execute.d.ts +14 -0
  195. package/dist/savings/swap-execute.d.ts.map +1 -1
  196. package/dist/savings/swap-execute.js +44 -1
  197. package/dist/savings/swaps-client.d.ts +14 -0
  198. package/dist/savings/swaps-client.d.ts.map +1 -1
  199. package/dist/savings/swaps-client.js +16 -2
  200. package/dist/settings/api-fetcher.d.ts +39 -0
  201. package/dist/settings/api-fetcher.d.ts.map +1 -0
  202. package/dist/settings/api-fetcher.js +92 -0
  203. package/dist/settings/index.d.ts +10 -0
  204. package/dist/settings/index.d.ts.map +1 -0
  205. package/dist/settings/index.js +15 -0
  206. package/dist/settings/task-settings.d.ts +91 -0
  207. package/dist/settings/task-settings.d.ts.map +1 -0
  208. package/dist/settings/task-settings.js +116 -0
  209. package/dist/status.d.ts +77 -0
  210. package/dist/status.d.ts.map +1 -0
  211. package/dist/status.js +113 -0
  212. package/dist/telemetry/artifact-store.d.ts +48 -0
  213. package/dist/telemetry/artifact-store.d.ts.map +1 -0
  214. package/dist/telemetry/artifact-store.js +189 -0
  215. package/dist/telemetry/cache-miss-streak.d.ts +79 -0
  216. package/dist/telemetry/cache-miss-streak.d.ts.map +1 -0
  217. package/dist/telemetry/cache-miss-streak.js +99 -0
  218. package/dist/telemetry/capture.d.ts +3 -0
  219. package/dist/telemetry/capture.d.ts.map +1 -1
  220. package/dist/telemetry/capture.js +15 -8
  221. package/dist/telemetry/exporter-setup.d.ts +28 -0
  222. package/dist/telemetry/exporter-setup.d.ts.map +1 -0
  223. package/dist/telemetry/exporter-setup.js +34 -0
  224. package/dist/telemetry/exporter.d.ts +75 -0
  225. package/dist/telemetry/exporter.d.ts.map +1 -1
  226. package/dist/telemetry/exporter.js +324 -47
  227. package/dist/telemetry/health.d.ts +76 -0
  228. package/dist/telemetry/health.d.ts.map +1 -0
  229. package/dist/telemetry/health.js +92 -0
  230. package/dist/telemetry/ingest-protocol.d.ts +152 -3
  231. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  232. package/dist/telemetry/ingest-protocol.js +204 -4
  233. package/dist/telemetry/load-error-report.d.ts +24 -0
  234. package/dist/telemetry/load-error-report.d.ts.map +1 -0
  235. package/dist/telemetry/load-error-report.js +50 -0
  236. package/dist/telemetry/otel-map.d.ts +121 -0
  237. package/dist/telemetry/otel-map.d.ts.map +1 -0
  238. package/dist/telemetry/otel-map.js +224 -0
  239. package/dist/telemetry/otel-normalize.d.ts +104 -0
  240. package/dist/telemetry/otel-normalize.d.ts.map +1 -0
  241. package/dist/telemetry/otel-normalize.js +530 -0
  242. package/dist/telemetry/otlp-emitter.d.ts +52 -0
  243. package/dist/telemetry/otlp-emitter.d.ts.map +1 -0
  244. package/dist/telemetry/otlp-emitter.js +144 -0
  245. package/dist/telemetry/otlp-proto.d.ts +29 -0
  246. package/dist/telemetry/otlp-proto.d.ts.map +1 -0
  247. package/dist/telemetry/otlp-proto.js +266 -0
  248. package/dist/telemetry/sinks.d.ts +37 -1
  249. package/dist/telemetry/sinks.d.ts.map +1 -1
  250. package/dist/telemetry/sinks.js +121 -30
  251. package/dist/telemetry/store.d.ts +28 -1
  252. package/dist/telemetry/store.d.ts.map +1 -1
  253. package/dist/telemetry/store.js +218 -3
  254. package/dist/tenancy/api-key-format.d.ts +15 -5
  255. package/dist/tenancy/api-key-format.d.ts.map +1 -1
  256. package/dist/tenancy/api-key-format.js +24 -5
  257. package/dist/types/config-teaching.d.ts +62 -0
  258. package/dist/types/config-teaching.d.ts.map +1 -0
  259. package/dist/types/config-teaching.js +183 -0
  260. package/dist/types/provider.d.ts +122 -1
  261. package/dist/types/provider.d.ts.map +1 -1
  262. package/dist/types/provider.js +21 -1
  263. package/dist/types/route.d.ts +79 -1
  264. package/dist/types/route.d.ts.map +1 -1
  265. package/dist/types/telemetry.d.ts +117 -0
  266. package/dist/types/telemetry.d.ts.map +1 -1
  267. package/dist/validate-cli.d.ts +12 -0
  268. package/dist/validate-cli.d.ts.map +1 -0
  269. package/dist/validate-cli.js +57 -0
  270. package/dist/verify/capture.d.ts +66 -0
  271. package/dist/verify/capture.d.ts.map +1 -0
  272. package/dist/verify/capture.js +180 -0
  273. package/dist/verify/cli-run.d.ts +23 -0
  274. package/dist/verify/cli-run.d.ts.map +1 -0
  275. package/dist/verify/cli-run.js +57 -0
  276. package/dist/verify/diff.d.ts +48 -0
  277. package/dist/verify/diff.d.ts.map +1 -0
  278. package/dist/verify/diff.js +157 -0
  279. package/dist/verify/index.d.ts +17 -0
  280. package/dist/verify/index.d.ts.map +1 -0
  281. package/dist/verify/index.js +24 -0
  282. package/dist/verify/report.d.ts +14 -0
  283. package/dist/verify/report.d.ts.map +1 -0
  284. package/dist/verify/report.js +88 -0
  285. package/dist/verify/types.d.ts +101 -0
  286. package/dist/verify/types.d.ts.map +1 -0
  287. package/dist/verify/types.js +17 -0
  288. package/dist/verify/verify.d.ts +29 -0
  289. package/dist/verify/verify.d.ts.map +1 -0
  290. package/dist/verify/verify.js +148 -0
  291. package/dist/watch/evaluate.d.ts +162 -3
  292. package/dist/watch/evaluate.d.ts.map +1 -1
  293. package/dist/watch/evaluate.js +61 -0
  294. package/dist/watch/monitor.d.ts +116 -0
  295. package/dist/watch/monitor.d.ts.map +1 -0
  296. package/dist/watch/monitor.js +199 -0
  297. package/dist/watch/proposals.d.ts +20 -4
  298. package/dist/watch/proposals.d.ts.map +1 -1
  299. package/dist/watch/proposals.js +31 -3
  300. package/dist/watch/run.d.ts +33 -0
  301. package/dist/watch/run.d.ts.map +1 -1
  302. package/dist/watch/run.js +39 -0
  303. package/package.json +7 -1
  304. package/registry/LICENSE +20 -0
  305. package/registry/leaderboard.json +3639 -0
  306. package/registry/registry.json +7263 -0
  307. package/dist/telemetry/supabase-exporter.d.ts +0 -64
  308. package/dist/telemetry/supabase-exporter.d.ts.map +0 -1
  309. package/dist/telemetry/supabase-exporter.js +0 -211
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ /**
3
+ * OTLP span emitter — E1 (spec §2.3, §4). A SIBLING of the control-plane
4
+ * exporter, not a sink inside it: it reads the same `inferences` table behind
5
+ * its OWN watermark (`${sourceId}:otlp`), so the OTLP stream and the
6
+ * control-plane export never block or drain each other — exactly the pattern
7
+ * the feedback stream already uses (`:feedback`).
8
+ *
9
+ * ── DOCTRINE CLONED FROM exporter.ts (verbatim, on purpose) ───────────────
10
+ * Fire-and-forget from the run loop; at-least-once with idempotent span ids
11
+ * (otel-map derives them deterministically, so a re-emit after a crash is a
12
+ * no-op at the sink); the watermark advances ONLY after a confirmed 2xx;
13
+ * sink errors are swallowed-and-logged with exponential skip backoff; the
14
+ * interval timer is `unref()`ed so telemetry never keeps a process alive.
15
+ *
16
+ * ── OFF BY DEFAULT ────────────────────────────────────────────────────────
17
+ * This module is only constructed when `MODELRIG_OTLP_ENDPOINT` is set
18
+ * (config.ts). No endpoint ⇒ no emitter ⇒ zero behaviour change (spec §6a).
19
+ *
20
+ * ── C2 (never leak credentials) ───────────────────────────────────────────
21
+ * `cfg.headers` may carry sink auth (e.g. Langfuse Basic auth) and `endpoint`
22
+ * itself may embed a token. NEITHER is ever logged or put in an error. Failure
23
+ * text is run through the shared secret scrubber before it reaches a warning.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.createOtlpEmitter = createOtlpEmitter;
27
+ exports.startOtlpEmitterIfConfigured = startOtlpEmitterIfConfigured;
28
+ const node_zlib_1 = require("node:zlib");
29
+ const otel_map_1 = require("./otel-map");
30
+ const scrub_1 = require("../tenancy/scrub");
31
+ const DEFAULT_BATCH_SIZE = 200;
32
+ const DEFAULT_INTERVAL_MS = 10000;
33
+ const BACKOFF_MAX_SKIPS = 32;
34
+ /** Below this, gzip costs more than it saves (mirrors the api sink). */
35
+ const GZIP_MIN_BYTES = 1024;
36
+ function createOtlpEmitter(store, config) {
37
+ const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
38
+ const intervalMs = config.intervalMs ?? DEFAULT_INTERVAL_MS;
39
+ const fetchFn = config.fetchFn ?? fetch;
40
+ const watermarkKey = `${config.sourceId}:otlp`;
41
+ const resource = { serviceName: config.sourceId };
42
+ let timer = null;
43
+ let flushing = false;
44
+ let skipsRemaining = 0;
45
+ let consecutiveFailures = 0;
46
+ async function postSpans(rows) {
47
+ const spans = rows.map(({ row }) => (0, otel_map_1.inferenceToSpan)(row));
48
+ const json = JSON.stringify((0, otel_map_1.buildExportTraceRequest)(spans, resource));
49
+ const compress = Buffer.byteLength(json) >= GZIP_MIN_BYTES;
50
+ const response = await fetchFn(config.endpoint, {
51
+ method: "POST",
52
+ headers: {
53
+ "content-type": "application/json",
54
+ ...config.headers,
55
+ ...(compress ? { "content-encoding": "gzip" } : {}),
56
+ },
57
+ body: compress ? (0, node_zlib_1.gzipSync)(json) : json,
58
+ });
59
+ if (!response.ok) {
60
+ // C2: neither the endpoint nor the headers ever appear here — status plus
61
+ // a SCRUBBED body only. A collector's error body is the likeliest place a
62
+ // reflected credential could reach someone else's log aggregator.
63
+ const body = (0, scrub_1.scrubSecrets)(await response.text().catch(() => ""));
64
+ throw new Error(`otlp export → ${response.status} ${body}`.trim());
65
+ }
66
+ }
67
+ async function flushOnce() {
68
+ // Inferences past the OTLP watermark, in batches. The watermark advances
69
+ // ONLY after a confirmed send (at-least-once; deterministic span ids make
70
+ // the duplicate a no-op at the sink).
71
+ for (;;) {
72
+ const watermark = store.getExportWatermark(watermarkKey);
73
+ const pending = store.readInferencesAfter(watermark, batchSize);
74
+ if (pending.length === 0)
75
+ break;
76
+ await postSpans(pending);
77
+ store.setExportWatermark(watermarkKey, pending[pending.length - 1].rowid);
78
+ if (pending.length < batchSize)
79
+ break;
80
+ }
81
+ }
82
+ async function guardedFlush() {
83
+ if (flushing)
84
+ return;
85
+ flushing = true;
86
+ try {
87
+ await flushOnce();
88
+ consecutiveFailures = 0;
89
+ }
90
+ catch (err) {
91
+ // Swallow-and-log, ALWAYS — an OTLP sink failure must never reach the run
92
+ // loop, and the scrubber guards the one place a secret could ride out.
93
+ consecutiveFailures += 1;
94
+ skipsRemaining = Math.min(2 ** consecutiveFailures, BACKOFF_MAX_SKIPS);
95
+ console.warn(`[modelrig otlp] export failed (attempt ${consecutiveFailures}, ` +
96
+ `backing off ${skipsRemaining} intervals): ` +
97
+ (0, scrub_1.scrubSecrets)(err instanceof Error ? err.message : String(err)));
98
+ }
99
+ finally {
100
+ flushing = false;
101
+ }
102
+ }
103
+ return {
104
+ start() {
105
+ if (timer !== null)
106
+ return;
107
+ timer = setInterval(() => {
108
+ if (skipsRemaining > 0) {
109
+ skipsRemaining -= 1;
110
+ return;
111
+ }
112
+ void guardedFlush();
113
+ }, intervalMs);
114
+ timer.unref?.();
115
+ },
116
+ async flushNow() {
117
+ await guardedFlush();
118
+ },
119
+ async stop() {
120
+ if (timer !== null) {
121
+ clearInterval(timer);
122
+ timer = null;
123
+ }
124
+ await guardedFlush();
125
+ },
126
+ };
127
+ }
128
+ /**
129
+ * Construct + start the OTLP emitter iff an endpoint is configured, else null
130
+ * (spec §6a). Lives here rather than index.ts so createRig's wiring stays one
131
+ * line and this file keeps the LOC cap on index.ts — the same reason
132
+ * `buildTelemetrySink` moved to sinks.ts.
133
+ */
134
+ function startOtlpEmitterIfConfigured(store, otlp, sourceId) {
135
+ if (otlp == null)
136
+ return null;
137
+ const emitter = createOtlpEmitter(store, {
138
+ endpoint: otlp.endpoint,
139
+ headers: otlp.headers,
140
+ sourceId,
141
+ });
142
+ emitter.start();
143
+ return emitter;
144
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Minimal OTLP/protobuf trace decoder — Expansion E3-Harvest (spec §3.2).
3
+ * PURE. Decodes an `ExportTraceServiceRequest` / `TracesData` protobuf body
4
+ * into the SAME loose OTLP-JSON-ish structure `otel-normalize.ts` consumes, so
5
+ * both transports (JSON + protobuf) converge on one normalizer.
6
+ *
7
+ * ── WHY HAND-ROLLED (§5.5 dependency firewall) ────────────────────────────
8
+ * Adding `@opentelemetry/*` or a generated proto runtime would be a new npm
9
+ * dependency, which needs an explicit addendum + G2 pack-safety review. We are
10
+ * decoding a fixed, small message set (trace/span/keyvalue), not implementing
11
+ * protobuf generally, so a focused wire reader is the right, dependency-free
12
+ * tool (implementation-plan "risks"). It reads only structure/identity/usage
13
+ * fields; content-adjacent fields (kvlist/bytes AnyValue) are skipped.
14
+ *
15
+ * Wire format: tag = (field << 3) | wireType; 0=varint, 1=64-bit, 2=length-
16
+ * delimited, 5=32-bit. int64 attribute + fixed64 timestamp values are emitted
17
+ * as DECIMAL STRINGS (what the normalizer's coercion expects), trace/span ids
18
+ * as lowercase hex.
19
+ */
20
+ /**
21
+ * Decode an OTLP `ExportTraceServiceRequest` / `TracesData` protobuf body into
22
+ * the `{ resourceSpans: [...] }` structure the normalizer reads. Throws on a
23
+ * truncated/garbled body (the caller turns that into a 400) — never returns
24
+ * partial garbage.
25
+ */
26
+ export declare function decodeOtlpProtobufTraces(body: Buffer): {
27
+ resourceSpans: unknown[];
28
+ };
29
+ //# sourceMappingURL=otlp-proto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otlp-proto.d.ts","sourceRoot":"","sources":["../../src/telemetry/otlp-proto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AA4OH;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,aAAa,EAAE,OAAO,EAAE,CAAA;CAAE,CAUnF"}
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ /**
3
+ * Minimal OTLP/protobuf trace decoder — Expansion E3-Harvest (spec §3.2).
4
+ * PURE. Decodes an `ExportTraceServiceRequest` / `TracesData` protobuf body
5
+ * into the SAME loose OTLP-JSON-ish structure `otel-normalize.ts` consumes, so
6
+ * both transports (JSON + protobuf) converge on one normalizer.
7
+ *
8
+ * ── WHY HAND-ROLLED (§5.5 dependency firewall) ────────────────────────────
9
+ * Adding `@opentelemetry/*` or a generated proto runtime would be a new npm
10
+ * dependency, which needs an explicit addendum + G2 pack-safety review. We are
11
+ * decoding a fixed, small message set (trace/span/keyvalue), not implementing
12
+ * protobuf generally, so a focused wire reader is the right, dependency-free
13
+ * tool (implementation-plan "risks"). It reads only structure/identity/usage
14
+ * fields; content-adjacent fields (kvlist/bytes AnyValue) are skipped.
15
+ *
16
+ * Wire format: tag = (field << 3) | wireType; 0=varint, 1=64-bit, 2=length-
17
+ * delimited, 5=32-bit. int64 attribute + fixed64 timestamp values are emitted
18
+ * as DECIMAL STRINGS (what the normalizer's coercion expects), trace/span ids
19
+ * as lowercase hex.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.decodeOtlpProtobufTraces = decodeOtlpProtobufTraces;
23
+ const WIRE_VARINT = 0;
24
+ const WIRE_64BIT = 1;
25
+ const WIRE_LEN = 2;
26
+ const WIRE_32BIT = 5;
27
+ class Reader {
28
+ constructor(buf) {
29
+ this.buf = buf;
30
+ this.pos = 0;
31
+ }
32
+ get done() {
33
+ return this.pos >= this.buf.length;
34
+ }
35
+ /** LEB128 varint → bigint (values can exceed 2^53). */
36
+ varint() {
37
+ let result = 0n;
38
+ let shift = 0n;
39
+ for (;;) {
40
+ if (this.pos >= this.buf.length)
41
+ throw new Error("varint overrun");
42
+ const byte = this.buf[this.pos++];
43
+ result |= BigInt(byte & 0x7f) << shift;
44
+ if ((byte & 0x80) === 0)
45
+ break;
46
+ shift += 7n;
47
+ if (shift > 70n)
48
+ throw new Error("varint too long");
49
+ }
50
+ return result;
51
+ }
52
+ /** A field tag → {field, wireType}. */
53
+ tag() {
54
+ const t = Number(this.varint());
55
+ return { field: t >>> 3, wireType: t & 0x7 };
56
+ }
57
+ bytes() {
58
+ const len = Number(this.varint());
59
+ if (this.pos + len > this.buf.length)
60
+ throw new Error("length-delimited overrun");
61
+ const slice = this.buf.subarray(this.pos, this.pos + len);
62
+ this.pos += len;
63
+ return slice;
64
+ }
65
+ fixed64() {
66
+ if (this.pos + 8 > this.buf.length)
67
+ throw new Error("fixed64 overrun");
68
+ const slice = this.buf.subarray(this.pos, this.pos + 8);
69
+ this.pos += 8;
70
+ return slice;
71
+ }
72
+ /** Skip a field of the given wire type we do not consume. */
73
+ skip(wireType) {
74
+ if (wireType === WIRE_VARINT)
75
+ this.varint();
76
+ else if (wireType === WIRE_64BIT)
77
+ this.pos += 8;
78
+ else if (wireType === WIRE_LEN)
79
+ this.bytes();
80
+ else if (wireType === WIRE_32BIT)
81
+ this.pos += 4;
82
+ else
83
+ throw new Error(`unknown wire type ${wireType}`);
84
+ }
85
+ }
86
+ function fixed64ToDecimalString(b) {
87
+ // Little-endian uint64 → decimal string.
88
+ let v = 0n;
89
+ for (let i = 7; i >= 0; i--)
90
+ v = (v << 8n) | BigInt(b[i]);
91
+ return v.toString();
92
+ }
93
+ function fixed64ToDouble(b) {
94
+ return b.readDoubleLE(0);
95
+ }
96
+ function decodeAnyValue(buf) {
97
+ const r = new Reader(buf);
98
+ const out = {};
99
+ while (!r.done) {
100
+ const { field, wireType } = r.tag();
101
+ // Guard the wire type on EVERY field: a field whose wire type does not match
102
+ // its schema (malformed input, or a wrong-message-type body such as a logs
103
+ // record decoded as a span) is skipped, never mis-read. Without this a
104
+ // fixed64 log timestamp at field 1 would be read as a length-delimited
105
+ // string and corrupt the parse.
106
+ if (field === 1 && wireType === WIRE_LEN)
107
+ out.stringValue = r.bytes().toString("utf8");
108
+ else if (field === 2 && wireType === WIRE_VARINT)
109
+ out.boolValue = r.varint() !== 0n;
110
+ else if (field === 3 && wireType === WIRE_VARINT)
111
+ out.intValue = r.varint().toString();
112
+ else if (field === 4 && wireType === WIRE_64BIT)
113
+ out.doubleValue = fixed64ToDouble(r.fixed64());
114
+ else if (field === 5 && wireType === WIRE_LEN)
115
+ out.arrayValue = { values: decodeArrayValue(r.bytes()) };
116
+ // 6 kvlist_value / 7 bytes_value are content-adjacent — skipped in v0.
117
+ else
118
+ r.skip(wireType);
119
+ }
120
+ return out;
121
+ }
122
+ function decodeArrayValue(buf) {
123
+ const r = new Reader(buf);
124
+ const values = [];
125
+ while (!r.done) {
126
+ const { field, wireType } = r.tag();
127
+ if (field === 1 && wireType === WIRE_LEN)
128
+ values.push(decodeAnyValue(r.bytes()));
129
+ else
130
+ r.skip(wireType);
131
+ }
132
+ return values;
133
+ }
134
+ function decodeKeyValue(buf) {
135
+ const r = new Reader(buf);
136
+ const kv = {};
137
+ while (!r.done) {
138
+ const { field, wireType } = r.tag();
139
+ if (field === 1 && wireType === WIRE_LEN)
140
+ kv.key = r.bytes().toString("utf8");
141
+ else if (field === 2 && wireType === WIRE_LEN)
142
+ kv.value = decodeAnyValue(r.bytes());
143
+ else
144
+ r.skip(wireType);
145
+ }
146
+ return kv;
147
+ }
148
+ function decodeSpan(buf) {
149
+ const r = new Reader(buf);
150
+ const span = { attributes: [] };
151
+ while (!r.done) {
152
+ const { field, wireType } = r.tag();
153
+ // Wire-type-guarded (see decodeAnyValue): a LogRecord decoded here as a Span
154
+ // has a fixed64 at field 1 where a Span expects bytes; guarding makes that a
155
+ // skip (→ an id-less span the normalizer drops / logs-detection catches),
156
+ // never a corrupt read.
157
+ if (field === 1 && wireType === WIRE_LEN)
158
+ span.traceId = r.bytes().toString("hex");
159
+ else if (field === 2 && wireType === WIRE_LEN)
160
+ span.spanId = r.bytes().toString("hex");
161
+ else if (field === 4 && wireType === WIRE_LEN)
162
+ span.parentSpanId = r.bytes().toString("hex");
163
+ else if (field === 5 && wireType === WIRE_LEN)
164
+ span.name = r.bytes().toString("utf8");
165
+ else if (field === 6 && wireType === WIRE_VARINT)
166
+ span.kind = Number(r.varint());
167
+ else if (field === 7 && wireType === WIRE_64BIT)
168
+ span.startTimeUnixNano = fixed64ToDecimalString(r.fixed64());
169
+ else if (field === 8 && wireType === WIRE_64BIT)
170
+ span.endTimeUnixNano = fixed64ToDecimalString(r.fixed64());
171
+ else if (field === 9 && wireType === WIRE_LEN)
172
+ span.attributes.push(decodeKeyValue(r.bytes()));
173
+ else if (field === 15 && wireType === WIRE_LEN)
174
+ span.status = decodeStatus(r.bytes());
175
+ else
176
+ r.skip(wireType);
177
+ }
178
+ return span;
179
+ }
180
+ function decodeStatus(buf) {
181
+ const r = new Reader(buf);
182
+ const status = {};
183
+ while (!r.done) {
184
+ const { field, wireType } = r.tag();
185
+ if (field === 2 && wireType === WIRE_LEN)
186
+ status.message = r.bytes().toString("utf8");
187
+ else if (field === 3 && wireType === WIRE_VARINT)
188
+ status.code = Number(r.varint());
189
+ else
190
+ r.skip(wireType);
191
+ }
192
+ return status;
193
+ }
194
+ function decodeScope(buf) {
195
+ const r = new Reader(buf);
196
+ const scope = {};
197
+ while (!r.done) {
198
+ const { field, wireType } = r.tag();
199
+ if (field === 1 && wireType === WIRE_LEN)
200
+ scope.name = r.bytes().toString("utf8");
201
+ else if (field === 2 && wireType === WIRE_LEN)
202
+ scope.version = r.bytes().toString("utf8");
203
+ else
204
+ r.skip(wireType);
205
+ }
206
+ return scope;
207
+ }
208
+ function decodeScopeSpans(buf) {
209
+ const r = new Reader(buf);
210
+ const out = { spans: [] };
211
+ while (!r.done) {
212
+ const { field, wireType } = r.tag();
213
+ if (field === 1 && wireType === WIRE_LEN)
214
+ out.scope = decodeScope(r.bytes());
215
+ else if (field === 2 && wireType === WIRE_LEN)
216
+ out.spans.push(decodeSpan(r.bytes()));
217
+ else
218
+ r.skip(wireType);
219
+ }
220
+ return out;
221
+ }
222
+ function decodeResource(buf) {
223
+ const r = new Reader(buf);
224
+ const attributes = [];
225
+ while (!r.done) {
226
+ const { field, wireType } = r.tag();
227
+ if (field === 1 && wireType === WIRE_LEN)
228
+ attributes.push(decodeKeyValue(r.bytes()));
229
+ else
230
+ r.skip(wireType);
231
+ }
232
+ return { attributes };
233
+ }
234
+ function decodeResourceSpans(buf) {
235
+ const r = new Reader(buf);
236
+ const out = { scopeSpans: [] };
237
+ while (!r.done) {
238
+ const { field, wireType } = r.tag();
239
+ if (field === 1 && wireType === WIRE_LEN)
240
+ out.resource = decodeResource(r.bytes());
241
+ else if (field === 2 && wireType === WIRE_LEN)
242
+ out.scopeSpans.push(decodeScopeSpans(r.bytes()));
243
+ else
244
+ r.skip(wireType);
245
+ }
246
+ return out;
247
+ }
248
+ /**
249
+ * Decode an OTLP `ExportTraceServiceRequest` / `TracesData` protobuf body into
250
+ * the `{ resourceSpans: [...] }` structure the normalizer reads. Throws on a
251
+ * truncated/garbled body (the caller turns that into a 400) — never returns
252
+ * partial garbage.
253
+ */
254
+ function decodeOtlpProtobufTraces(body) {
255
+ const r = new Reader(body);
256
+ const resourceSpans = [];
257
+ while (!r.done) {
258
+ const { field, wireType } = r.tag();
259
+ // TracesData.resource_spans = 1; ExportTraceServiceRequest.resource_spans = 1.
260
+ if (field === 1 && wireType === WIRE_LEN)
261
+ resourceSpans.push(decodeResourceSpans(r.bytes()));
262
+ else
263
+ r.skip(wireType);
264
+ }
265
+ return { resourceSpans };
266
+ }
@@ -20,14 +20,42 @@
20
20
  * cost with no payer.
21
21
  */
22
22
  import type { IngestBatch, IngestOutcome, Tenancy } from "./ingest-protocol";
23
+ import type { ApiExportConfig, DirectExportConfig } from "../config";
23
24
  export interface TelemetrySink {
24
25
  /** Named so a boot banner and `/status` can say which lane is live without
25
26
  * inspecting config — the "which mode am I in?" question got asked enough
26
27
  * during WS1 to be worth answering in the object. */
27
28
  readonly mode: "direct" | "api";
28
- /** Deliver one batch. Throws on failure; the exporter owns retry/backoff. */
29
+ /** Deliver one batch. Throws `SinkSendError` on failure; the exporter owns
30
+ * retry/backoff and reads `retriable` to tell a transient blip apart from a
31
+ * deterministic rejection it must poison-skip (E2 hardening Phase 2). */
29
32
  send(batch: IngestBatch): Promise<void>;
30
33
  }
34
+ /**
35
+ * A sink send failed — the typed error both transports throw (E2 hardening
36
+ * Phase 2, spec §2.3.1). It carries the HTTP status the sink observed (absent
37
+ * for a network-level throw) and a `retriable` verdict the exporter branches on:
38
+ *
39
+ * retriable network throw (no status) · any 5xx · a 429 (overload)
40
+ * → the same batch may land next time; retry forever, never drop.
41
+ * NOT every other 4xx, including the server's deterministic 422
42
+ * → the same bytes will be rejected identically forever; the
43
+ * exporter poison-skips it after a bounded self-heal window.
44
+ *
45
+ * The message is leak-safe BY CONSTRUCTION — status + table only, NEVER the
46
+ * PostgREST/response body. An exporter's warnings are the likeliest place for a
47
+ * secret or a foreign row to reach someone else's log aggregator (C2's leak
48
+ * gate), so the body a failed write returns is logged nowhere.
49
+ */
50
+ export declare class SinkSendError extends Error {
51
+ /** The HTTP status observed, or undefined for a network-level throw. */
52
+ readonly status?: number;
53
+ /** True ⇔ a transient failure the exporter should retry forever (never a
54
+ * poison-skip candidate). Derived once, from the status, so the classification
55
+ * lives in exactly one place. */
56
+ readonly retriable: boolean;
57
+ constructor(message: string, status: number | undefined);
58
+ }
31
59
  export interface DirectSinkConfig {
32
60
  readonly supabaseUrl: string;
33
61
  /** Service-role key — server-side only; never reaches a browser. */
@@ -51,4 +79,12 @@ export interface ApiSinkConfig {
51
79
  readonly onOutcome?: (outcome: IngestOutcome) => void;
52
80
  }
53
81
  export declare function createApiSink(config: ApiSinkConfig): TelemetrySink;
82
+ /**
83
+ * Build the telemetry transport for the configured export mode. One place, so
84
+ * the run-loop exporter and the load-error reporter build it the same way —
85
+ * telemetry-ingestion-boundary.md §4's "one loop, two transports". (Lived in
86
+ * index.ts until Phase D; moved here — its natural home — to keep that file
87
+ * under the LOC cap.)
88
+ */
89
+ export declare function buildTelemetrySink(exportConfig: ApiExportConfig | DirectExportConfig, sourceId: string): TelemetrySink;
54
90
  //# sourceMappingURL=sinks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sinks.d.ts","sourceRoot":"","sources":["../../src/telemetry/sinks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE7E,MAAM,WAAW,aAAa;IAC5B;;yDAEqD;IACrD,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAC;IAChC,6EAA6E;IAC7E,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAMD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,aAAa,CAiCxE;AAMD,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;4EACwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IAChC;;;wDAGoD;IACpD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;CACvD;AAKD,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAgClE"}
1
+ {"version":3,"file":"sinks.d.ts","sourceRoot":"","sources":["../../src/telemetry/sinks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAGrE,MAAM,WAAW,aAAa;IAC5B;;yDAEqD;IACrD,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAC;IAChC;;6EAEyE;IACzE,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;qCAEiC;IACjC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;gBAEhB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS;CAMxD;AAMD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,aAAa,CA0CxE;AAMD,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;4EACwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IAChC;;;wDAGoD;IACpD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;CACvD;AAKD,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CA4ClE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,eAAe,GAAG,kBAAkB,EAClD,QAAQ,EAAE,MAAM,GACf,aAAa,CA0Bf"}