@metrxbot/sdk 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Confidence gate for the SDK cascade (spec §5.3): G1 structural validators
3
+ * (zero-cost floor) + G4 cheap checker call (the primary signal).
4
+ *
5
+ * Gate contract: any error or timeout inside the gate means ESCALATE — the
6
+ * gate fails toward quality, never toward serving an unchecked answer (F5).
7
+ */
8
+ /** G1 — structural validators. Cheap, high-precision, low-recall. */
9
+ export function runValidators(output, config) {
10
+ if (!config)
11
+ return { pass: true };
12
+ if (config.min_length != null && output.length < config.min_length) {
13
+ return { pass: false, reason: `g1:min_length(${output.length}<${config.min_length})` };
14
+ }
15
+ if (config.max_length != null && output.length > config.max_length) {
16
+ return { pass: false, reason: `g1:max_length(${output.length}>${config.max_length})` };
17
+ }
18
+ if (config.regex != null) {
19
+ try {
20
+ if (!new RegExp(config.regex).test(output)) {
21
+ return { pass: false, reason: 'g1:regex' };
22
+ }
23
+ }
24
+ catch {
25
+ // A malformed regex in the policy must not throw in the request path
26
+ // (F3-adjacent) — treat as validator failure → escalate.
27
+ return { pass: false, reason: 'g1:regex_invalid' };
28
+ }
29
+ }
30
+ if (config.require_json) {
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(output);
34
+ }
35
+ catch {
36
+ return { pass: false, reason: 'g1:json_parse' };
37
+ }
38
+ if (config.required_fields?.length) {
39
+ if (parsed === null || typeof parsed !== 'object') {
40
+ return { pass: false, reason: 'g1:json_not_object' };
41
+ }
42
+ for (const field of config.required_fields) {
43
+ if (!(field in parsed)) {
44
+ return { pass: false, reason: `g1:missing_field(${field})` };
45
+ }
46
+ }
47
+ }
48
+ }
49
+ return { pass: true };
50
+ }
51
+ const CHECKER_INSTRUCTIONS = 'You are a strict quality checker. Given a task input and a candidate answer, ' +
52
+ 'judge whether the answer adequately and correctly addresses the task. ' +
53
+ 'Respond with ONLY a JSON object: {"score": <number between 0 and 1>} — ' +
54
+ 'no prose, no code fences.';
55
+ /**
56
+ * G4 — cheap checker call via the customer-supplied checkerCaller.
57
+ * Throws on any failure (timeout, unparsable verdict) — the caller escalates.
58
+ */
59
+ export async function runChecker(opts) {
60
+ const task = {
61
+ instructions: CHECKER_INSTRUCTIONS,
62
+ rubric: opts.rubric ?? null,
63
+ task_input: safeStringify(opts.input),
64
+ candidate_answer: opts.output,
65
+ };
66
+ const result = await withTimeout(opts.checkerCaller(opts.checkerModel, task, opts.ctx), opts.timeoutMs, 'checker');
67
+ const score = parseScore(result.output);
68
+ if (score === null) {
69
+ throw new Error('checker returned an unparsable verdict');
70
+ }
71
+ return score;
72
+ }
73
+ /** Extract {"score": x} from checker output, tolerating stray text. */
74
+ export function parseScore(text) {
75
+ try {
76
+ const direct = JSON.parse(text);
77
+ if (typeof direct?.score === 'number')
78
+ return clamp01(direct.score);
79
+ }
80
+ catch {
81
+ // fall through to fragment scan
82
+ }
83
+ const match = text.match(/"score"\s*:\s*(-?\d+(?:\.\d+)?)/);
84
+ if (match)
85
+ return clamp01(Number(match[1]));
86
+ return null;
87
+ }
88
+ function clamp01(n) {
89
+ if (isNaN(n))
90
+ return null;
91
+ return Math.max(0, Math.min(1, n));
92
+ }
93
+ function safeStringify(value) {
94
+ if (typeof value === 'string')
95
+ return value;
96
+ try {
97
+ return JSON.stringify(value);
98
+ }
99
+ catch {
100
+ return String(value);
101
+ }
102
+ }
103
+ export async function withTimeout(promise, ms, label) {
104
+ let timer;
105
+ try {
106
+ return await Promise.race([
107
+ promise,
108
+ new Promise((_, reject) => {
109
+ timer = setTimeout(() => reject(new Error(`${label} timeout after ${ms}ms`)), ms);
110
+ }),
111
+ ]);
112
+ }
113
+ finally {
114
+ if (timer)
115
+ clearTimeout(timer);
116
+ }
117
+ }
118
+ //# sourceMappingURL=routing-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-gate.js","sourceRoot":"","sources":["../src/routing-gate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,qEAAqE;AACrE,MAAM,UAAU,aAAa,CAC3B,MAAc,EACd,MAAmC;IAEnC,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAEnC,IAAI,MAAM,CAAC,UAAU,IAAI,IAAI,IAAI,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,UAAU,GAAG,EAAE,CAAC;IACzF,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,IAAI,IAAI,IAAI,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,UAAU,GAAG,EAAE,CAAC;IACzF,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3C,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;YAC7C,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;YACrE,yDAAyD;YACzD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;QACrD,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC9B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;QAClD,CAAC;QACD,IAAI,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,CAAC;YACnC,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;gBAClD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;YACvD,CAAC;YACD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;gBAC3C,IAAI,CAAC,CAAC,KAAK,IAAK,MAAkC,CAAC,EAAE,CAAC;oBACpD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,KAAK,GAAG,EAAE,CAAC;gBAC/D,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACxB,CAAC;AAED,MAAM,oBAAoB,GACxB,+EAA+E;IAC/E,wEAAwE;IACxE,yEAAyE;IACzE,2BAA2B,CAAC;AAE9B;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAShC;IACC,MAAM,IAAI,GAAG;QACX,YAAY,EAAE,oBAAoB;QAClC,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI;QAC3B,UAAU,EAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;QACrC,gBAAgB,EAAE,IAAI,CAAC,MAAM;KAC9B,CAAC;IAEF,MAAM,MAAM,GAAG,MAAM,WAAW,CAC9B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,EACrD,IAAI,CAAC,SAAS,EACd,SAAS,CACV,CAAC;IAEF,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,OAAO,MAAM,EAAE,KAAK,KAAK,QAAQ;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACP,gCAAgC;IAClC,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;IAC5D,IAAI,KAAK;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,OAAO,CAAC,CAAS;IACxB,IAAI,KAAK,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAI,OAAmB,EAAE,EAAU,EAAE,KAAa;IACjF,IAAI,KAAgD,CAAC;IACrD,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC;YACxB,OAAO;YACP,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;gBAC/B,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,KAAK,kBAAkB,EAAE,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACpF,CAAC,CAAC;SACH,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;AACH,CAAC"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Policy cache for the SDK cascade router (spec §5.6).
3
+ *
4
+ * - One fetch caches the whole org's policy set (GET /api/v1/routing/policies).
5
+ * - TTL 5 min (matches the server's max-age) with ETag revalidation.
6
+ * - Stale tolerance ≤ 60 min: a kill-switch flip must propagate; past the
7
+ * bound the cache reports "unknown" and every caller falls back to
8
+ * pass-through (seam F2).
9
+ * - Lazy + non-blocking: the first call triggers a background load and
10
+ * returns "unknown" — cold start is pass-through, never a blocked request
11
+ * (seam F1).
12
+ *
13
+ * NEVER throws. All fetch failures degrade to "unknown" → pass-through.
14
+ */
15
+ export interface RoutingPolicy {
16
+ id: string;
17
+ agent_key: string;
18
+ enabled: boolean;
19
+ denied: boolean;
20
+ mode: 'pass_through' | 'shadow_sample' | 'cascade';
21
+ pool: string;
22
+ candidate_model: string | null;
23
+ prod_model: string | null;
24
+ confidence_threshold: number | null;
25
+ gate_config: Record<string, unknown>;
26
+ sample_rate: number;
27
+ holdout_pct: number;
28
+ max_extra_latency_ms: number;
29
+ min_sdk_version: string | null;
30
+ policy_version: number;
31
+ experiment_id: string | null;
32
+ }
33
+ export interface PolicyFetchResult {
34
+ policies: RoutingPolicy[];
35
+ /** Org-level monotonic version (informational). */
36
+ version: number;
37
+ /** The server's ETag header, echoed back OPAQUELY on the next fetch — the
38
+ * SDK cannot reconstruct it (it embeds the org id, review H1). */
39
+ etag?: string | null;
40
+ /** True when the server answered 304 (cache still valid). */
41
+ notModified?: boolean;
42
+ }
43
+ /** Injectable fetcher — the router wires this to the Metrx API; tests stub it.
44
+ * `etag` is the OPAQUE value from the previous response's ETag header. */
45
+ export type PolicyFetcher = (etag: string | null) => Promise<PolicyFetchResult>;
46
+ export declare class PolicyCache {
47
+ private byKey;
48
+ private etag;
49
+ private loaded;
50
+ private loadedAt;
51
+ private loading;
52
+ private readonly fetcher;
53
+ private readonly ttlMs;
54
+ private readonly staleMaxMs;
55
+ private readonly now;
56
+ constructor(opts: {
57
+ fetcher: PolicyFetcher;
58
+ ttlMs?: number;
59
+ staleMaxMs?: number;
60
+ now?: () => number;
61
+ });
62
+ /**
63
+ * Non-blocking lookup.
64
+ * - `undefined` → policy state UNKNOWN (never loaded, or stale past the
65
+ * bound) — caller must pass through.
66
+ * - `null` → the org's policies ARE loaded and this agent has no row —
67
+ * pass-through by design (no row = not allow-listed).
68
+ * - a policy row otherwise.
69
+ */
70
+ get(agentKey: string): RoutingPolicy | null | undefined;
71
+ /** Await any in-flight load — for tests and graceful shutdown. */
72
+ settle(): Promise<void>;
73
+ private refresh;
74
+ }
75
+ /** Normalization rule shared with policy writes (spec §5.1 M8). */
76
+ export declare function normalizeAgentKey(key: string): string;
77
+ //# sourceMappingURL=routing-policy-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-policy-cache.d.ts","sourceRoot":"","sources":["../src/routing-policy-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,cAAc,GAAG,eAAe,GAAG,SAAS,CAAC;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB;sEACkE;IAClE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;0EAC0E;AAC1E,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAKhF,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,IAAI,EAAE;QAChB,OAAO,EAAE,aAAa,CAAC;QACvB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;KACpB;IAOD;;;;;;;OAOG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,GAAG,SAAS;IAuBvD,kEAAkE;IAC5D,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B,OAAO,CAAC,OAAO;CA0BhB;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Policy cache for the SDK cascade router (spec §5.6).
3
+ *
4
+ * - One fetch caches the whole org's policy set (GET /api/v1/routing/policies).
5
+ * - TTL 5 min (matches the server's max-age) with ETag revalidation.
6
+ * - Stale tolerance ≤ 60 min: a kill-switch flip must propagate; past the
7
+ * bound the cache reports "unknown" and every caller falls back to
8
+ * pass-through (seam F2).
9
+ * - Lazy + non-blocking: the first call triggers a background load and
10
+ * returns "unknown" — cold start is pass-through, never a blocked request
11
+ * (seam F1).
12
+ *
13
+ * NEVER throws. All fetch failures degrade to "unknown" → pass-through.
14
+ */
15
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
16
+ const DEFAULT_STALE_MAX_MS = 60 * 60 * 1000;
17
+ export class PolicyCache {
18
+ byKey = new Map();
19
+ etag = null;
20
+ loaded = false;
21
+ loadedAt = 0;
22
+ loading = null;
23
+ fetcher;
24
+ ttlMs;
25
+ staleMaxMs;
26
+ now;
27
+ constructor(opts) {
28
+ this.fetcher = opts.fetcher;
29
+ this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
30
+ this.staleMaxMs = opts.staleMaxMs ?? DEFAULT_STALE_MAX_MS;
31
+ this.now = opts.now ?? Date.now;
32
+ }
33
+ /**
34
+ * Non-blocking lookup.
35
+ * - `undefined` → policy state UNKNOWN (never loaded, or stale past the
36
+ * bound) — caller must pass through.
37
+ * - `null` → the org's policies ARE loaded and this agent has no row —
38
+ * pass-through by design (no row = not allow-listed).
39
+ * - a policy row otherwise.
40
+ */
41
+ get(agentKey) {
42
+ if (!this.loaded) {
43
+ // Cold: kick off the first load, serve pass-through now (F1).
44
+ this.refresh();
45
+ return undefined;
46
+ }
47
+ const age = this.now() - this.loadedAt;
48
+ if (age > this.staleMaxMs) {
49
+ // Too stale to trust — a kill-switch flip may not have propagated (F2).
50
+ this.refresh();
51
+ return undefined;
52
+ }
53
+ if (age > this.ttlMs) {
54
+ // Stale-but-tolerable: serve, refresh in the background.
55
+ this.refresh();
56
+ }
57
+ return this.byKey.get(normalizeAgentKey(agentKey)) ?? null;
58
+ }
59
+ /** Await any in-flight load — for tests and graceful shutdown. */
60
+ async settle() {
61
+ if (this.loading)
62
+ await this.loading.catch(() => undefined);
63
+ }
64
+ refresh() {
65
+ if (this.loading)
66
+ return; // single-flight
67
+ this.loading = this.fetcher(this.etag)
68
+ .then((result) => {
69
+ if (result.notModified) {
70
+ this.loadedAt = this.now();
71
+ return;
72
+ }
73
+ const next = new Map();
74
+ for (const p of result.policies) {
75
+ if (p && typeof p.agent_key === 'string') {
76
+ next.set(normalizeAgentKey(p.agent_key), p);
77
+ }
78
+ }
79
+ this.byKey = next;
80
+ this.etag = result.etag ?? null;
81
+ this.loaded = true;
82
+ this.loadedAt = this.now();
83
+ })
84
+ .catch(() => {
85
+ // F1: fetch failure never surfaces; state stays as-is (or unknown).
86
+ })
87
+ .finally(() => {
88
+ this.loading = null;
89
+ });
90
+ }
91
+ }
92
+ /** Normalization rule shared with policy writes (spec §5.1 M8). */
93
+ export function normalizeAgentKey(key) {
94
+ return key.trim().toLowerCase();
95
+ }
96
+ //# sourceMappingURL=routing-policy-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-policy-cache.js","sourceRoot":"","sources":["../src/routing-policy-cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * SDK cascade routing module — `runWithPolicy()` (spec
3
+ * docs/roi-engine-cascade-sdk-phase1-spec.md §5, Phase 1 S2).
4
+ *
5
+ * Bring-your-own-caller: the customer supplies the function that calls their
6
+ * provider with THEIR keys; this module supplies policy consultation, the
7
+ * confidence gate, escalation, holdout randomization, and evidence emission.
8
+ *
9
+ * THE INVARIANT (throw-parity, §5.7): no code path in runWithPolicy throws an
10
+ * error that the customer's own `callModel(prodModel)` would not have thrown.
11
+ * Policy fetch, gate, candidate calls, and every emit fail OPEN; only the
12
+ * production-model call's own errors propagate.
13
+ *
14
+ * Streaming is OUT OF SCOPE for v1 (§5.1 H7): this contract handles complete
15
+ * responses only — keep streaming call sites on their original path.
16
+ */
17
+ import type { MetrxbotClient } from './client';
18
+ /** Bump with package.json (atomic-version rule). Server can force pass-through
19
+ * for known-bad versions via routing_policies.min_sdk_version (F10). */
20
+ export declare const SDK_VERSION = "0.2.1";
21
+ export interface ModelCallResult {
22
+ output: string;
23
+ costMicrocents?: number;
24
+ /** Server-side cost fallback via model_registry pricing when cost is absent. */
25
+ usage?: {
26
+ inputTokens: number;
27
+ outputTokens: number;
28
+ };
29
+ raw?: unknown;
30
+ }
31
+ /**
32
+ * Call-context handed to the customer's `ModelCaller`/`checkerCaller` on every
33
+ * provider call (v0.2.1, additive). Lets caller-side instrumentation (a) stamp
34
+ * `requestId`/`experimentId`/`arm` on the provider call so cascade cost SUMs
35
+ * join across `llm_events ↔ optimization_decisions ↔ quality`, and (b) detect
36
+ * the background shadow candidate (`phase === 'shadow_candidate'`) and suppress
37
+ * it from a cost dashboard.
38
+ *
39
+ * `phase` marks which leg of routing is making the call:
40
+ * - `primary` served production call (pass-through, shadow served,
41
+ * cascade control arm)
42
+ * - `candidate` cascade treatment candidate (the cheaper model)
43
+ * - `escalation` cascade treatment fell back to the production model
44
+ * - `shadow_candidate` background dual-call in shadow_sample (never served)
45
+ * - `checker` any `checkerCaller` invocation (gate + post-response judging)
46
+ */
47
+ export interface ModelCallContext {
48
+ requestId: string;
49
+ experimentId?: string;
50
+ arm?: 'control' | 'treatment';
51
+ phase: 'primary' | 'candidate' | 'escalation' | 'shadow_candidate' | 'checker';
52
+ }
53
+ export interface ModelCaller {
54
+ /**
55
+ * `model === undefined` ⇒ "call your own default" — the day-0 pass-through
56
+ * contract (§5.1 M2). The input is passed through verbatim and is never
57
+ * serialized to Metrx.
58
+ *
59
+ * `ctx` (v0.2.1) is an OPTIONAL third argument — existing two-arg callers
60
+ * keep working unchanged. It is purely informational: it never changes what
61
+ * the router serves and passing it introduces no new throw path (§5.7).
62
+ */
63
+ (model: string | undefined, input: unknown, ctx?: ModelCallContext): Promise<ModelCallResult>;
64
+ }
65
+ export interface ClientJudge {
66
+ (args: {
67
+ input: unknown;
68
+ output: string;
69
+ model: string;
70
+ }): Promise<{
71
+ score: number;
72
+ judgeModel: string;
73
+ }>;
74
+ }
75
+ export interface RunWithPolicyOptions {
76
+ agentKey: string;
77
+ /** The customer's own provider call — their client, their keys. */
78
+ callModel: ModelCaller;
79
+ /**
80
+ * Caller for the cross-family checker model. REQUIRED for shadow/cascade
81
+ * when the checker family is one the primary caller can't reach (e.g. an
82
+ * all-Anthropic customer with a non-Anthropic checker — §5.3 C3).
83
+ */
84
+ checkerCaller?: ModelCaller;
85
+ input: unknown;
86
+ /** Optional custom scorer; default = the G4 checker via checkerCaller. */
87
+ judge?: ClientJudge;
88
+ requestId?: string;
89
+ }
90
+ export interface RunWithPolicyResult {
91
+ output: string;
92
+ /** undefined on no-policy pass-through (the customer's own default ran). */
93
+ servedModel: string | undefined;
94
+ escalated: boolean;
95
+ arm?: 'control' | 'treatment';
96
+ /** Cascade experiment id — attach to telemetry events (with the arm) so the
97
+ * contamination guard can exclude routed calls from baselines. */
98
+ experimentId?: string;
99
+ requestId: string;
100
+ /** What the policy said: pass_through | shadow_sample | cascade | disabled. */
101
+ mode: string;
102
+ }
103
+ export interface RouterConfig {
104
+ apiKey: string;
105
+ apiUrl: string;
106
+ /** Injectable for tests. */
107
+ fetchImpl?: typeof fetch;
108
+ now?: () => number;
109
+ random?: () => number;
110
+ /** Hard local kill switch (F10); also honors METRX_ROUTING_DISABLED=true. */
111
+ disabled?: boolean;
112
+ policyTtlMs?: number;
113
+ policyStaleMaxMs?: number;
114
+ }
115
+ export declare class MetrxRouter {
116
+ private readonly cfg;
117
+ private readonly cache;
118
+ private readonly now;
119
+ private readonly random;
120
+ private readonly fetchImpl;
121
+ private readonly breakers;
122
+ private readonly background;
123
+ constructor(cfg: RouterConfig);
124
+ /** Await all in-flight background work (emits, shadow calls, judging).
125
+ * For tests and graceful shutdown — never required for correctness. */
126
+ flush(): Promise<void>;
127
+ run(opts: RunWithPolicyOptions): Promise<RunWithPolicyResult>;
128
+ private passThrough;
129
+ private runShadowSample;
130
+ private runCascade;
131
+ private judgeOutput;
132
+ private emitDecision;
133
+ private emitPair;
134
+ private emitQuality;
135
+ private fireAndForget;
136
+ /** F8/F9: background work never throws into the request path. */
137
+ private spawnBackground;
138
+ private breakerFor;
139
+ private breakerOpen;
140
+ private recordCandidateError;
141
+ private recordEscalation;
142
+ private makePolicyFetcher;
143
+ }
144
+ export declare function routerFor(client: MetrxbotClient, overrides?: Partial<RouterConfig>): MetrxRouter;
145
+ /**
146
+ * Run one request under the agent's routing policy. Day 0 (no policy rows):
147
+ * behaves exactly like `opts.callModel(undefined, opts.input)`.
148
+ */
149
+ export declare function runWithPolicy(client: MetrxbotClient, opts: RunWithPolicyOptions): Promise<RunWithPolicyResult>;
150
+ /**
151
+ * Loose semver-ish comparison. Returns true when a < b, false when a >= b,
152
+ * and `undefined` when either side is unparseable — the caller treats
153
+ * "unknown" as pass-through (fail toward doing nothing).
154
+ */
155
+ export declare function semverLt(a: string, b: string): boolean | undefined;
156
+ //# sourceMappingURL=routing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../src/routing.ts"],"names":[],"mappings":"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"}