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
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  **Is that API endpoint really serving the model it claims?**
4
4
 
5
5
  Resellers and relay stations sell "Claude Opus" or "GPT-5" and quietly serve
6
- something cheaper. Given a `base_url`, `api_key` and `model`, this library
6
+ something cheaper. Given a `baseUrl`, `apiKey` and `model`, this library
7
7
  probes the endpoint and reports what it actually found: a substituted model, a
8
8
  downgraded Claude tier, a stripped thinking signature, inflated token billing,
9
9
  or a translation layer sitting in front of the real vendor.
@@ -13,36 +13,93 @@ engine runs in the browser, on a server, and inside our sync pipeline.
13
13
 
14
14
  ## Design
15
15
 
16
- - **Isomorphic.** Web-standard APIs only (`fetch`, `TextEncoder`,
17
- `AbortSignal`); no `node:` imports. The HTTP call itself is injected as a
18
- `TransportFn`, so a browser can route around CORS and a server can route
19
- through an SSRF-safe fetch.
20
- - **Opt-in.** Providers and detectors have their own entry points, so a consumer
21
- that only checks Anthropic never bundles the Gemini config.
16
+ - **Adapters.** One object per wire format (`anthropic`, `openai`, `gemini`)
17
+ builds every request and reads every reply. Rules never see a URL.
18
+ - **Rules over evidence.** Every detection is a rule that declares the
19
+ evidence it needs (the four behavioural probes, a fixed-text pair, a thinking
20
+ reply, a floor reply). Evidence is collected once per run and shared; the
21
+ rule table's order is the verdict's precedence.
22
+ - **Model facts.** One glob table says which models think adaptively, which
23
+ tokenizer generation they bill with, which cannot switch reasoning off.
24
+ - **Isomorphic.** Web-standard APIs only, no `node:` imports. The HTTP call is
25
+ an injected `TransportFn`, so a browser can route through its backend and a
26
+ server through an SSRF-safe fetch.
22
27
  - **Zero required dependencies.**
23
28
 
29
+ ## Install
30
+
31
+ ```sh
32
+ npm i ai-model-verifier
33
+ ```
34
+
24
35
  ## Use
25
36
 
26
37
  ```ts
27
- import { runVerification } from "ai-model-verifier";
28
- import { anthropicConfig } from "ai-model-verifier/providers/anthropic";
38
+ import { verify } from "ai-model-verifier";
39
+
40
+ const result = await verify({
41
+ vendor: "anthropic",
42
+ baseUrl: "https://relay.example.com",
43
+ apiKey: process.env.RELAY_KEY!,
44
+ model: "claude-opus-4-6",
45
+ mode: "direct",
46
+ checks: { signature: true, tokenTruth: true, tokenizerFingerprint: true },
47
+ });
48
+ console.log(result.verdict, result.reasons, result.findings);
49
+ ```
50
+
51
+ `runRules` judges chosen rules on a wire you already know works, without a
52
+ handshake, and is what a pipeline with its own connectivity probes wants:
53
+
54
+ ```ts
55
+ import { runRules } from "ai-model-verifier";
56
+
57
+ const run = await runRules({
58
+ vendor: "openai",
59
+ baseUrl,
60
+ apiKey,
61
+ model: "pool/claude-opus-4-6",
62
+ mode: "server",
63
+ transport: myFetch,
64
+ bodyExtras: { provider: ["bedrock"] },
65
+ only: ["coding-tool", "foreign", "served-model-mismatch", "quorum", "tokenizer-fingerprint"],
66
+ });
67
+ for (const f of run.findings) console.log(f.rule, f.severity, f.reason);
68
+ ```
69
+
70
+ Extend without forking:
71
+
72
+ ```ts
73
+ import { createVerifier, defineRule, defineVendor, defineModelFacts } from "ai-model-verifier";
74
+
75
+ const { verify } = createVerifier({
76
+ vendors: [defineVendor({ id: "mywire", ... })],
77
+ rules: [defineRule({ id: "my-rule", layer: "probe", needs: ["probes"], applies: () => true, judge: async (ctx) => ... })],
78
+ omit: ["substituted"],
79
+ modelFacts: defineModelFacts([{ match: "my-model*", alwaysThinks: true }]),
80
+ });
29
81
  ```
30
82
 
31
83
  ## What it detects
32
84
 
33
- `coding-tool` refusals (Kiro/Amazon Q wearing a Claude badge), scam pages,
34
- CJK language leaks from a substituted Chinese model, response-mixing proxies,
35
- foreign vendor identity, Claude tier substitution (opus billed, sonnet served),
36
- envelope-level model substitution, and (opt-in, `checkThinkingFloor`) a
37
- model that cannot switch thinking off answering with no reasoning tokens, the
38
- signature of a flash tier sold as Gemini 2.5 Pro, and (opt-in,
39
- `checkTokenizerFingerprint`) the input-token delta for a fixed text, which no
40
- system prompt can coach. Its job is drift: the delta is deterministic per
41
- endpoint, so a lane whose delta moves between two measurements changed its
42
- backend whatever the reply now calls itself. It does not name a tier on its
43
- own (Opus 4.6, Sonnet 4.6 and Haiku 4.5 share a tokenizer, and relays count
44
- differently from one another), so the signature table ships empty and is a
45
- per-endpoint calibration the caller supplies.
85
+ Verdict rules, in precedence order: a must-think model answering with no
86
+ reasoning tokens (`thinking-floor`, opt-in); a tokenizer fingerprint matching a
87
+ cheaper tier under a caller-supplied calibration (`tokenizer-fingerprint`,
88
+ opt-in; ships no table, see the rule's note); `coding-tool` refusals (Kiro or
89
+ Amazon Q wearing a Claude badge); scam pages; CJK language leaks from a
90
+ substituted Chinese model; response-mixing proxies (`mux`); a foreign vendor
91
+ named as the maker; the reply's own `model` field naming another tier
92
+ (`served-model-mismatch`) or another model (`substituted`); and a probe quorum
93
+ that only fails on non-transient evidence.
94
+
95
+ Note rules report without judging: the Claude thinking signature, token
96
+ accounting against the billed usage, a self-reported tier, the response
97
+ envelope (which vendor's shape and id minted the reply) and throughput.
98
+
99
+ ## Tests
100
+
101
+ `bun test` replays recordings made with the 1.4.1 build (`test/parity`) and a
102
+ set of synthetic endpoints. `test/parity/record.ts` records a new fixture.
46
103
 
47
104
  ## License
48
105
 
@@ -0,0 +1,43 @@
1
+ import type { ModelFacts } from "../models/facts";
2
+ import type { TierSignatures } from "../rules/tokenizer-fingerprint";
3
+ import type { TransportFn, TransportMode, TransportResult } from "../transport";
4
+ import type { Checks, ProbeAttempt } from "../types";
5
+ import type { BuiltRequest, ChatRequest, VendorAdapter, WireCtx } from "../vendors/types";
6
+ export type ResolvedChecks = {
7
+ signature: {
8
+ strict: boolean;
9
+ } | null;
10
+ tokenTruth: boolean;
11
+ thinkingFloor: {
12
+ minCompletionTokens: number | null;
13
+ } | null;
14
+ tokenizerFingerprint: {
15
+ signatures: TierSignatures | undefined;
16
+ } | null;
17
+ };
18
+ export declare function resolveChecks(checks: Checks | undefined): ResolvedChecks;
19
+ /** Everything a rule may read about the run. */
20
+ export type RunCtx<V extends string = string> = {
21
+ model: string;
22
+ facts: ModelFacts;
23
+ requestedVendor: V;
24
+ /** Wire the requests go over (after a handshake fallback, not the requested one). */
25
+ wire: VendorAdapter<V>;
26
+ mode: TransportMode;
27
+ direct: boolean;
28
+ baseUrl: string;
29
+ apiKey: string;
30
+ timeoutMs: number;
31
+ transport: TransportFn;
32
+ bodyExtras?: Record<string, unknown>;
33
+ nonce: () => string;
34
+ onProbe?: (attempt: ProbeAttempt) => void;
35
+ checks: ResolvedChecks;
36
+ };
37
+ export declare const wireCtx: (ctx: RunCtx) => WireCtx;
38
+ /** Multipart and binary bodies pass through untouched. */
39
+ export declare function mergeBodyExtras(body: unknown, extras?: Record<string, unknown>): unknown;
40
+ export declare function callWire(ctx: RunCtx, built: BuiltRequest): Promise<TransportResult>;
41
+ export declare const buildChat: (ctx: RunCtx, req: Omit<ChatRequest, "model">) => BuiltRequest;
42
+ export declare const chat: (ctx: RunCtx, req: Omit<ChatRequest, "model">) => Promise<TransportResult>;
43
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/engine/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,aAAa,EACb,OAAO,EACR,MAAM,kBAAkB,CAAC;AAE1B,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACtC,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE;QAAE,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7D,oBAAoB,EAAE;QAAE,UAAU,EAAE,cAAc,GAAG,SAAS,CAAA;KAAE,GAAG,IAAI,CAAC;CACzE,CAAC;AAEF,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,CA2BxE;AAED,gDAAgD;AAChD,MAAM,MAAM,MAAM,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,CAAC,CAAC;IACnB,qFAAqF;IACrF,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IACvB,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,WAAW,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IAC1C,MAAM,EAAE,cAAc,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,OAAO,QAAS,MAAM,KAAG,OAKpC,CAAC;AAEH,0DAA0D;AAC1D,wBAAgB,eAAe,CAC7B,IAAI,EAAE,OAAO,EACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAGT;AAED,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,eAAe,CAAC,CAQ1B;AAED,eAAO,MAAM,SAAS,QACf,MAAM,OACN,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,KAC9B,YAC4D,CAAC;AAEhE,eAAO,MAAM,IAAI,QAAS,MAAM,OAAO,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,6BAC7B,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { isRecord } from "../internal/utils";
2
+ export function resolveChecks(checks) {
3
+ const c = checks ?? {};
4
+ return {
5
+ signature: c.signature
6
+ ? {
7
+ strict: typeof c.signature === "object" && c.signature.strict === true,
8
+ }
9
+ : null,
10
+ tokenTruth: c.tokenTruth === true,
11
+ thinkingFloor: c.thinkingFloor
12
+ ? {
13
+ minCompletionTokens: typeof c.thinkingFloor === "object"
14
+ ? (c.thinkingFloor.minCompletionTokens ?? null)
15
+ : null,
16
+ }
17
+ : null,
18
+ tokenizerFingerprint: c.tokenizerFingerprint
19
+ ? {
20
+ signatures: typeof c.tokenizerFingerprint === "object"
21
+ ? c.tokenizerFingerprint.signatures
22
+ : undefined,
23
+ }
24
+ : null,
25
+ };
26
+ }
27
+ export const wireCtx = (ctx) => ({
28
+ baseUrl: ctx.baseUrl,
29
+ apiKey: ctx.apiKey,
30
+ direct: ctx.direct,
31
+ facts: ctx.facts,
32
+ });
33
+ /** Multipart and binary bodies pass through untouched. */
34
+ export function mergeBodyExtras(body, extras) {
35
+ if (!extras || !isRecord(body))
36
+ return body;
37
+ return { ...body, ...extras };
38
+ }
39
+ export function callWire(ctx, built) {
40
+ return ctx.transport({
41
+ mode: ctx.mode,
42
+ url: built.url,
43
+ headers: built.headers,
44
+ reqBody: mergeBodyExtras(built.body, ctx.bodyExtras),
45
+ timeoutMs: ctx.timeoutMs,
46
+ });
47
+ }
48
+ export const buildChat = (ctx, req) => ctx.wire.ops.chat({ model: ctx.model, ...req }, wireCtx(ctx));
49
+ export const chat = (ctx, req) => callWire(ctx, buildChat(ctx, req));
50
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/engine/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAmB7C,MAAM,UAAU,aAAa,CAAC,MAA0B;IACtD,MAAM,CAAC,GAAG,MAAM,IAAI,EAAE,CAAC;IACvB,OAAO;QACL,SAAS,EAAE,CAAC,CAAC,SAAS;YACpB,CAAC,CAAC;gBACE,MAAM,EACJ,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,KAAK,IAAI;aACjE;YACH,CAAC,CAAC,IAAI;QACR,UAAU,EAAE,CAAC,CAAC,UAAU,KAAK,IAAI;QACjC,aAAa,EAAE,CAAC,CAAC,aAAa;YAC5B,CAAC,CAAC;gBACE,mBAAmB,EACjB,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ;oBACjC,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,mBAAmB,IAAI,IAAI,CAAC;oBAC/C,CAAC,CAAC,IAAI;aACX;YACH,CAAC,CAAC,IAAI;QACR,oBAAoB,EAAE,CAAC,CAAC,oBAAoB;YAC1C,CAAC,CAAC;gBACE,UAAU,EACR,OAAO,CAAC,CAAC,oBAAoB,KAAK,QAAQ;oBACxC,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,UAAU;oBACnC,CAAC,CAAC,SAAS;aAChB;YACH,CAAC,CAAC,IAAI;KACT,CAAC;AACJ,CAAC;AAqBD,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,GAAW,EAAW,EAAE,CAAC,CAAC;IAChD,OAAO,EAAE,GAAG,CAAC,OAAO;IACpB,MAAM,EAAE,GAAG,CAAC,MAAM;IAClB,MAAM,EAAE,GAAG,CAAC,MAAM;IAClB,KAAK,EAAE,GAAG,CAAC,KAAK;CACjB,CAAC,CAAC;AAEH,0DAA0D;AAC1D,MAAM,UAAU,eAAe,CAC7B,IAAa,EACb,MAAgC;IAEhC,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,QAAQ,CACtB,GAAW,EACX,KAAmB;IAEnB,OAAO,GAAG,CAAC,SAAS,CAAC;QACnB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,OAAO,EAAE,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,UAAU,CAAC;QACpD,SAAS,EAAE,GAAG,CAAC,SAAS;KACzB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,GAAW,EACX,GAA+B,EACjB,EAAE,CAChB,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,GAAG,GAAG,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;AAEhE,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,GAA+B,EAAE,EAAE,CACnE,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { TransportResult } from "../transport";
2
+ import { type RunCtx } from "./context";
3
+ import { type ProbeEval } from "./probe-runner";
4
+ export type FixedTextEvidence = {
5
+ short: TransportResult;
6
+ long: TransportResult;
7
+ };
8
+ export type ThinkingReplyEvidence = {
9
+ res: TransportResult;
10
+ /** Only when the strict replay ran: the endpoint accepted the block back. */
11
+ replayAccepted?: boolean;
12
+ } | null;
13
+ /**
14
+ * What the rules can ask for. Each key is collected at most once per run, so
15
+ * two rules reading the same replies (token truth and the fingerprint both use
16
+ * the fixed text) cost one set of requests.
17
+ */
18
+ export type EvidenceBag = {
19
+ probes: ProbeEval[];
20
+ fixedText: FixedTextEvidence;
21
+ /** null when the wire has no count endpoint or the short reply had no usage. */
22
+ countTokens: TransportResult | null;
23
+ /** null when the model has no thinking mode to ask for. */
24
+ thinkingReply: ThinkingReplyEvidence;
25
+ /** null when the model may legitimately answer without thinking. */
26
+ floorReply: TransportResult | null;
27
+ };
28
+ export type EvidenceKey = keyof EvidenceBag;
29
+ /** Collection order when several keys are needed: probes first, then the extras. */
30
+ export declare const EVIDENCE_ORDER: readonly ["probes", "thinkingReply", "fixedText", "countTokens", "floorReply"];
31
+ export declare class EvidenceStore {
32
+ private readonly probes;
33
+ private readonly fixedText;
34
+ private readonly countTokens;
35
+ private readonly thinkingReply;
36
+ private readonly floorReply;
37
+ constructor(ctx: RunCtx);
38
+ get<K extends EvidenceKey>(key: K): Promise<EvidenceBag[K]>;
39
+ }
40
+ //# sourceMappingURL=evidence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../../src/engine/evidence.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAA2B,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAQ/D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,eAAe,CAAC;IACvB,IAAI,EAAE,eAAe,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,eAAe,CAAC;IACrB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GAAG,IAAI,CAAC;AAET;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,gFAAgF;IAChF,WAAW,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,2DAA2D;IAC3D,aAAa,EAAE,qBAAqB,CAAC;IACrC,oEAAoE;IACpE,UAAU,EAAE,eAAe,GAAG,IAAI,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,MAAM,WAAW,CAAC;AAE5C,oFAAoF;AACpF,eAAO,MAAM,cAAc,gFAMgB,CAAC;AA8E5C,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmC;IAC7D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwC;IACpE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAuC;IACrE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwC;IAEnE,YAAY,GAAG,EAAE,MAAM,EAMtB;IAED,GAAG,CAAC,CAAC,SAAS,WAAW,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;CAe7D"}
@@ -0,0 +1,108 @@
1
+ import { COUNT_PROBE_MAX_TOKENS, FLOOR_PROMPT, LONG_PROMPT, SHORT_PROMPT, SIGNATURE_PROMPT, } from "../probes/prompts";
2
+ import { callWire, chat, wireCtx } from "./context";
3
+ import { collectProbes } from "./probe-runner";
4
+ const THINKING_BUDGET_TOKENS = 2000;
5
+ /** Thinking plus answer; too small and an adaptive model skips thinking to fit. */
6
+ const SIGNATURE_MAX_TOKENS = 16000;
7
+ /** Room for the thoughts plus the word; too small truncates thinking and reads as a fake. */
8
+ const FLOOR_MAX_TOKENS = 2048;
9
+ /** Collection order when several keys are needed: probes first, then the extras. */
10
+ export const EVIDENCE_ORDER = [
11
+ "probes",
12
+ "thinkingReply",
13
+ "fixedText",
14
+ "countTokens",
15
+ "floorReply",
16
+ ];
17
+ const ok = (r) => r.status !== null && r.status < 400;
18
+ async function collectFixedText(ctx) {
19
+ const ask = (prompt) => chat(ctx, {
20
+ maxTokens: COUNT_PROBE_MAX_TOKENS,
21
+ messages: [{ role: "user", content: prompt }],
22
+ });
23
+ const [short, long] = await Promise.all([
24
+ ask(SHORT_PROMPT),
25
+ ask(LONG_PROMPT),
26
+ ]);
27
+ return { short, long };
28
+ }
29
+ async function collectCountTokens(ctx, store) {
30
+ const count = ctx.wire.ops.countTokens;
31
+ if (!count)
32
+ return null;
33
+ const { short } = await store.get("fixedText");
34
+ if (!ok(short) || ctx.wire.read.meta(short.data).usage?.prompt == null)
35
+ return null;
36
+ return callWire(ctx, count({
37
+ model: ctx.model,
38
+ messages: [{ role: "user", content: SHORT_PROMPT }],
39
+ }, wireCtx(ctx)));
40
+ }
41
+ async function collectThinkingReply(ctx) {
42
+ const kind = ctx.facts.thinking;
43
+ if (kind === "none")
44
+ return null;
45
+ const messages = [{ role: "user", content: SIGNATURE_PROMPT }];
46
+ // Non-streaming on purpose: streaming an adaptive model silently drops the
47
+ // thinking block, so a real Claude would look unsigned.
48
+ const res = await chat(ctx, {
49
+ maxTokens: SIGNATURE_MAX_TOKENS,
50
+ messages,
51
+ thinking: { kind, budgetTokens: THINKING_BUDGET_TOKENS },
52
+ });
53
+ if (!ctx.checks.signature?.strict || !ok(res))
54
+ return { res };
55
+ const found = ctx.wire.read.thinkingBlock?.(res.data);
56
+ if (!found)
57
+ return { res };
58
+ // Hand the block back. Against the vendor this re-validates the signature;
59
+ // through a relay it does not (a forged 308-byte signature returned 200 on
60
+ // both upstreams measured), so it is reported as evidence, never a verdict.
61
+ const replay = await chat(ctx, {
62
+ maxTokens: 1,
63
+ messages: [...messages, { role: "assistant", content: [found.block] }],
64
+ });
65
+ // A network failure proves nothing; only an explicit rejection counts.
66
+ return { res, replayAccepted: replay.status === null || replay.status < 400 };
67
+ }
68
+ async function collectFloorReply(ctx) {
69
+ if (!ctx.facts.alwaysThinks)
70
+ return null;
71
+ return chat(ctx, {
72
+ maxTokens: FLOOR_MAX_TOKENS,
73
+ messages: [{ role: "user", content: FLOOR_PROMPT }],
74
+ });
75
+ }
76
+ const memo = (fn) => {
77
+ let p;
78
+ return () => (p ??= fn());
79
+ };
80
+ export class EvidenceStore {
81
+ probes;
82
+ fixedText;
83
+ countTokens;
84
+ thinkingReply;
85
+ floorReply;
86
+ constructor(ctx) {
87
+ this.probes = memo(() => collectProbes(ctx));
88
+ this.fixedText = memo(() => collectFixedText(ctx));
89
+ this.countTokens = memo(() => collectCountTokens(ctx, this));
90
+ this.thinkingReply = memo(() => collectThinkingReply(ctx));
91
+ this.floorReply = memo(() => collectFloorReply(ctx));
92
+ }
93
+ get(key) {
94
+ switch (key) {
95
+ case "probes":
96
+ return this.probes();
97
+ case "fixedText":
98
+ return this.fixedText();
99
+ case "countTokens":
100
+ return this.countTokens();
101
+ case "thinkingReply":
102
+ return this.thinkingReply();
103
+ case "floorReply":
104
+ return this.floorReply();
105
+ }
106
+ }
107
+ }
108
+ //# sourceMappingURL=evidence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidence.js","sourceRoot":"","sources":["../../src/engine/evidence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { ConnectivityError } from "../types";
2
+ import type { VendorAdapter } from "../vendors/types";
3
+ import { type RunCtx } from "./context";
4
+ export type HandshakeOutcome<V extends string> = {
5
+ ok: true;
6
+ wire: VendorAdapter<V>;
7
+ status: number;
8
+ } | {
9
+ ok: false;
10
+ reason: ConnectivityError;
11
+ status: number | null;
12
+ corsBlocked: boolean;
13
+ };
14
+ /** The requested wire first, then its declared fallbacks, until one answers. */
15
+ export declare function runHandshake<V extends string>(ctx: RunCtx<V>, vendors: readonly VendorAdapter<V>[]): Promise<HandshakeOutcome<V>>;
16
+ //# sourceMappingURL=handshake.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handshake.d.ts","sourceRoot":"","sources":["../../src/engine/handshake.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAqB,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAE3D,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,MAAM,IACzC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAwEN,gFAAgF;AAChF,wBAAsB,YAAY,CAAC,CAAC,SAAS,MAAM,EACjD,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,EACd,OAAO,EAAE,SAAS,aAAa,CAAC,CAAC,CAAC,EAAE,GACnC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CA6C9B"}
@@ -1,6 +1,6 @@
1
- import { PROVIDER_CONFIGS } from "./providers/config";
2
- import { probeTransport } from "./transport";
3
- import { isTransientError } from "./signals";
1
+ import { isTransientError } from "../identity/signals";
2
+ import { vendorFor } from "../vendors/table";
3
+ import { callWire, wireCtx } from "./context";
4
4
  const HANDSHAKE_PROMPT = "hi";
5
5
  const HANDSHAKE_MAX_TOKENS = 1;
6
6
  function classifyStatus(status) {
@@ -40,23 +40,13 @@ function rejectsModel(data) {
40
40
  msg.includes("not supported") ||
41
41
  msg.includes("does not exist")));
42
42
  }
43
- async function tryFormat(args) {
44
- const cfg = PROVIDER_CONFIGS[args.provider];
45
- const built = cfg.buildRequest({
46
- baseUrl: args.baseUrl,
47
- apiKey: args.apiKey,
48
- model: args.model,
49
- prompt: HANDSHAKE_PROMPT,
43
+ async function tryWire(ctx, wire) {
44
+ const built = wire.ops.chat({
45
+ model: ctx.model,
50
46
  maxTokens: HANDSHAKE_MAX_TOKENS,
51
- direct: args.mode === "direct",
52
- });
53
- const res = await args.transport({
54
- mode: args.mode,
55
- url: built.url,
56
- headers: built.headers,
57
- reqBody: built.body,
58
- timeoutMs: args.timeoutMs,
59
- });
47
+ messages: [{ role: "user", content: HANDSHAKE_PROMPT }],
48
+ }, wireCtx(ctx));
49
+ const res = await callWire(ctx, built);
60
50
  if (res.corsBlocked)
61
51
  return { outcome: "cors", status: null, corsBlocked: true };
62
52
  if (res.status === null)
@@ -68,15 +58,21 @@ async function tryFormat(args) {
68
58
  corsBlocked: false,
69
59
  };
70
60
  }
71
- export async function runHandshake(opts) {
72
- const transport = opts.transport ?? probeTransport;
73
- const order = opts.provider === "openai" ? ["openai"] : [opts.provider, "openai"];
61
+ /** The requested wire first, then its declared fallbacks, until one answers. */
62
+ export async function runHandshake(ctx, vendors) {
63
+ const order = [
64
+ ctx.wire,
65
+ ...ctx.wire.fallbackWires.flatMap((id) => {
66
+ const w = vendorFor(vendors, id);
67
+ return w ? [w] : [];
68
+ }),
69
+ ];
74
70
  let sawAuth = false;
75
71
  let sawModelRejected = false;
76
72
  let sawTransient = false;
77
73
  let lastStatus = null;
78
- for (const provider of order) {
79
- const r = await tryFormat({ ...opts, provider, transport });
74
+ for (const wire of order) {
75
+ const r = await tryWire(ctx, wire);
80
76
  lastStatus = r.status;
81
77
  if (r.outcome === "cors")
82
78
  return {
@@ -86,12 +82,7 @@ export async function runHandshake(opts) {
86
82
  corsBlocked: true,
87
83
  };
88
84
  if (r.outcome === "ok")
89
- return {
90
- ok: true,
91
- resolvedProvider: provider,
92
- mode: opts.mode,
93
- status: r.status,
94
- };
85
+ return { ok: true, wire, status: r.status };
95
86
  if (r.outcome === "auth")
96
87
  sawAuth = true;
97
88
  if (r.outcome === "model")
@@ -99,43 +90,24 @@ export async function runHandshake(opts) {
99
90
  if (r.outcome === "transient")
100
91
  sawTransient = true;
101
92
  }
93
+ const fail = (reason) => ({
94
+ ok: false,
95
+ reason,
96
+ status: lastStatus,
97
+ corsBlocked: false,
98
+ });
102
99
  // Before the key check: a gateway that rejects the model name often answers
103
- // 403 on the second format too, which would otherwise read as a bad key.
100
+ // 403 on the second wire too, which would otherwise read as a bad key.
104
101
  if (sawModelRejected)
105
- return {
106
- ok: false,
107
- reason: "model-rejected",
108
- status: lastStatus,
109
- corsBlocked: false,
110
- };
102
+ return fail("model-rejected");
111
103
  if (sawAuth)
112
- return {
113
- ok: false,
114
- reason: "invalid-key",
115
- status: lastStatus,
116
- corsBlocked: false,
117
- };
104
+ return fail("invalid-key");
118
105
  if (lastStatus === null)
119
- return {
120
- ok: false,
121
- reason: "unreachable",
122
- status: null,
123
- corsBlocked: false,
124
- };
125
- // Every format answered 429/5xx: the endpoint is having a bad moment, which
126
- // is never evidence that it speaks no supported format.
106
+ return fail("unreachable");
107
+ // Every wire answered 429/5xx: the endpoint is having a bad moment, which is
108
+ // never evidence that it speaks no supported format.
127
109
  if (sawTransient)
128
- return {
129
- ok: false,
130
- reason: "endpoint-busy",
131
- status: lastStatus,
132
- corsBlocked: false,
133
- };
134
- return {
135
- ok: false,
136
- reason: "no-format",
137
- status: lastStatus,
138
- corsBlocked: false,
139
- };
110
+ return fail("endpoint-busy");
111
+ return fail("no-format");
140
112
  }
141
113
  //# sourceMappingURL=handshake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handshake.js","sourceRoot":"","sources":["../../src/engine/handshake.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { type ProbeSignal } from "../probes/table";
2
+ import type { ProbeOutcome } from "../types";
3
+ import { type RunCtx } from "./context";
4
+ /** A probe's outcome plus what the rules need but the result never carries. */
5
+ export type ProbeEval = ProbeOutcome & {
6
+ text: string | undefined;
7
+ raw: unknown;
8
+ corsBlocked: boolean;
9
+ };
10
+ export declare function probeReason(pass: boolean, signal: ProbeSignal, muxFailure: boolean, transient: boolean): string;
11
+ export declare function collectProbes(ctx: RunCtx): Promise<ProbeEval[]>;
12
+ /** The probe that generated the most: the reply worth reading metadata from. */
13
+ export declare function richestProbe(probes: readonly ProbeEval[]): ProbeEval | null;
14
+ //# sourceMappingURL=probe-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe-runner.d.ts","sourceRoot":"","sources":["../../src/engine/probe-runner.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAS7D,+EAA+E;AAC/E,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG;IACrC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,GAAG,EAAE,OAAO,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,wBAAgB,WAAW,CACzB,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,OAAO,EACnB,SAAS,EAAE,OAAO,GACjB,MAAM,CAUR;AAsJD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAE/D;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,GAAG,IAAI,CAM3E"}