ai-model-verifier 1.4.1 → 2.0.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 (222) hide show
  1. package/README.md +79 -22
  2. package/dist/engine/context.d.ts +43 -0
  3. package/dist/engine/context.d.ts.map +1 -0
  4. package/dist/engine/context.js +50 -0
  5. package/dist/engine/context.js.map +1 -0
  6. package/dist/engine/evidence.d.ts +40 -0
  7. package/dist/engine/evidence.d.ts.map +1 -0
  8. package/dist/engine/evidence.js +108 -0
  9. package/dist/engine/evidence.js.map +1 -0
  10. package/dist/engine/handshake.d.ts +16 -0
  11. package/dist/engine/handshake.d.ts.map +1 -0
  12. package/dist/{handshake.js → engine/handshake.js} +35 -63
  13. package/dist/engine/handshake.js.map +1 -0
  14. package/dist/engine/probe-runner.d.ts +14 -0
  15. package/dist/engine/probe-runner.d.ts.map +1 -0
  16. package/dist/engine/probe-runner.js +163 -0
  17. package/dist/engine/probe-runner.js.map +1 -0
  18. package/dist/engine/verdict.d.ts +15 -0
  19. package/dist/engine/verdict.d.ts.map +1 -0
  20. package/dist/engine/verdict.js +19 -0
  21. package/dist/engine/verdict.js.map +1 -0
  22. package/dist/highlight.d.ts +2 -2
  23. package/dist/highlight.d.ts.map +1 -1
  24. package/dist/highlight.js +10 -9
  25. package/dist/highlight.js.map +1 -1
  26. package/dist/{patterns.d.ts → identity/patterns.d.ts} +7 -0
  27. package/dist/identity/patterns.d.ts.map +1 -0
  28. package/dist/{patterns.js → identity/patterns.js} +7 -0
  29. package/dist/identity/patterns.js.map +1 -0
  30. package/dist/identity/signals.d.ts +15 -0
  31. package/dist/identity/signals.d.ts.map +1 -0
  32. package/dist/{signals.js → identity/signals.js} +15 -24
  33. package/dist/identity/signals.js.map +1 -0
  34. package/dist/index.d.ts +15 -5
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +12 -5
  37. package/dist/index.js.map +1 -1
  38. package/dist/internal/utils.d.ts +3 -1
  39. package/dist/internal/utils.d.ts.map +1 -1
  40. package/dist/internal/utils.js +12 -6
  41. package/dist/internal/utils.js.map +1 -1
  42. package/dist/models/catalog.d.ts +7 -0
  43. package/dist/models/catalog.d.ts.map +1 -0
  44. package/dist/models/catalog.js +23 -0
  45. package/dist/models/catalog.js.map +1 -0
  46. package/dist/models/facts.d.ts +56 -0
  47. package/dist/models/facts.d.ts.map +1 -0
  48. package/dist/models/facts.js +91 -0
  49. package/dist/models/facts.js.map +1 -0
  50. package/dist/models/index.d.ts +6 -0
  51. package/dist/models/index.d.ts.map +1 -0
  52. package/dist/models/index.js +5 -0
  53. package/dist/models/index.js.map +1 -0
  54. package/dist/{substitution.d.ts → models/substitution.d.ts} +3 -4
  55. package/dist/models/substitution.d.ts.map +1 -0
  56. package/dist/{substitution.js → models/substitution.js} +3 -4
  57. package/dist/models/substitution.js.map +1 -0
  58. package/dist/models/tiers.d.ts +14 -0
  59. package/dist/models/tiers.d.ts.map +1 -0
  60. package/dist/models/tiers.js +35 -0
  61. package/dist/models/tiers.js.map +1 -0
  62. package/dist/probes/prompts.d.ts +23 -0
  63. package/dist/probes/prompts.d.ts.map +1 -0
  64. package/dist/probes/prompts.js +31 -0
  65. package/dist/probes/prompts.js.map +1 -0
  66. package/dist/probes/table.d.ts +37 -0
  67. package/dist/probes/table.d.ts.map +1 -0
  68. package/dist/probes/table.js +57 -0
  69. package/dist/probes/table.js.map +1 -0
  70. package/dist/registry.d.ts +26 -0
  71. package/dist/registry.d.ts.map +1 -0
  72. package/dist/registry.js +23 -0
  73. package/dist/registry.js.map +1 -0
  74. package/dist/rules/index.d.ts +15 -0
  75. package/dist/rules/index.d.ts.map +1 -0
  76. package/dist/rules/index.js +8 -0
  77. package/dist/rules/index.js.map +1 -0
  78. package/dist/rules/probe-ladder.d.ts +33 -0
  79. package/dist/rules/probe-ladder.d.ts.map +1 -0
  80. package/dist/rules/probe-ladder.js +156 -0
  81. package/dist/rules/probe-ladder.js.map +1 -0
  82. package/dist/{detectors → rules}/response-metadata.d.ts +11 -14
  83. package/dist/rules/response-metadata.d.ts.map +1 -0
  84. package/dist/{detectors → rules}/response-metadata.js +32 -15
  85. package/dist/rules/response-metadata.js.map +1 -0
  86. package/dist/rules/table.d.ts +27 -0
  87. package/dist/rules/table.d.ts.map +1 -0
  88. package/dist/rules/table.js +47 -0
  89. package/dist/rules/table.js.map +1 -0
  90. package/dist/{detectors → rules}/thinking-floor.d.ts +14 -29
  91. package/dist/rules/thinking-floor.d.ts.map +1 -0
  92. package/dist/rules/thinking-floor.js +119 -0
  93. package/dist/rules/thinking-floor.js.map +1 -0
  94. package/dist/rules/thinking-signature.d.ts +43 -0
  95. package/dist/rules/thinking-signature.d.ts.map +1 -0
  96. package/dist/rules/thinking-signature.js +72 -0
  97. package/dist/rules/thinking-signature.js.map +1 -0
  98. package/dist/{detectors → rules}/throughput.d.ts +7 -10
  99. package/dist/rules/throughput.d.ts.map +1 -0
  100. package/dist/{detectors → rules}/throughput.js +32 -11
  101. package/dist/rules/throughput.js.map +1 -0
  102. package/dist/{detectors → rules}/token-truth.d.ts +7 -13
  103. package/dist/rules/token-truth.d.ts.map +1 -0
  104. package/dist/rules/token-truth.js +112 -0
  105. package/dist/rules/token-truth.js.map +1 -0
  106. package/dist/{detectors → rules}/tokenizer-fingerprint.d.ts +7 -19
  107. package/dist/rules/tokenizer-fingerprint.d.ts.map +1 -0
  108. package/dist/{detectors → rules}/tokenizer-fingerprint.js +40 -58
  109. package/dist/rules/tokenizer-fingerprint.js.map +1 -0
  110. package/dist/rules/types.d.ts +57 -0
  111. package/dist/rules/types.d.ts.map +1 -0
  112. package/dist/rules/types.js +2 -0
  113. package/dist/rules/types.js.map +1 -0
  114. package/dist/transport.d.ts +10 -2
  115. package/dist/transport.d.ts.map +1 -1
  116. package/dist/transport.js +40 -32
  117. package/dist/transport.js.map +1 -1
  118. package/dist/types.d.ts +70 -31
  119. package/dist/types.d.ts.map +1 -1
  120. package/dist/vendors/anthropic.d.ts +44 -0
  121. package/dist/vendors/anthropic.d.ts.map +1 -0
  122. package/dist/vendors/anthropic.js +114 -0
  123. package/dist/vendors/anthropic.js.map +1 -0
  124. package/dist/vendors/base-url.d.ts +3 -0
  125. package/dist/vendors/base-url.d.ts.map +1 -0
  126. package/dist/vendors/base-url.js +6 -0
  127. package/dist/vendors/base-url.js.map +1 -0
  128. package/dist/vendors/gemini.d.ts +44 -0
  129. package/dist/vendors/gemini.d.ts.map +1 -0
  130. package/dist/vendors/gemini.js +77 -0
  131. package/dist/vendors/gemini.js.map +1 -0
  132. package/dist/vendors/index.d.ts +9 -0
  133. package/dist/vendors/index.d.ts.map +1 -0
  134. package/dist/vendors/index.js +7 -0
  135. package/dist/vendors/index.js.map +1 -0
  136. package/dist/vendors/openai.d.ts +46 -0
  137. package/dist/vendors/openai.d.ts.map +1 -0
  138. package/dist/vendors/openai.js +77 -0
  139. package/dist/vendors/openai.js.map +1 -0
  140. package/dist/vendors/table.d.ts +113 -0
  141. package/dist/vendors/table.d.ts.map +1 -0
  142. package/dist/vendors/table.js +14 -0
  143. package/dist/vendors/table.js.map +1 -0
  144. package/dist/vendors/types.d.ts +93 -0
  145. package/dist/vendors/types.d.ts.map +1 -0
  146. package/dist/vendors/types.js +2 -0
  147. package/dist/vendors/types.js.map +1 -0
  148. package/dist/verify.d.ts +30 -0
  149. package/dist/verify.d.ts.map +1 -0
  150. package/dist/verify.js +185 -0
  151. package/dist/verify.js.map +1 -0
  152. package/package.json +25 -10
  153. package/dist/detectors/response-metadata.d.ts.map +0 -1
  154. package/dist/detectors/response-metadata.js.map +0 -1
  155. package/dist/detectors/thinking-floor.d.ts.map +0 -1
  156. package/dist/detectors/thinking-floor.js +0 -135
  157. package/dist/detectors/thinking-floor.js.map +0 -1
  158. package/dist/detectors/thinking-signature.d.ts +0 -70
  159. package/dist/detectors/thinking-signature.d.ts.map +0 -1
  160. package/dist/detectors/thinking-signature.js +0 -177
  161. package/dist/detectors/thinking-signature.js.map +0 -1
  162. package/dist/detectors/throughput.d.ts.map +0 -1
  163. package/dist/detectors/throughput.js.map +0 -1
  164. package/dist/detectors/token-truth.d.ts.map +0 -1
  165. package/dist/detectors/token-truth.js +0 -142
  166. package/dist/detectors/token-truth.js.map +0 -1
  167. package/dist/detectors/tokenizer-fingerprint.d.ts.map +0 -1
  168. package/dist/detectors/tokenizer-fingerprint.js.map +0 -1
  169. package/dist/handshake.d.ts +0 -23
  170. package/dist/handshake.d.ts.map +0 -1
  171. package/dist/handshake.js.map +0 -1
  172. package/dist/internal/fixed-text.d.ts +0 -13
  173. package/dist/internal/fixed-text.d.ts.map +0 -1
  174. package/dist/internal/fixed-text.js +0 -13
  175. package/dist/internal/fixed-text.js.map +0 -1
  176. package/dist/models.d.ts +0 -5
  177. package/dist/models.d.ts.map +0 -1
  178. package/dist/models.js +0 -54
  179. package/dist/models.js.map +0 -1
  180. package/dist/nonce.d.ts +0 -4
  181. package/dist/nonce.d.ts.map +0 -1
  182. package/dist/nonce.js +0 -12
  183. package/dist/nonce.js.map +0 -1
  184. package/dist/patterns.d.ts.map +0 -1
  185. package/dist/patterns.js.map +0 -1
  186. package/dist/probes.d.ts +0 -10
  187. package/dist/probes.d.ts.map +0 -1
  188. package/dist/probes.js +0 -54
  189. package/dist/probes.js.map +0 -1
  190. package/dist/providers/anthropic.d.ts +0 -3
  191. package/dist/providers/anthropic.d.ts.map +0 -1
  192. package/dist/providers/anthropic.js +0 -67
  193. package/dist/providers/anthropic.js.map +0 -1
  194. package/dist/providers/config.d.ts +0 -34
  195. package/dist/providers/config.d.ts.map +0 -1
  196. package/dist/providers/config.js +0 -14
  197. package/dist/providers/config.js.map +0 -1
  198. package/dist/providers/gemini.d.ts +0 -3
  199. package/dist/providers/gemini.d.ts.map +0 -1
  200. package/dist/providers/gemini.js +0 -58
  201. package/dist/providers/gemini.js.map +0 -1
  202. package/dist/providers/openai.d.ts +0 -3
  203. package/dist/providers/openai.d.ts.map +0 -1
  204. package/dist/providers/openai.js +0 -56
  205. package/dist/providers/openai.js.map +0 -1
  206. package/dist/rules.d.ts +0 -6
  207. package/dist/rules.d.ts.map +0 -1
  208. package/dist/rules.js +0 -28
  209. package/dist/rules.js.map +0 -1
  210. package/dist/runner.d.ts +0 -39
  211. package/dist/runner.d.ts.map +0 -1
  212. package/dist/runner.js +0 -352
  213. package/dist/runner.js.map +0 -1
  214. package/dist/signals.d.ts +0 -11
  215. package/dist/signals.d.ts.map +0 -1
  216. package/dist/signals.js.map +0 -1
  217. package/dist/substitution.d.ts.map +0 -1
  218. package/dist/substitution.js.map +0 -1
  219. package/dist/verdict.d.ts +0 -26
  220. package/dist/verdict.d.ts.map +0 -1
  221. package/dist/verdict.js +0 -84
  222. package/dist/verdict.js.map +0 -1
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Anthropic thinking signature.
3
+ *
4
+ * Claude attaches a server-generated `signature` to every thinking block. A
5
+ * relay serving some other model cannot produce one, so this is the only
6
+ * evidence here that does not rest on asking the model who it is: identity and
7
+ * behaviour probes can be coached with a system prompt, a signature cannot.
8
+ *
9
+ * What it does NOT prove: which tier answered. Sonnet returns a perfectly valid
10
+ * signature when sold as Opus, so a pass here still needs the tier checks.
11
+ *
12
+ * The `strict` replay (hand the block back so the vendor re-validates it) was
13
+ * built and measured: it does not work through a relay. A genuine block and the
14
+ * same block with 308 bytes of random base64 in place of the signature both
15
+ * came back 200 from two live upstreams, because a relay re-issues the turn to
16
+ * its own backend rather than passing our block through. It stays off by
17
+ * default and only tells you the endpoint accepts a thinking block.
18
+ */
19
+ import { defineRule } from "./types";
20
+ /** Real signatures run to hundreds of chars; this only rejects a stub. */
21
+ const SIGNATURE_MIN_LEN = 50;
22
+ async function signatureResult(ctx) {
23
+ const empty = { signatureLength: 0, signaturePrefix: null, thinkingChars: 0 };
24
+ const reply = await ctx.evidence("thinkingReply");
25
+ if (!reply)
26
+ return { state: "skipped", ...empty, reason: "model-has-no-thinking-mode" };
27
+ const res = reply.res;
28
+ if (res.status === null || res.status >= 400)
29
+ return {
30
+ state: "skipped",
31
+ ...empty,
32
+ reason: `probe-failed:${res.status ?? "network"}`,
33
+ };
34
+ const found = ctx.wire.read.thinkingBlock?.(res.data) ?? null;
35
+ if (!found)
36
+ return { state: "no-thinking", ...empty, reason: "no-thinking-block" };
37
+ const sig = found.signature;
38
+ const base = {
39
+ signatureLength: sig.length,
40
+ signaturePrefix: sig ? sig.slice(0, 32) : null,
41
+ thinkingChars: found.chars,
42
+ };
43
+ if (!sig)
44
+ return { state: "unsigned", ...base, reason: "no-signature-field" };
45
+ if (sig.length < SIGNATURE_MIN_LEN)
46
+ return { state: "unsigned", ...base, reason: "signature-too-short" };
47
+ if (reply.replayAccepted === undefined)
48
+ return { state: "signed", ...base };
49
+ // A rejection is real evidence (the endpoint checked and refused); an
50
+ // acceptance is not, so it never upgrades the state on its own.
51
+ return {
52
+ state: reply.replayAccepted ? "signed" : "unsigned",
53
+ ...base,
54
+ replayVerified: reply.replayAccepted,
55
+ ...(reply.replayAccepted ? {} : { reason: "signature-rejected-on-replay" }),
56
+ };
57
+ }
58
+ export const signatureRule = defineRule({
59
+ id: "signature",
60
+ layer: "note",
61
+ needs: ["thinkingReply"],
62
+ check: "signature",
63
+ applies: (ctx) => ctx.wire.read.thinkingBlock !== undefined,
64
+ judge: async (ctx) => {
65
+ const r = await signatureResult(ctx);
66
+ return r.state === "no-thinking"
67
+ ? { severity: "note", reason: `signature: ${r.reason}` }
68
+ : null;
69
+ },
70
+ report: async (ctx) => ({ signature: await signatureResult(ctx) }),
71
+ });
72
+ //# sourceMappingURL=thinking-signature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thinking-signature.js","sourceRoot":"","sources":["../../src/rules/thinking-signature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,0EAA0E;AAC1E,MAAM,iBAAiB,GAAG,EAAE,CAAC;AA2B7B,KAAK,UAAU,eAAe,CAC5B,GAA6B;IAE7B,MAAM,KAAK,GAAG,EAAE,eAAe,EAAE,CAAC,EAAE,eAAe,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;IAC9E,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IAClD,IAAI,CAAC,KAAK;QACR,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,4BAA4B,EAAE,CAAC;IAC9E,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;IACtB,IAAI,GAAG,CAAC,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG;QAC1C,OAAO;YACL,KAAK,EAAE,SAAS;YAChB,GAAG,KAAK;YACR,MAAM,EAAE,gBAAgB,GAAG,CAAC,MAAM,IAAI,SAAS,EAAE;SAClD,CAAC;IACJ,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;IAC9D,IAAI,CAAC,KAAK;QACR,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAEzE,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC;IAC5B,MAAM,IAAI,GAAG;QACX,eAAe,EAAE,GAAG,CAAC,MAAM;QAC3B,eAAe,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;QAC9C,aAAa,EAAE,KAAK,CAAC,KAAK;KAC3B,CAAC;IACF,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC9E,IAAI,GAAG,CAAC,MAAM,GAAG,iBAAiB;QAChC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;IACvE,IAAI,KAAK,CAAC,cAAc,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC;IAC5E,sEAAsE;IACtE,gEAAgE;IAChE,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU;QACnD,GAAG,IAAI;QACP,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,8BAA8B,EAAE,CAAC;KAC5E,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;IACtC,EAAE,EAAE,WAAW;IACf,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,eAAe,CAAC;IACxB,KAAK,EAAE,WAAW;IAClB,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,KAAK,SAAS;IAC3D,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QACnB,MAAM,CAAC,GAAG,MAAM,eAAe,CAAC,GAAG,CAAC,CAAC;QACrC,OAAO,CAAC,CAAC,KAAK,KAAK,aAAa;YAC9B,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,MAAM,EAAE,EAAE;YACxD,CAAC,CAAC,IAAI,CAAC;IACX,CAAC;IACD,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC;CACnE,CAAC,CAAC"}
@@ -2,16 +2,12 @@
2
2
  * Throughput metadata: tokens per second, derived from data the run already
3
3
  * collected.
4
4
  *
5
- * Motivation: bigger models are slower per token, so a lane billed as Opus but
6
- * served by Sonnet tends to run fast. That makes throughput the one tier signal
7
- * that survives a relay refusing to name its model.
8
- *
9
- * It is metadata, not a verdict, and deliberately has no thresholds. Rate
10
- * varies with load, batching, quantisation and time of day, so a single
11
- * measurement cannot separate "cheaper model" from "quiet hour". The useful
12
- * comparison is across lanes serving the SAME model, which only the caller can
13
- * assemble: `compareThroughput` does that part and still only describes what it
14
- * sees.
5
+ * Bigger models are slower per token, so a lane billed as Opus but served by
6
+ * Sonnet tends to run fast. That makes throughput the one tier signal that
7
+ * survives a relay refusing to name its model. It is metadata, not a verdict,
8
+ * and deliberately has no thresholds: rate varies with load, batching,
9
+ * quantisation and time of day. The useful comparison is across lanes serving
10
+ * the SAME model, which only the caller can assemble with `compareThroughput`.
15
11
  */
16
12
  export type ThroughputSample = {
17
13
  /** Identifies the lane in the caller's own terms. */
@@ -48,4 +44,5 @@ export type ThroughputComparison = {
48
44
  export declare function compareThroughput(samples: ThroughputSample[], opts?: {
49
45
  outlierRatio?: number;
50
46
  }): ThroughputComparison[];
47
+ export declare const throughputRule: import("./types").Rule<"throughput", "probes", "note">;
51
48
  //# sourceMappingURL=throughput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throughput.d.ts","sourceRoot":"","sources":["../../src/rules/throughput.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACxC,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,gBAAgB,GAAG,IAAI,CAa1B;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,eAAe,EAAE,MAAM,CAAC;QACxB,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAC;KAChC,EAAE,CAAC;IACJ,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAQF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,gBAAgB,EAAE,EAC3B,IAAI,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/B,oBAAoB,EAAE,CAgDxB;AAED,eAAO,MAAM,cAAc,wDAmBzB,CAAC"}
@@ -2,17 +2,16 @@
2
2
  * Throughput metadata: tokens per second, derived from data the run already
3
3
  * collected.
4
4
  *
5
- * Motivation: bigger models are slower per token, so a lane billed as Opus but
6
- * served by Sonnet tends to run fast. That makes throughput the one tier signal
7
- * that survives a relay refusing to name its model.
8
- *
9
- * It is metadata, not a verdict, and deliberately has no thresholds. Rate
10
- * varies with load, batching, quantisation and time of day, so a single
11
- * measurement cannot separate "cheaper model" from "quiet hour". The useful
12
- * comparison is across lanes serving the SAME model, which only the caller can
13
- * assemble: `compareThroughput` does that part and still only describes what it
14
- * sees.
5
+ * Bigger models are slower per token, so a lane billed as Opus but served by
6
+ * Sonnet tends to run fast. That makes throughput the one tier signal that
7
+ * survives a relay refusing to name its model. It is metadata, not a verdict,
8
+ * and deliberately has no thresholds: rate varies with load, batching,
9
+ * quantisation and time of day. The useful comparison is across lanes serving
10
+ * the SAME model, which only the caller can assemble with `compareThroughput`.
15
11
  */
12
+ import { richestProbe } from "../engine/probe-runner";
13
+ import { hostOf } from "../internal/utils";
14
+ import { defineRule } from "./types";
16
15
  export function sampleThroughput(args) {
17
16
  const tokens = args.outputTokens;
18
17
  // A handful of tokens over a noisy connection says nothing; the rate is
@@ -62,7 +61,9 @@ export function compareThroughput(samples, opts) {
62
61
  lane: s.lane,
63
62
  tokensPerSecond: s.tokensPerSecond,
64
63
  ratioToMedian: +(s.tokensPerSecond / med).toFixed(2),
65
- direction: s.tokensPerSecond > med ? "faster" : "slower",
64
+ direction: s.tokensPerSecond > med
65
+ ? "faster"
66
+ : "slower",
66
67
  }))
67
68
  .sort((a, b) => b.tokensPerSecond - a.tokensPerSecond)
68
69
  : [];
@@ -80,4 +81,24 @@ export function compareThroughput(samples, opts) {
80
81
  }
81
82
  return out;
82
83
  }
84
+ export const throughputRule = defineRule({
85
+ id: "throughput",
86
+ layer: "note",
87
+ needs: ["probes"],
88
+ applies: () => true,
89
+ judge: async () => null,
90
+ report: async (ctx) => {
91
+ const richest = richestProbe(await ctx.evidence("probes"));
92
+ return richest
93
+ ? {
94
+ throughput: sampleThroughput({
95
+ lane: hostOf(ctx.baseUrl),
96
+ model: ctx.model,
97
+ outputTokens: richest.usage?.completion ?? null,
98
+ elapsedMs: richest.latencyMs,
99
+ }),
100
+ }
101
+ : {};
102
+ },
103
+ });
83
104
  //# sourceMappingURL=throughput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throughput.js","sourceRoot":"","sources":["../../src/rules/throughput.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Token-accounting checks for Anthropic endpoints.
2
+ * Token accounting.
3
3
  *
4
- * The signature check answers "is this really Claude". These answer a different
5
- * question the signature cannot: "are the numbers you are billing me true".
6
- * A relay can forward genuine Claude and still inflate `input_tokens` on every
4
+ * The signature answers "is this really Claude". This answers a different
5
+ * question the signature cannot: "are the numbers you are billing me true". A
6
+ * relay can forward genuine Claude and still inflate `input_tokens` on every
7
7
  * call, and nothing in an identity probe would notice.
8
8
  *
9
9
  * Ported from veridrop (AGPL-3.0) `token_usage.py` / `integrity.py`, whose
@@ -11,7 +11,7 @@
11
11
  * without re-measuring: the loose-looking bounds exist because real Anthropic
12
12
  * responses sat near them.
13
13
  */
14
- import type { TransportFn } from "../transport";
14
+ import type { ModelFacts } from "../models/facts";
15
15
  export type TokenTruthCheck = {
16
16
  id: "usage-present" | "input-delta" | "output-bounded" | "count-tokens";
17
17
  pass: boolean;
@@ -26,15 +26,9 @@ export type TokenTruthResult = {
26
26
  countTokens: number | null;
27
27
  reason?: string;
28
28
  };
29
- export declare function expectedInputDelta(model: string): {
29
+ export declare const expectedInputDelta: (facts: ModelFacts) => {
30
30
  min: number;
31
31
  max: number;
32
32
  };
33
- export declare function checkTokenTruth(opts: {
34
- transport: TransportFn;
35
- baseUrl: string;
36
- apiKey: string;
37
- model: string;
38
- timeoutMs: number;
39
- }): Promise<TokenTruthResult>;
33
+ export declare const tokenTruthRule: import("./types").Rule<"token-truth", "countTokens" | "fixedText", "note">;
40
34
  //# sourceMappingURL=token-truth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-truth.d.ts","sourceRoot":"","sources":["../../src/rules/token-truth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAelD,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,eAAe,GAAG,aAAa,GAAG,gBAAgB,GAAG,cAAc,CAAC;IACxE,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,0EAA0E;IAC1E,EAAE,EAAE,OAAO,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,kBAAkB,UAAW,UAAU;;;CACK,CAAC;AAmF1D,eAAO,MAAM,cAAc,4EAazB,CAAC"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Token accounting.
3
+ *
4
+ * The signature answers "is this really Claude". This answers a different
5
+ * question the signature cannot: "are the numbers you are billing me true". A
6
+ * relay can forward genuine Claude and still inflate `input_tokens` on every
7
+ * call, and nothing in an identity probe would notice.
8
+ *
9
+ * Ported from veridrop (AGPL-3.0) `token_usage.py` / `integrity.py`, whose
10
+ * tolerances were calibrated against the official API. Do not tighten them
11
+ * without re-measuring: the loose-looking bounds exist because real Anthropic
12
+ * responses sat near them.
13
+ */
14
+ import { COUNT_PROBE_MAX_TOKENS as MAX_TOKENS } from "../probes/prompts";
15
+ import { defineRule } from "./types";
16
+ /**
17
+ * Expected `input_tokens` growth between the short and long prompt. Two bands
18
+ * because Anthropic changed tokenizer: a relay claiming a new-tokenizer model
19
+ * while serving an older one lands in the wrong band, which makes this a tier
20
+ * check as well as a billing check.
21
+ */
22
+ const DELTA_OLD = { min: 45, max: 140 };
23
+ const DELTA_NEW = { min: 90, max: 230 };
24
+ /** `count_tokens` should agree with the billed `input_tokens` this closely. */
25
+ const countTolerance = (n) => Math.max(4, Math.round(n * 0.2));
26
+ export const expectedInputDelta = (facts) => facts.tokenizer === "claude-v2" ? DELTA_NEW : DELTA_OLD;
27
+ async function tokenTruthResult(ctx) {
28
+ const noCounts = {
29
+ shortInputTokens: null,
30
+ longInputTokens: null,
31
+ countTokens: null,
32
+ };
33
+ const ft = await ctx.evidence("fixedText");
34
+ if (ft.short.status === null || ft.short.status >= 400)
35
+ return {
36
+ ok: null,
37
+ checks: [],
38
+ ...noCounts,
39
+ reason: `probe-failed:${ft.short.status ?? "network"}`,
40
+ };
41
+ const s = ctx.wire.read.meta(ft.short.data).usage;
42
+ const l = ctx.wire.read.meta(ft.long.data).usage;
43
+ const checks = [];
44
+ const shortIn = s?.prompt ?? null;
45
+ const longIn = l?.prompt ?? null;
46
+ const usagePresent = shortIn !== null && longIn !== null;
47
+ checks.push({
48
+ id: "usage-present",
49
+ pass: usagePresent,
50
+ detail: usagePresent
51
+ ? `short=${shortIn}, long=${longIn}`
52
+ : "endpoint reported no usage counts",
53
+ });
54
+ // Without usage there is nothing to verify; a missing field is not fraud.
55
+ if (shortIn === null || longIn === null)
56
+ return { ok: null, checks, ...noCounts, reason: "no-usage-reported" };
57
+ const band = expectedInputDelta(ctx.facts);
58
+ const delta = longIn - shortIn;
59
+ checks.push({
60
+ id: "input-delta",
61
+ pass: delta >= band.min && delta <= band.max,
62
+ detail: `delta=${delta}, expected ${band.min}-${band.max}`,
63
+ });
64
+ // Output above the cap we set means the count is invented, not merely padded.
65
+ const outputs = [s?.completion ?? null, l?.completion ?? null].filter((v) => v !== null);
66
+ checks.push({
67
+ id: "output-bounded",
68
+ pass: outputs.every((v) => v > 0 && v <= MAX_TOKENS + 4),
69
+ detail: `outputs=${outputs.join(",") || "none"} (cap ${MAX_TOKENS})`,
70
+ });
71
+ // The same upstream pricing the same request without generating: the
72
+ // strongest anti-inflation signal available, when the endpoint exposes it.
73
+ // A missing count endpoint is common on relays and proves nothing, so it is
74
+ // simply absent from the checks rather than counted as a failure.
75
+ let counted = null;
76
+ const countRes = await ctx.evidence("countTokens");
77
+ if (countRes && countRes.status !== null && countRes.status < 400) {
78
+ counted = ctx.wire.read.countedTokens?.(countRes.data) ?? null;
79
+ if (counted !== null) {
80
+ const drift = Math.abs(counted - shortIn);
81
+ const tol = countTolerance(shortIn);
82
+ checks.push({
83
+ id: "count-tokens",
84
+ pass: drift <= tol,
85
+ detail: `count=${counted} vs billed=${shortIn}, drift=${drift} (tolerance ${tol})`,
86
+ });
87
+ }
88
+ }
89
+ return {
90
+ ok: checks.every((c) => c.pass),
91
+ checks,
92
+ shortInputTokens: shortIn,
93
+ longInputTokens: longIn,
94
+ countTokens: counted,
95
+ };
96
+ }
97
+ export const tokenTruthRule = defineRule({
98
+ id: "token-truth",
99
+ layer: "note",
100
+ needs: ["fixedText", "countTokens"],
101
+ check: "tokenTruth",
102
+ applies: (ctx) => ctx.wire.read.countedTokens !== undefined,
103
+ judge: async (ctx) => {
104
+ const r = await tokenTruthResult(ctx);
105
+ if (r.ok !== false)
106
+ return null;
107
+ const failed = r.checks.filter((c) => !c.pass).map((c) => c.id);
108
+ return { severity: "note", reason: `token-truth: ${failed.join(", ")}` };
109
+ },
110
+ report: async (ctx) => ({ tokenTruth: await tokenTruthResult(ctx) }),
111
+ });
112
+ //# sourceMappingURL=token-truth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-truth.js","sourceRoot":"","sources":["../../src/rules/token-truth.ts"],"names":[],"mappings":"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"}
@@ -13,15 +13,11 @@
13
13
  * Sonnet 4.6 and Haiku 4.5 share one tokenizer, so they bill the same delta on
14
14
  * the same path, and relays count differently from each other (77, 90, 134,
15
15
  * 160, 211 and 609 were all seen for one text on lanes echoing one model name)
16
- * or invent usage outright (13 tokens for eighty words, negative deltas on
17
- * repeat). A signature table therefore ships EMPTY: only a caller who has
18
- * calibrated a table on a specific endpoint family, per model generation and
19
- * across several text lengths, should pass one, and even then only
20
- * between-model differences on the same endpoint are evidence.
16
+ * or invent usage outright. A signature table therefore ships EMPTY: only a
17
+ * caller who has calibrated a table on a specific endpoint family should pass
18
+ * one, and even then only between-model differences on the same endpoint are
19
+ * evidence.
21
20
  */
22
- import type { TransportFn } from "../transport";
23
- /** Wire format the endpoint speaks; Claude is sold over both. */
24
- export type CountWire = "anthropic" | "openai";
25
21
  export type TierSignatures = Readonly<Record<string, readonly number[]>>;
26
22
  /** Empty on purpose; see the module note. Pass your own calibration. */
27
23
  export declare const DEFAULT_TIER_SIGNATURES: TierSignatures;
@@ -40,19 +36,11 @@ export type TokenizerFingerprintResult = {
40
36
  };
41
37
  export declare function tierForDelta(delta: number, signatures?: TierSignatures): string | null;
42
38
  /**
43
- * The served tier when the fingerprint names one cheaper than requested, or
44
- * null. `tiers` is the provider's tier vocabulary (opus, sonnet, haiku, fable).
39
+ * The served tier when the fingerprint names one other than requested, or
40
+ * null. `tiers` is the wire's tier vocabulary (opus, sonnet, haiku, fable).
45
41
  */
46
42
  export declare function judgeTokenizerFingerprint(requestedModel: string, result: TokenizerFingerprintResult, tiers: readonly string[]): string | null;
47
43
  /** True when two measurements of the same lane disagree: the backend changed. */
48
44
  export declare function fingerprintDrifted(prior: number | null | undefined, current: TokenizerFingerprintResult): boolean;
49
- export declare function measureTokenizerFingerprint(opts: {
50
- transport: TransportFn;
51
- baseUrl: string;
52
- apiKey: string;
53
- model: string;
54
- wire: CountWire;
55
- timeoutMs: number;
56
- signatures?: TierSignatures;
57
- }): Promise<TokenizerFingerprintResult>;
45
+ export declare const tokenizerFingerprintRule: import("./types").Rule<"tokenizer-fingerprint", "fixedText", "evidence">;
58
46
  //# sourceMappingURL=tokenizer-fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenizer-fingerprint.d.ts","sourceRoot":"","sources":["../../src/rules/tokenizer-fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;AAEzE,wEAAwE;AACxE,eAAO,MAAM,uBAAuB,EAAE,cAAmB,CAAC;AAE1D,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,YAAY,CAAC;AAElE,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,yBAAyB,CAAC;IACjC,6DAA6D;IAC7D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,yEAAyE;IACzE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,4DAA4D;IAC5D,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,cAAwC,GACnD,MAAM,GAAG,IAAI,CAIf;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,0BAA0B,EAClC,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,MAAM,GAAG,IAAI,CAMf;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAOT;AA4CD,eAAO,MAAM,wBAAwB,0EAuBnC,CAAC"}
@@ -13,28 +13,14 @@
13
13
  * Sonnet 4.6 and Haiku 4.5 share one tokenizer, so they bill the same delta on
14
14
  * the same path, and relays count differently from each other (77, 90, 134,
15
15
  * 160, 211 and 609 were all seen for one text on lanes echoing one model name)
16
- * or invent usage outright (13 tokens for eighty words, negative deltas on
17
- * repeat). A signature table therefore ships EMPTY: only a caller who has
18
- * calibrated a table on a specific endpoint family, per model generation and
19
- * across several text lengths, should pass one, and even then only
20
- * between-model differences on the same endpoint are evidence.
16
+ * or invent usage outright. A signature table therefore ships EMPTY: only a
17
+ * caller who has calibrated a table on a specific endpoint family should pass
18
+ * one, and even then only between-model differences on the same endpoint are
19
+ * evidence.
21
20
  */
22
- import { COUNT_PROBE_MAX_TOKENS, LONG_PROMPT, SHORT_PROMPT, } from "../internal/fixed-text";
21
+ import { defineRule } from "./types";
23
22
  /** Empty on purpose; see the module note. Pass your own calibration. */
24
23
  export const DEFAULT_TIER_SIGNATURES = {};
25
- const intOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 ? v : null;
26
- function readInputTokens(data, wire) {
27
- const u = data?.usage;
28
- if (!u || typeof u !== "object")
29
- return null;
30
- return wire === "anthropic"
31
- ? intOf(u["input_tokens"])
32
- : intOf(u["prompt_tokens"]);
33
- }
34
- function readServedModel(data) {
35
- const m = data?.model;
36
- return typeof m === "string" && m.length > 0 ? m : null;
37
- }
38
24
  export function tierForDelta(delta, signatures = DEFAULT_TIER_SIGNATURES) {
39
25
  for (const [tier, values] of Object.entries(signatures))
40
26
  if (values.includes(delta))
@@ -42,8 +28,8 @@ export function tierForDelta(delta, signatures = DEFAULT_TIER_SIGNATURES) {
42
28
  return null;
43
29
  }
44
30
  /**
45
- * The served tier when the fingerprint names one cheaper than requested, or
46
- * null. `tiers` is the provider's tier vocabulary (opus, sonnet, haiku, fable).
31
+ * The served tier when the fingerprint names one other than requested, or
32
+ * null. `tiers` is the wire's tier vocabulary (opus, sonnet, haiku, fable).
47
33
  */
48
34
  export function judgeTokenizerFingerprint(requestedModel, result, tiers) {
49
35
  if (result.state !== "measured" || result.matchedTier === null)
@@ -61,36 +47,8 @@ export function fingerprintDrifted(prior, current) {
61
47
  current.delta !== null &&
62
48
  current.delta !== prior);
63
49
  }
64
- export async function measureTokenizerFingerprint(opts) {
65
- const base = opts.baseUrl.replace(/\/+$/, "");
66
- const url = opts.wire === "anthropic"
67
- ? `${base}/v1/messages`
68
- : `${base}/v1/chat/completions`;
69
- const headers = opts.wire === "anthropic"
70
- ? {
71
- "Content-Type": "application/json",
72
- "x-api-key": opts.apiKey,
73
- "anthropic-version": "2023-06-01",
74
- }
75
- : {
76
- "Content-Type": "application/json",
77
- Authorization: `Bearer ${opts.apiKey}`,
78
- };
79
- const ask = (prompt) => opts.transport({
80
- mode: "direct",
81
- url,
82
- headers,
83
- reqBody: {
84
- model: opts.model,
85
- max_tokens: COUNT_PROBE_MAX_TOKENS,
86
- messages: [{ role: "user", content: prompt }],
87
- },
88
- timeoutMs: opts.timeoutMs,
89
- });
90
- const [shortRes, longRes] = await Promise.all([
91
- ask(SHORT_PROMPT),
92
- ask(LONG_PROMPT),
93
- ]);
50
+ async function fingerprintResult(ctx) {
51
+ const ft = await ctx.evidence("fixedText");
94
52
  const empty = {
95
53
  delta: null,
96
54
  shortInputTokens: null,
@@ -98,21 +56,21 @@ export async function measureTokenizerFingerprint(opts) {
98
56
  servedModel: null,
99
57
  matchedTier: null,
100
58
  };
101
- for (const r of [shortRes, longRes])
59
+ for (const r of [ft.short, ft.long])
102
60
  if (r.status === null || r.status >= 400)
103
61
  return {
104
62
  state: "unmeasured",
105
63
  ...empty,
106
64
  reason: `probe-failed:${r.status ?? "network"}`,
107
65
  };
108
- const short = readInputTokens(shortRes.data, opts.wire);
109
- const long = readInputTokens(longRes.data, opts.wire);
110
- const servedModel = readServedModel(shortRes.data);
66
+ const shortMeta = ctx.wire.read.meta(ft.short.data);
67
+ const short = shortMeta.usage?.prompt ?? null;
68
+ const long = ctx.wire.read.meta(ft.long.data).usage?.prompt ?? null;
111
69
  if (short === null || long === null)
112
70
  return {
113
71
  state: "unmeasured",
114
72
  ...empty,
115
- servedModel,
73
+ servedModel: shortMeta.detectedModel,
116
74
  reason: "no-usage-reported",
117
75
  };
118
76
  const delta = long - short;
@@ -121,8 +79,32 @@ export async function measureTokenizerFingerprint(opts) {
121
79
  delta,
122
80
  shortInputTokens: short,
123
81
  longInputTokens: long,
124
- servedModel,
125
- matchedTier: tierForDelta(delta, opts.signatures),
82
+ servedModel: shortMeta.detectedModel,
83
+ matchedTier: tierForDelta(delta, ctx.checks.tokenizerFingerprint?.signatures),
126
84
  };
127
85
  }
86
+ export const tokenizerFingerprintRule = defineRule({
87
+ id: "tokenizer-fingerprint",
88
+ layer: "evidence",
89
+ needs: ["fixedText"],
90
+ check: "tokenizerFingerprint",
91
+ // Claude only, over either wire that carries its usage as is.
92
+ applies: (ctx) => ctx.facts.vendor === "anthropic" && ctx.wire.id !== "gemini",
93
+ judge: async (ctx) => {
94
+ if (!ctx.wire.tiers)
95
+ return null;
96
+ const r = await fingerprintResult(ctx);
97
+ const tier = judgeTokenizerFingerprint(ctx.model, r, ctx.wire.tiers);
98
+ return tier
99
+ ? {
100
+ severity: "fail",
101
+ reason: `tokenizer-fingerprint: delta ${r.delta} is the ${tier} signature, requested ${ctx.model}`,
102
+ data: r,
103
+ }
104
+ : null;
105
+ },
106
+ report: async (ctx) => ({
107
+ tokenizerFingerprint: await fingerprintResult(ctx),
108
+ }),
109
+ });
128
110
  //# sourceMappingURL=tokenizer-fingerprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenizer-fingerprint.js","sourceRoot":"","sources":["../../src/rules/tokenizer-fingerprint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import type { ResolvedChecks, RunCtx } from "../engine/context";
2
+ import type { EvidenceBag, EvidenceKey } from "../engine/evidence";
3
+ import type { ResponseMetadata } from "./response-metadata";
4
+ import type { SignatureResult } from "./thinking-signature";
5
+ import type { ThinkingFloorResult } from "./thinking-floor";
6
+ import type { ThroughputSample } from "./throughput";
7
+ import type { TokenTruthResult } from "./token-truth";
8
+ import type { TokenizerFingerprintResult } from "./tokenizer-fingerprint";
9
+ /**
10
+ * How a finding weighs on the verdict. `fail` and `suspect` both make the
11
+ * endpoint suspicious (kept apart so a consumer policy can demote one),
12
+ * `inconclusive` leaves it unverified, `note` never touches the verdict.
13
+ */
14
+ export type Severity = "fail" | "suspect" | "inconclusive" | "note";
15
+ /**
16
+ * Which findings outrank which: `evidence` rules read hard facts the probes
17
+ * cannot see and beat everything; `probe` rules read the behavioural probes and
18
+ * decide `versionUnverifiable`; `note` rules only report.
19
+ */
20
+ export type RuleLayer = "evidence" | "probe" | "note";
21
+ /** What a rule returns; the engine stamps the rule id and layer on. */
22
+ export type RuleVerdict = {
23
+ severity: Severity;
24
+ reason: string;
25
+ data?: unknown;
26
+ };
27
+ export type Finding<Id extends string = string> = RuleVerdict & {
28
+ rule: Id;
29
+ layer: RuleLayer;
30
+ };
31
+ /** Per-rule artefacts copied onto the result, present only when the rule ran. */
32
+ export type Reports = {
33
+ signature?: SignatureResult;
34
+ tokenTruth?: TokenTruthResult;
35
+ thinkingFloor?: ThinkingFloorResult;
36
+ tokenizerFingerprint?: TokenizerFingerprintResult;
37
+ responseMetadata?: ResponseMetadata;
38
+ throughput?: ThroughputSample | null;
39
+ };
40
+ export type CheckKey = keyof ResolvedChecks;
41
+ /** A rule sees the run and only the evidence it declared in `needs`. */
42
+ export type RuleCtx<N extends EvidenceKey = EvidenceKey> = RunCtx & {
43
+ evidence<K extends N>(key: K): Promise<EvidenceBag[K]>;
44
+ };
45
+ export type Rule<Id extends string = string, N extends EvidenceKey = EvidenceKey, L extends RuleLayer = RuleLayer> = {
46
+ id: Id;
47
+ layer: L;
48
+ needs: readonly N[];
49
+ /** Opt-in switch in `checks` that enables this rule under `verify`. */
50
+ check?: CheckKey;
51
+ /** Whether the rule has anything to say on this wire and model. */
52
+ applies(ctx: RunCtx): boolean;
53
+ judge(ctx: RuleCtx<N>): Promise<RuleVerdict | null>;
54
+ report?(ctx: RuleCtx<N>): Promise<Partial<Reports>>;
55
+ };
56
+ export declare const defineRule: <const Id extends string, const N extends EvidenceKey, const L extends RuleLayer>(rule: Rule<Id, N, L>) => Rule<Id, N, L>;
57
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/rules/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export const defineRule = (rule) => rule;
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/rules/types.ts"],"names":[],"mappings":"AAoEA,MAAM,CAAC,MAAM,UAAU,GAAG,CAKxB,IAAoB,EACJ,EAAE,CAAC,IAAI,CAAC"}