@zanii/blackbox 0.21.2 → 0.23.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 (95) hide show
  1. package/README.md +1 -0
  2. package/dist/actions/index.d.ts +29 -0
  3. package/dist/actions/index.js +114 -0
  4. package/dist/admission/index.d.ts +30 -0
  5. package/dist/admission/index.js +106 -0
  6. package/dist/analysis/accepted.d.ts +26 -0
  7. package/dist/analysis/accepted.js +61 -0
  8. package/dist/analysis/detectors.js +12 -9
  9. package/dist/analysis/faults.js +71 -0
  10. package/dist/analysis/grounding.js +5 -9
  11. package/dist/analysis/guard.d.ts +72 -0
  12. package/dist/analysis/guard.js +240 -0
  13. package/dist/analysis/hallucination.js +2 -2
  14. package/dist/analysis/index.d.ts +21 -1
  15. package/dist/analysis/index.js +101 -19
  16. package/dist/analysis/injection.d.ts +2 -1
  17. package/dist/analysis/injection.js +16 -16
  18. package/dist/analysis/landing.js +60 -14
  19. package/dist/analysis/plan-checks.d.ts +55 -0
  20. package/dist/analysis/plan-checks.js +748 -0
  21. package/dist/analysis/taxonomy.js +1 -0
  22. package/dist/analysis/waste.js +2 -3
  23. package/dist/approvals/index.d.ts +12 -3
  24. package/dist/approvals/index.js +36 -10
  25. package/dist/authority/index.js +3 -3
  26. package/dist/aviation/index.d.ts +422 -0
  27. package/dist/aviation/index.js +1014 -0
  28. package/dist/aviation/qrh.d.ts +4 -0
  29. package/dist/aviation/qrh.js +171 -0
  30. package/dist/budgets/index.d.ts +63 -0
  31. package/dist/budgets/index.js +161 -0
  32. package/dist/cli.js +199 -24
  33. package/dist/compliance/index.d.ts +2 -0
  34. package/dist/compliance/index.js +26 -2
  35. package/dist/cost/batch.d.ts +26 -0
  36. package/dist/cost/batch.js +173 -0
  37. package/dist/cost/index.d.ts +21 -0
  38. package/dist/cost/index.js +115 -30
  39. package/dist/cv/index.js +1 -1
  40. package/dist/duty/index.js +2 -1
  41. package/dist/evals/index.d.ts +233 -0
  42. package/dist/evals/index.js +488 -0
  43. package/dist/evidence/index.d.ts +9 -0
  44. package/dist/evidence/index.js +50 -0
  45. package/dist/federation/index.d.ts +9 -0
  46. package/dist/federation/index.js +8 -1
  47. package/dist/finance/index.js +3 -4
  48. package/dist/fleet/index.d.ts +25 -0
  49. package/dist/fleet/index.js +57 -17
  50. package/dist/fleet/insights.js +4 -1
  51. package/dist/health/index.js +2 -2
  52. package/dist/hooks/ai-sdk.d.ts +17 -19
  53. package/dist/hooks/ai-sdk.js +13 -7
  54. package/dist/hooks/index.d.ts +1 -0
  55. package/dist/hooks/index.js +1 -0
  56. package/dist/hooks/mastra.d.ts +26 -0
  57. package/dist/hooks/mastra.js +63 -0
  58. package/dist/index.d.ts +24 -9
  59. package/dist/index.js +25 -9
  60. package/dist/mcp-requests/index.d.ts +26 -0
  61. package/dist/mcp-requests/index.js +154 -0
  62. package/dist/mcp-server/index.d.ts +12 -0
  63. package/dist/mcp-server/index.js +84 -2
  64. package/dist/mcp-wrap/index.js +8 -4
  65. package/dist/otlp/index.d.ts +155 -0
  66. package/dist/otlp/index.js +468 -0
  67. package/dist/otlp/protobuf.d.ts +7 -0
  68. package/dist/otlp/protobuf.js +314 -0
  69. package/dist/packs/index.d.ts +2 -0
  70. package/dist/packs/index.js +5 -1
  71. package/dist/pdf/arabic.d.ts +45 -0
  72. package/dist/pdf/arabic.js +373 -0
  73. package/dist/pdf/index.d.ts +77 -0
  74. package/dist/pdf/index.js +673 -0
  75. package/dist/policy/index.d.ts +0 -4
  76. package/dist/policy/index.js +13 -5
  77. package/dist/realtime/index.d.ts +33 -0
  78. package/dist/realtime/index.js +204 -0
  79. package/dist/reconcile/record.d.ts +14 -0
  80. package/dist/reconcile/record.js +161 -66
  81. package/dist/session/index.d.ts +43 -0
  82. package/dist/session/index.js +83 -0
  83. package/dist/undo/index.js +3 -3
  84. package/dist/verify/anchor.d.ts +5 -1
  85. package/dist/verify/anchor.js +13 -4
  86. package/dist/verify/chain.d.ts +4 -0
  87. package/dist/verify/chain.js +4 -3
  88. package/dist/verify/envelope.d.ts +1 -1
  89. package/dist/verify/envelope.js +5 -0
  90. package/dist/version.d.ts +1 -1
  91. package/dist/version.js +1 -1
  92. package/dist/walls/index.js +2 -2
  93. package/dist/warehouse/index.d.ts +62 -0
  94. package/dist/warehouse/index.js +142 -0
  95. package/package.json +6 -3
@@ -3,6 +3,7 @@
3
3
  import { createHash } from "node:crypto";
4
4
  import { taxonomyOf } from "../analysis/taxonomy.js";
5
5
  import { tokensOf } from "../cost/index.js";
6
+ import { canonical } from "../reconcile/shared.js";
6
7
  const hex = (s, n) => createHash("sha256").update(s).digest("hex").slice(0, n);
7
8
  const nanos = (ts) => `${Date.parse(ts)}000000`;
8
9
  const str = (key, v) => typeof v === "string" ? [{ key, value: { stringValue: v } }] : [];
@@ -525,3 +526,470 @@ export function fromOtlp(body) {
525
526
  }
526
527
  return { events };
527
528
  }
529
+ // ---------------------------------------------------------------- metrics and logs (spec/otlp.md §5)
530
+ const MAX_POINTS = 5000;
531
+ const NANOS = /^[0-9]{1,20}$/;
532
+ const DECIMAL = /^-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?$/;
533
+ const arr = (v) => (Array.isArray(v) ? v : []);
534
+ const obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
535
+ const idOf = (v) => createHash("sha256").update(canonical(v)).digest("hex").slice(0, 32);
536
+ const nanoStr = (v) => {
537
+ const s = typeof v === "string" || typeof v === "number" ? String(v) : "";
538
+ return NANOS.test(s) ? s : "";
539
+ };
540
+ /** An OTLP integer (a JSON number or a decimal string): a number when safe, else the string. */
541
+ const intOf = (v) => {
542
+ const s = typeof v === "number" && Number.isInteger(v) ? String(v) : typeof v === "string" ? v : "";
543
+ if (!/^-?[0-9]{1,20}$/.test(s))
544
+ return null;
545
+ const n = Number(s);
546
+ return Number.isSafeInteger(n) ? n : s;
547
+ };
548
+ /** spec/otlp.md §5: a double as integer micro-units, half up; null when it isn't a finite number
549
+ * or its micro-units aren't a safe integer. */
550
+ export function microOf(v) {
551
+ const n = typeof v === "number" ? v : typeof v === "string" && DECIMAL.test(v) ? Number(v) : NaN;
552
+ if (!Number.isFinite(n))
553
+ return null;
554
+ const m = Math.floor(n * 1e6 + 0.5);
555
+ return Number.isSafeInteger(m) ? m : null;
556
+ }
557
+ /** No floats in the record (house rule): a number that isn't a safe integer becomes its decimal
558
+ * text, as JavaScript writes it, at any depth. */
559
+ const integral = (v) => typeof v === "number"
560
+ ? Number.isSafeInteger(v)
561
+ ? v
562
+ : String(v)
563
+ : Array.isArray(v)
564
+ ? v.map(integral)
565
+ : v && typeof v === "object"
566
+ ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, integral(x)]))
567
+ : v;
568
+ /** A data point or log record about GenAI or this gateway: a `gen_ai.*` or `blackbox.*` key. */
569
+ const keyed = (a) => Object.keys(a).some((k) => k.startsWith("gen_ai.") || k.startsWith("blackbox."));
570
+ const scopeOf = (s) => {
571
+ const scope = obj(s);
572
+ return {
573
+ name: typeof scope.name === "string" ? scope.name : "",
574
+ ...(typeof scope.version === "string" ? { version: scope.version } : {}),
575
+ };
576
+ };
577
+ /** Over 60,000 bytes: the attributes of at most 1,000 bytes are kept (as for spans, §4). */
578
+ const capped = (data, drop = []) => {
579
+ if (size(data) <= MAX_DATA)
580
+ return data;
581
+ for (const k of drop)
582
+ delete data[k];
583
+ data.attributes = Object.fromEntries(Object.entries(data.attributes).filter(([, v]) => size(v) <= 1000));
584
+ data.truncated = true;
585
+ return data;
586
+ };
587
+ const put = (o, key, v) => {
588
+ if (v !== null && v !== undefined)
589
+ o[key] = v;
590
+ };
591
+ const METRIC_KINDS = [
592
+ ["gauge", "gauge"],
593
+ ["sum", "sum"],
594
+ ["histogram", "histogram"],
595
+ ["exponentialHistogram", "exponential_histogram"],
596
+ ["summary", "summary"],
597
+ ];
598
+ /**
599
+ * spec/otlp.md §5: OTLP/JSON metrics as session events, one per data point of a GenAI metric
600
+ * (`gen_ai.*`) or a point with a `gen_ai.*` or `blackbox.*` attribute (the resource's count);
601
+ * others are skipped. Doubles become integer micro-units; the event_id is a hash of the point's
602
+ * identity, so a resent point adds nothing.
603
+ */
604
+ export function fromOtlpMetrics(body) {
605
+ const rm = body?.resourceMetrics;
606
+ if (!body || typeof body !== "object" || !Array.isArray(rm))
607
+ return { error: "not OTLP/JSON metrics (no resourceMetrics)" };
608
+ const events = [];
609
+ let points = 0;
610
+ for (const r of arr(rm)) {
611
+ const res = attributes(obj(r.resource).attributes);
612
+ const service = typeof res["service.name"] === "string" ? res["service.name"] : undefined;
613
+ const sid = res["blackbox.session_id"];
614
+ for (const sm of arr(r.scopeMetrics)) {
615
+ const scope = scopeOf(sm.scope);
616
+ for (const m of arr(sm.metrics)) {
617
+ if (typeof m.name !== "string" || !m.name)
618
+ return { error: "a metric without a name" };
619
+ const name = m.name;
620
+ const found = METRIC_KINDS.find(([k]) => m[k] && typeof m[k] === "object");
621
+ if (!found)
622
+ continue;
623
+ const [field, kind] = found;
624
+ const agg = obj(m[field]);
625
+ for (const p of arr(agg.dataPoints)) {
626
+ if (++points > MAX_POINTS)
627
+ return { error: `more than ${MAX_POINTS} data points` };
628
+ const own = integral(attributes(p.attributes));
629
+ if (!name.startsWith("gen_ai.") && !keyed(own) && !keyed(res))
630
+ continue;
631
+ const attrs = {
632
+ ...(typeof sid === "string" ? { "blackbox.session_id": sid } : {}),
633
+ ...own,
634
+ };
635
+ const data = { source: "otlp", signal: "metrics" };
636
+ put(data, "service", service);
637
+ data.scope = scope;
638
+ data.metric = name;
639
+ if (typeof m.unit === "string" && m.unit)
640
+ data.unit = m.unit;
641
+ data.kind = kind;
642
+ if (typeof agg.aggregationTemporality === "number")
643
+ data.temporality = agg.aggregationTemporality;
644
+ if (typeof agg.isMonotonic === "boolean")
645
+ data.monotonic = agg.isMonotonic;
646
+ const start = nanoStr(p.startTimeUnixNano);
647
+ if (start && start !== "0")
648
+ data.start_unix_nano = start;
649
+ data.time_unix_nano = nanoStr(p.timeUnixNano);
650
+ data.attributes = attrs;
651
+ if (kind === "gauge" || kind === "sum") {
652
+ put(data, "value", p.asInt !== undefined ? intOf(p.asInt) : null);
653
+ put(data, "value_micro", p.asInt === undefined ? microOf(p.asDouble) : null);
654
+ }
655
+ else {
656
+ put(data, "count", intOf(p.count));
657
+ put(data, "sum_micro", microOf(p.sum));
658
+ put(data, "min_micro", microOf(p.min));
659
+ put(data, "max_micro", microOf(p.max));
660
+ if (kind === "histogram") {
661
+ const counts = arr(p.bucketCounts).map((c) => intOf(c));
662
+ const bounds = arr(p.explicitBounds).map((b) => microOf(b));
663
+ if (counts.length)
664
+ data.bucket_counts = counts;
665
+ if (bounds.length)
666
+ data.bounds_micro = bounds;
667
+ }
668
+ }
669
+ const id = idOf([service ?? null, scope.name, name, start, data.time_unix_nano, attrs]);
670
+ events.push({
671
+ event_id: `otel:m:${id}`,
672
+ type: "otel.metric",
673
+ name: name.slice(0, 256),
674
+ ts: isoOfNanos(p.timeUnixNano),
675
+ data: capped(data),
676
+ });
677
+ }
678
+ }
679
+ }
680
+ }
681
+ return { events };
682
+ }
683
+ /**
684
+ * spec/otlp.md §5: OTLP/JSON logs as session events, one per GenAI record (an event name
685
+ * `gen_ai.*`, or a `gen_ai.*` or `blackbox.*` attribute, the resource's included); others are
686
+ * skipped. The body is recorded as `body_sha256`, and itself only with `bodies: true`.
687
+ */
688
+ export function fromOtlpLogs(body, options = {}) {
689
+ const rl = body?.resourceLogs;
690
+ if (!body || typeof body !== "object" || !Array.isArray(rl))
691
+ return { error: "not OTLP/JSON logs (no resourceLogs)" };
692
+ const events = [];
693
+ let records = 0;
694
+ for (const r of arr(rl)) {
695
+ const res = attributes(obj(r.resource).attributes);
696
+ const service = typeof res["service.name"] === "string" ? res["service.name"] : undefined;
697
+ const sid = res["blackbox.session_id"];
698
+ for (const sl of arr(r.scopeLogs)) {
699
+ const scope = scopeOf(sl.scope);
700
+ for (const lr of arr(sl.logRecords)) {
701
+ if (++records > MAX_POINTS)
702
+ return { error: `more than ${MAX_POINTS} log records` };
703
+ const own = integral(attributes(lr.attributes));
704
+ const eventName = typeof lr.eventName === "string" && lr.eventName
705
+ ? lr.eventName
706
+ : typeof own["event.name"] === "string" && own["event.name"]
707
+ ? own["event.name"]
708
+ : undefined;
709
+ if (!eventName?.startsWith("gen_ai.") && !keyed(own) && !keyed(res))
710
+ continue;
711
+ const attrs = {
712
+ ...(typeof sid === "string" ? { "blackbox.session_id": sid } : {}),
713
+ ...own,
714
+ };
715
+ const data = { source: "otlp", signal: "logs" };
716
+ put(data, "service", service);
717
+ data.scope = scope;
718
+ const time = nanoStr(lr.timeUnixNano);
719
+ const observed = nanoStr(lr.observedTimeUnixNano);
720
+ data.time_unix_nano = time;
721
+ if (observed && observed !== "0")
722
+ data.observed_unix_nano = observed;
723
+ if (Number.isSafeInteger(lr.severityNumber) && lr.severityNumber !== 0)
724
+ data.severity_number = lr.severityNumber;
725
+ if (typeof lr.severityText === "string" && lr.severityText)
726
+ data.severity_text = lr.severityText;
727
+ put(data, "event_name", eventName);
728
+ if (typeof lr.traceId === "string" && /^[0-9a-f]{32}$/i.test(lr.traceId))
729
+ data.trace_id = lr.traceId.toLowerCase();
730
+ if (typeof lr.spanId === "string" && /^[0-9a-f]{16}$/i.test(lr.spanId))
731
+ data.span_id = lr.spanId.toLowerCase();
732
+ data.attributes = attrs;
733
+ const value = lr.body === undefined ? undefined : integral(anyValue(lr.body));
734
+ if (value !== undefined)
735
+ data.body_sha256 = `sha256:${createHash("sha256").update(canonical(value)).digest("hex")}`;
736
+ const id = idOf([service ?? null, scope.name, data]);
737
+ if (value !== undefined && options.bodies)
738
+ data.body = value;
739
+ events.push({
740
+ event_id: `otel:l:${id}`,
741
+ type: "otel.log",
742
+ name: String(eventName ?? data.severity_text ?? "log").slice(0, 256),
743
+ ts: isoOfNanos(time && time !== "0" ? time : observed),
744
+ data: capped(data, ["body"]),
745
+ });
746
+ }
747
+ }
748
+ }
749
+ return { events };
750
+ }
751
+ // ---------------------------------------------------------------- exporting metrics and logs (§6)
752
+ /** The GenAI conventions' advised bucket boundaries: tokens, and seconds (as milliseconds). */
753
+ const TOKEN_BOUNDS = [
754
+ 1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864,
755
+ ];
756
+ const SECOND_BOUNDS = [
757
+ 0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92,
758
+ ];
759
+ const MS_BOUNDS = [10, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120, 10240, 20480, 40960, 81920];
760
+ const collect = (m, attrs, v) => {
761
+ const key = JSON.stringify(attrs);
762
+ const p = m.get(key) ?? { attrs, values: [] };
763
+ p.values.push(v);
764
+ m.set(key, p);
765
+ };
766
+ const histogram = (m, bounds, scale, times) => [...m.values()].map((p) => {
767
+ const counts = new Array(bounds.length + 1).fill(0);
768
+ for (const v of p.values) {
769
+ const i = bounds.findIndex((b) => v <= b);
770
+ counts[i < 0 ? bounds.length : i] = counts[i < 0 ? bounds.length : i] + 1;
771
+ }
772
+ return {
773
+ attributes: p.attrs,
774
+ ...times,
775
+ count: String(p.values.length),
776
+ sum: p.values.reduce((a, b) => a + b, 0) / scale,
777
+ bucketCounts: counts.map(String),
778
+ explicitBounds: scale === 1 ? bounds : SECOND_BOUNDS,
779
+ min: Math.min(...p.values) / scale,
780
+ max: Math.max(...p.values) / scale,
781
+ };
782
+ });
783
+ /** What failed about a model call (the `error.type` of §2), or undefined. */
784
+ const callError = (end) => {
785
+ if (!end)
786
+ return "incomplete";
787
+ if (end.kind === "llm.incomplete")
788
+ return String(end.meta.reason ?? "incomplete");
789
+ const status = end.meta.status;
790
+ return typeof status === "number" && status >= 400 ? String(status) : undefined;
791
+ };
792
+ const resourceOf = (session, service = "zanii-blackbox") => ({
793
+ attributes: [...str("service.name", service), ...str("blackbox.session_id", session)],
794
+ });
795
+ /**
796
+ * spec/otlp.md §6: a session's metrics as OTLP/JSON: token usage and call duration (the GenAI
797
+ * client histograms), the calls witnessed, and the priced cost in micro-USD per model from its cost
798
+ * report (spec/cost.md), cumulative from the session's open to its last event.
799
+ */
800
+ export function toOtlpMetrics(lines, cost) {
801
+ const events = lines.map((l) => JSON.parse(l));
802
+ const first = events[0];
803
+ if (!first)
804
+ return { resourceMetrics: [] };
805
+ const session = first.session_id;
806
+ const times = {
807
+ startTimeUnixNano: nanos(first.ts),
808
+ timeUnixNano: nanos(events[events.length - 1].ts),
809
+ };
810
+ const ends = new Map();
811
+ for (const e of events)
812
+ if ((e.kind === "llm.response" || e.kind === "llm.incomplete") &&
813
+ typeof e.meta.request_seq === "number")
814
+ ends.set(e.meta.request_seq, e);
815
+ const tokens = new Map();
816
+ const durations = new Map();
817
+ let chats = 0;
818
+ for (const e of events.filter((x) => x.kind === "llm.request")) {
819
+ chats++;
820
+ const end = ends.get(e.seq);
821
+ const provider = typeof e.meta.provider === "string" ? e.meta.provider : undefined;
822
+ const model = end?.meta.model ?? e.meta.model;
823
+ const base = [
824
+ ...str("gen_ai.operation.name", "chat"),
825
+ ...str("gen_ai.provider.name", provider ? (PROVIDER_NAMES[provider] ?? provider) : undefined),
826
+ ...str("gen_ai.response.model", model),
827
+ ];
828
+ if (!end)
829
+ continue;
830
+ collect(durations, [...base, ...str("error.type", callError(end))], Math.max(0, Date.parse(end.ts) - Date.parse(e.ts)));
831
+ const usage = end.meta.usage && typeof end.meta.usage === "object"
832
+ ? tokensOf(end.meta.usage)
833
+ : null;
834
+ if (!usage)
835
+ continue;
836
+ collect(tokens, [...base, ...str("gen_ai.token.type", "input")], usage.input + usage.cache_read + usage.cache_write);
837
+ collect(tokens, [...base, ...str("gen_ai.token.type", "output")], usage.output);
838
+ }
839
+ const tools = events.filter((e) => e.kind === "tool.call").length;
840
+ const micro = new Map();
841
+ for (const c of cost?.calls ?? [])
842
+ if (typeof c.micro_usd === "number")
843
+ micro.set(c.model ?? "", (micro.get(c.model ?? "") ?? 0) + c.micro_usd);
844
+ const sum = (dataPoints) => ({
845
+ aggregationTemporality: 2,
846
+ isMonotonic: true,
847
+ dataPoints,
848
+ });
849
+ const metrics = [
850
+ {
851
+ name: "gen_ai.client.token.usage",
852
+ description: "Tokens used by the session's model calls",
853
+ unit: "{token}",
854
+ histogram: {
855
+ aggregationTemporality: 2,
856
+ dataPoints: histogram(tokens, TOKEN_BOUNDS, 1, times),
857
+ },
858
+ },
859
+ {
860
+ name: "gen_ai.client.operation.duration",
861
+ description: "The session's model calls, from the request to the answer",
862
+ unit: "s",
863
+ histogram: {
864
+ aggregationTemporality: 2,
865
+ dataPoints: histogram(durations, MS_BOUNDS, 1000, times),
866
+ },
867
+ },
868
+ {
869
+ name: "blackbox.session.calls",
870
+ description: "Calls the gateway witnessed",
871
+ unit: "{call}",
872
+ sum: sum([
873
+ ["chat", chats],
874
+ ["tool", tools],
875
+ ]
876
+ .filter(([, n]) => n > 0)
877
+ .map(([type, n]) => ({
878
+ attributes: str("blackbox.call.type", type),
879
+ ...times,
880
+ asInt: String(n),
881
+ }))),
882
+ },
883
+ {
884
+ name: "blackbox.session.cost",
885
+ description: "The session's priced cost in micro-USD (spec/cost.md)",
886
+ unit: "{micro_USD}",
887
+ sum: sum([...micro].map(([model, n]) => ({
888
+ attributes: str("gen_ai.response.model", model || undefined),
889
+ ...times,
890
+ asInt: String(n),
891
+ }))),
892
+ },
893
+ ].filter((m) => (m.histogram ?? m.sum)?.dataPoints.length);
894
+ return {
895
+ resourceMetrics: [
896
+ {
897
+ resource: resourceOf(session),
898
+ scopeMetrics: [{ scope: { name: "zanii-blackbox", version: "1" }, metrics }],
899
+ },
900
+ ],
901
+ };
902
+ }
903
+ /** A finding's severity as an OpenTelemetry severity number. */
904
+ const SEVERITY = {
905
+ advisory: 9,
906
+ caution: 13,
907
+ warning: 13,
908
+ medium: 13,
909
+ high: 17,
910
+ critical: 21,
911
+ };
912
+ /**
913
+ * spec/otlp.md §6: a session's findings as OTLP/JSON logs, one record each (event name
914
+ * `blackbox.finding`, the code as its body), in the session's trace (the root span's ids).
915
+ */
916
+ export function toOtlpLogs(lines) {
917
+ const events = lines.map((l) => JSON.parse(l));
918
+ const first = events[0];
919
+ if (!first)
920
+ return { resourceLogs: [] };
921
+ const session = first.session_id;
922
+ const logRecords = events
923
+ .filter((e) => e.kind === "finding")
924
+ .map((e) => {
925
+ const code = String(e.meta.code);
926
+ const severity = typeof e.meta.severity === "string" ? e.meta.severity : undefined;
927
+ return {
928
+ timeUnixNano: nanos(e.ts),
929
+ ...(severity && SEVERITY[severity] ? { severityNumber: SEVERITY[severity] } : {}),
930
+ ...(severity ? { severityText: severity } : {}),
931
+ body: { stringValue: code },
932
+ attributes: [
933
+ ...str("blackbox.finding.code", code),
934
+ ...str("blackbox.severity", severity),
935
+ ...str("blackbox.fault", e.meta.fault),
936
+ ...list("blackbox.owasp_asi", taxonomyOf(code).owasp.map((o) => o.id)),
937
+ ...list("blackbox.mitre_atlas", taxonomyOf(code).atlas.map((a) => a.id)),
938
+ ...int("blackbox.seq", e.seq),
939
+ ...str("gen_ai.conversation.id", session),
940
+ ],
941
+ traceId: hex(session, 32),
942
+ spanId: hex(`${session}:0`, 16),
943
+ eventName: "blackbox.finding",
944
+ };
945
+ });
946
+ return {
947
+ resourceLogs: [
948
+ {
949
+ resource: resourceOf(session),
950
+ scopeLogs: [{ scope: { name: "zanii-blackbox", version: "1" }, logRecords }],
951
+ },
952
+ ],
953
+ };
954
+ }
955
+ /**
956
+ * spec/otlp.md §7: the gateway's own spans for a session's forwarded requests, in the session's
957
+ * trace: each a SERVER span `forward {provider}`, child of the record's model-call span (§1, §2).
958
+ */
959
+ export function gatewayTrace(sessionId, calls) {
960
+ const spans = calls.map((c) => {
961
+ const error = c.reason !== undefined
962
+ ? c.reason
963
+ : typeof c.status === "number" && c.status >= 400
964
+ ? String(c.status)
965
+ : undefined;
966
+ return {
967
+ traceId: hex(sessionId, 32),
968
+ spanId: hex(`${sessionId}:gateway:${c.request_seq}`, 16),
969
+ parentSpanId: hex(`${sessionId}:${c.request_seq}`, 16),
970
+ name: `forward ${c.provider}`,
971
+ kind: 2,
972
+ startTimeUnixNano: c.start_unix_nano,
973
+ endTimeUnixNano: c.end_unix_nano,
974
+ attributes: [
975
+ ...str("gen_ai.provider.name", PROVIDER_NAMES[c.provider] ?? c.provider),
976
+ ...str("gen_ai.response.model", c.model),
977
+ ...str("blackbox.provider", c.provider),
978
+ ...int("http.response.status_code", c.status),
979
+ ...str("error.type", error),
980
+ ...int("blackbox.recorded_seq", c.request_seq),
981
+ ...int("blackbox.response_seq", c.response_seq),
982
+ ...str("gen_ai.conversation.id", sessionId),
983
+ ],
984
+ ...(error !== undefined ? { status: { code: 2, message: error } } : {}),
985
+ };
986
+ });
987
+ return {
988
+ resourceSpans: [
989
+ {
990
+ resource: resourceOf(sessionId, "zanii-blackbox-gateway"),
991
+ scopeSpans: [{ scope: { name: "zanii-blackbox-gateway", version: "1" }, spans }],
992
+ },
993
+ ],
994
+ };
995
+ }
@@ -2,3 +2,10 @@
2
2
  export declare function otlpFromProtobuf(buf: Uint8Array): {
3
3
  resourceSpans: object[];
4
4
  };
5
+ export type Signal = "traces" | "metrics" | "logs";
6
+ /** spec/otlp.md §6: an Export{Trace,Metrics,Logs}ServiceRequest's bytes as OTLP/JSON, every field
7
+ * read (64-bit integers as strings, ids as hex). Throws on bad bytes. */
8
+ export declare function otlpDecode(buf: Uint8Array, signal: Signal): Record<string, unknown>;
9
+ /** spec/otlp.md §6: OTLP/JSON as the Export{Trace,Metrics,Logs}ServiceRequest's protobuf bytes,
10
+ * the fields in number order, so both SDKs give the same bytes (spec/vectors/otlp-protobuf-out.json). */
11
+ export declare function otlpEncode(doc: object, signal: Signal): Uint8Array;