@claudexor/budget 3.0.4 → 3.1.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.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +43 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +189 -21
- package/dist/router.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from "./ledger.js";
|
|
2
2
|
export * from "./router.js";
|
|
3
3
|
export * from "./observe.js";
|
|
4
|
-
export { loadHarnessMetrics, recordHarnessMetric
|
|
4
|
+
export { loadHarnessMetrics, recordHarnessMetric } from "./metrics.js";
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from "./ledger.js";
|
|
2
2
|
export * from "./router.js";
|
|
3
3
|
export * from "./observe.js";
|
|
4
|
-
export { loadHarnessMetrics, recordHarnessMetric
|
|
4
|
+
export { loadHarnessMetrics, recordHarnessMetric } from "./metrics.js";
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/router.d.ts
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
|
-
import type { BillingKnowledge, CredentialRoute, EffortHint, Intent, PaidFallback, QualityTierSet, RoutingGoal } from "@claudexor/schema";
|
|
1
|
+
import type { AuthVerification, BillingKnowledge, CredentialRoute, EffortHint, Intent, PaidFallback, QualityTierSet, RouteRankingRationale, RoutingGoal } from "@claudexor/schema";
|
|
2
2
|
import type { BudgetLedger } from "./ledger.js";
|
|
3
|
+
/**
|
|
4
|
+
* The typed auth-route evidence a route ran (or would run) under — the doctor's
|
|
5
|
+
* `auth_sources` verification for the credential source plus the concrete
|
|
6
|
+
* `credential_route` receipt. This is the ONLY sanctioned source of billing
|
|
7
|
+
* knowledge (INV-060 / schema `auth.ts`: a credential transport alone never
|
|
8
|
+
* proves entitlement or zero cost, but a VERIFIED native/subscription route is
|
|
9
|
+
* a subscription-entitlement fact). No per-vendor price table is inferred.
|
|
10
|
+
*/
|
|
11
|
+
export interface RouteAuthEvidence {
|
|
12
|
+
route: CredentialRoute | null;
|
|
13
|
+
verification: AuthVerification;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Billing knowledge derived STRICTLY from the typed auth route (QA-034). A
|
|
17
|
+
* verified vendor-native route proves `subscription_entitlement`; a managed
|
|
18
|
+
* API-key route is `metered` and is NEVER presumed free; every other route
|
|
19
|
+
* (unverified native, credential-free local, unknown) stays `unknown`. This is
|
|
20
|
+
* the missing production producer the schema/router already knew how to consume.
|
|
21
|
+
*/
|
|
22
|
+
export declare function billingKnowledgeForAuthRoute(evidence: RouteAuthEvidence): BillingKnowledge;
|
|
3
23
|
export interface RouterCandidate {
|
|
4
24
|
harnessId: string;
|
|
5
25
|
available: boolean;
|
|
6
26
|
model?: string;
|
|
7
27
|
effort?: EffortHint;
|
|
8
28
|
billingKnowledge?: BillingKnowledge;
|
|
29
|
+
/** Typed auth-route evidence. When present it is AUTHORITATIVE for billing
|
|
30
|
+
* knowledge (derived via billingKnowledgeForAuthRoute), so a verified native
|
|
31
|
+
* route survives paid_fallback:never and ranks with a real economy tuple.
|
|
32
|
+
* Falls back to the explicit billingKnowledge (else unknown) when absent. */
|
|
33
|
+
authRoute?: RouteAuthEvidence;
|
|
9
34
|
incrementalCostUsd?: number | null;
|
|
10
35
|
credentialRoute?: CredentialRoute;
|
|
11
36
|
/** Effective credential subject for quota filtering (release wave round-16
|
|
@@ -20,11 +45,27 @@ export interface RouteContext {
|
|
|
20
45
|
intent: Intent;
|
|
21
46
|
qualityTiers: QualityTierSet;
|
|
22
47
|
ledger: BudgetLedger;
|
|
48
|
+
/**
|
|
49
|
+
* The instant EVERY clock-dependent axis is evaluated at (cooldown expiry and
|
|
50
|
+
* quota pace slack). Optional: each entry point pins one `Date.now()` for the
|
|
51
|
+
* whole call when it is absent. A caller that ranks a pool and then explains
|
|
52
|
+
* the SAME pool must pass a pinned instant so both passes read one clock.
|
|
53
|
+
*/
|
|
54
|
+
now?: number;
|
|
23
55
|
}
|
|
24
56
|
export declare class RoutingPreflightError extends Error {
|
|
25
57
|
readonly code = "routing_preflight_refused";
|
|
26
58
|
}
|
|
27
59
|
/** Order candidates transparently; lower tuple values win. Unknown quota remains eligible. */
|
|
28
|
-
export declare function rankHarnesses(candidates: RouterCandidate[],
|
|
60
|
+
export declare function rankHarnesses(candidates: RouterCandidate[], context: RouteContext): RouterCandidate[];
|
|
29
61
|
export declare function selectHarness(candidates: RouterCandidate[], ctx: RouteContext): RouterCandidate | null;
|
|
62
|
+
/**
|
|
63
|
+
* Explain a ranking as a typed record (QA-034): the per-candidate billing/cost
|
|
64
|
+
* tuple, the eligible/dropped split, and the DECISIVE reason. Kept axis-aligned
|
|
65
|
+
* with rankHarnesses so the recorded rationale can never disagree with the
|
|
66
|
+
* order actually taken. Prose-free; a surface projects it, never reconstructs it.
|
|
67
|
+
* The shape is the schema-owned RouteRankingRationale (snake_case) so it can be
|
|
68
|
+
* persisted verbatim as RunTelemetry.routing_rationale.
|
|
69
|
+
*/
|
|
70
|
+
export declare function explainRanking(candidates: RouterCandidate[], context: RouteContext): RouteRankingRationale;
|
|
30
71
|
//# sourceMappingURL=router.d.ts.map
|
package/dist/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,MAAM,EACN,YAAY,EACZ,cAAc,
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,MAAM,EACN,YAAY,EACZ,cAAc,EAEd,qBAAqB,EAErB,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,gBAAgB,CAAC;CAChC;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,iBAAiB,GAAG,gBAAgB,CAM1F;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC;;;iFAG6E;IAC7E,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;qEAGiE;IACjE,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,YAAY,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,cAAc,CAAC;IAC7B,MAAM,EAAE,YAAY,CAAC;IACrB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,+BAA+B;CAC7C;AAkHD,8FAA8F;AAC9F,wBAAgB,aAAa,CAC3B,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,EAAE,YAAY,GACpB,eAAe,EAAE,CAwBnB;AAED,wBAAgB,aAAa,CAC3B,UAAU,EAAE,eAAe,EAAE,EAC7B,GAAG,EAAE,YAAY,GAChB,eAAe,GAAG,IAAI,CAExB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,EAAE,YAAY,GACpB,qBAAqB,CAgBvB"}
|
package/dist/router.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing knowledge derived STRICTLY from the typed auth route (QA-034). A
|
|
3
|
+
* verified vendor-native route proves `subscription_entitlement`; a managed
|
|
4
|
+
* API-key route is `metered` and is NEVER presumed free; every other route
|
|
5
|
+
* (unverified native, credential-free local, unknown) stays `unknown`. This is
|
|
6
|
+
* the missing production producer the schema/router already knew how to consume.
|
|
7
|
+
*/
|
|
8
|
+
export function billingKnowledgeForAuthRoute(evidence) {
|
|
9
|
+
if (evidence.route === "managed_api_key")
|
|
10
|
+
return "metered";
|
|
11
|
+
if (evidence.route === "vendor_native" && evidence.verification === "passed") {
|
|
12
|
+
return "subscription_entitlement";
|
|
13
|
+
}
|
|
14
|
+
return "unknown";
|
|
15
|
+
}
|
|
1
16
|
export class RoutingPreflightError extends Error {
|
|
2
17
|
code = "routing_preflight_refused";
|
|
3
18
|
}
|
|
@@ -8,12 +23,30 @@ function tierIndex(candidate, ctx) {
|
|
|
8
23
|
route.effort === candidate.effort));
|
|
9
24
|
return index < 0 ? null : index;
|
|
10
25
|
}
|
|
26
|
+
/** Effective billing knowledge: the typed auth route is authoritative when
|
|
27
|
+
* present (QA-034), else the explicitly supplied value, else unknown. */
|
|
28
|
+
function effectiveBilling(candidate) {
|
|
29
|
+
if (candidate.authRoute)
|
|
30
|
+
return billingKnowledgeForAuthRoute(candidate.authRoute);
|
|
31
|
+
return candidate.billingKnowledge ?? "unknown";
|
|
32
|
+
}
|
|
11
33
|
function isIncrementalPaid(candidate) {
|
|
12
|
-
return !["proven_zero", "subscription_entitlement"].includes(candidate
|
|
34
|
+
return !["proven_zero", "subscription_entitlement"].includes(effectiveBilling(candidate));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Pin ONE evaluation instant for a whole ranking pass. Reading the clock again
|
|
38
|
+
* per candidate makes the comparator time-varying: two candidates carrying
|
|
39
|
+
* IDENTICAL quota observations get slacks that differ by whatever milliseconds
|
|
40
|
+
* elapsed between the two reads, so a pool nothing actually separates is
|
|
41
|
+
* ordered by scheduler jitter and reports `expiring_quota_slack` instead of the
|
|
42
|
+
* honest `declared_order`. The pin is what makes equal inputs compare equal.
|
|
43
|
+
*/
|
|
44
|
+
function pinInstant(ctx) {
|
|
45
|
+
return ctx.now === undefined ? { ...ctx, now: Date.now() } : ctx;
|
|
13
46
|
}
|
|
14
47
|
function eligible(candidates, ctx) {
|
|
15
48
|
const ready = candidates.filter((candidate) => candidate.available &&
|
|
16
|
-
!ctx.ledger.cooldownActive(candidate.harnessId, candidate.credentialRoute, candidate.credentialSubjectId));
|
|
49
|
+
!ctx.ledger.cooldownActive(candidate.harnessId, candidate.credentialRoute, candidate.credentialSubjectId, ctx.now));
|
|
17
50
|
const free = ready.filter((candidate) => !isIncrementalPaid(candidate));
|
|
18
51
|
if (ctx.paidFallback === "never")
|
|
19
52
|
return free;
|
|
@@ -21,36 +54,171 @@ function eligible(candidates, ctx) {
|
|
|
21
54
|
return free;
|
|
22
55
|
return ready;
|
|
23
56
|
}
|
|
57
|
+
/** User-declared tier index the comparators order by (missing => MAX). Shared
|
|
58
|
+
* by the sort and the traced comparator so a "tier" claim is never re-derived. */
|
|
59
|
+
function tierValueOf(candidate, ctx) {
|
|
60
|
+
return tierIndex(candidate, ctx) ?? Number.MAX_SAFE_INTEGER;
|
|
61
|
+
}
|
|
62
|
+
/** Binding pace slack the `auto` comparator orders by; null = none observed.
|
|
63
|
+
* Evaluated at the pass-wide pinned instant (`ctx.now`) — never at a freshly
|
|
64
|
+
* read clock, which would make two identical quota states differ. */
|
|
65
|
+
function autoSlack(candidate, ctx) {
|
|
66
|
+
return ctx.ledger.bindingPaceSlack(candidate.harnessId, candidate.credentialRoute, candidate.credentialSubjectId, ctx.now);
|
|
67
|
+
}
|
|
68
|
+
/** null slack collapses to this so a slack-carrying candidate always outranks a
|
|
69
|
+
* slack-less one, exactly as the comparator subtraction does. */
|
|
70
|
+
const AUTO_SLACK_ABSENT = Number.NEGATIVE_INFINITY;
|
|
71
|
+
/**
|
|
72
|
+
* The SINGLE traced comparator for `auto` (round-4 #3 — this rationale has been
|
|
73
|
+
* wrong three times). It returns both the pair ORDER and the AXIS that produced
|
|
74
|
+
* it, and is the only place the auto ordering is defined. `rankHarnesses` sorts
|
|
75
|
+
* by `.order`; `rankingReason` reads `.axis`. Deriving the recorded reason from
|
|
76
|
+
* the very comparator that ordered the pool makes it structurally impossible for
|
|
77
|
+
* the rationale to claim a factor the sort did not use (the axis-registry
|
|
78
|
+
* pattern used in arbitration, specialized to auto's conditional slack axis).
|
|
79
|
+
*
|
|
80
|
+
* The slack axis is in force whenever EITHER candidate has a non-null slack; the
|
|
81
|
+
* subtraction of the (null => -Infinity) effective values is what decides — so
|
|
82
|
+
* two EQUAL non-null slacks yield order 0 and a null axis (declared order, and
|
|
83
|
+
* the sort does NOT fall through to tiers). Only when BOTH slacks are absent
|
|
84
|
+
* does the comparator compare tier indices.
|
|
85
|
+
*
|
|
86
|
+
* Both slacks are read at the pass-wide pinned instant (`ctx.now`), so the
|
|
87
|
+
* subtraction compares two quota states and nothing else. Re-reading the clock
|
|
88
|
+
* per candidate made the comparator time-varying and broke exactly the
|
|
89
|
+
* guarantee above: equal quota states produced a non-zero order.
|
|
90
|
+
*/
|
|
91
|
+
function compareAutoTraced(a, b, ctx) {
|
|
92
|
+
const aSlack = autoSlack(a, ctx);
|
|
93
|
+
const bSlack = autoSlack(b, ctx);
|
|
94
|
+
if (aSlack !== null || bSlack !== null) {
|
|
95
|
+
const order = (bSlack ?? AUTO_SLACK_ABSENT) - (aSlack ?? AUTO_SLACK_ABSENT);
|
|
96
|
+
return { order, axis: order !== 0 ? "expiring_quota_slack" : null };
|
|
97
|
+
}
|
|
98
|
+
const order = tierValueOf(a, ctx) - tierValueOf(b, ctx);
|
|
99
|
+
return { order, axis: order !== 0 ? "quality_tier" : null };
|
|
100
|
+
}
|
|
24
101
|
/** Order candidates transparently; lower tuple values win. Unknown quota remains eligible. */
|
|
25
|
-
export function rankHarnesses(candidates,
|
|
102
|
+
export function rankHarnesses(candidates, context) {
|
|
103
|
+
const ctx = pinInstant(context);
|
|
26
104
|
const routes = eligible(candidates, ctx);
|
|
27
105
|
if (ctx.goal === "quality" && routes.every((candidate) => tierIndex(candidate, ctx) === null)) {
|
|
28
106
|
throw new RoutingPreflightError(`quality routing requires a comparable user-declared tier for intent '${ctx.intent}'`);
|
|
29
107
|
}
|
|
108
|
+
if (ctx.goal === "auto") {
|
|
109
|
+
return routes.toSorted((a, b) => compareAutoTraced(a, b, ctx).order);
|
|
110
|
+
}
|
|
30
111
|
return routes.toSorted((a, b) => {
|
|
31
|
-
const aTier =
|
|
32
|
-
const bTier =
|
|
112
|
+
const aTier = tierValueOf(a, ctx);
|
|
113
|
+
const bTier = tierValueOf(b, ctx);
|
|
33
114
|
if (ctx.goal === "quality")
|
|
34
115
|
return aTier - bTier;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
const aSlack = ctx.ledger.bindingPaceSlack(a.harnessId, a.credentialRoute, a.credentialSubjectId);
|
|
46
|
-
const bSlack = ctx.ledger.bindingPaceSlack(b.harnessId, b.credentialRoute, b.credentialSubjectId);
|
|
47
|
-
if (aSlack !== null || bSlack !== null) {
|
|
48
|
-
return (bSlack ?? Number.NEGATIVE_INFINITY) - (aSlack ?? Number.NEGATIVE_INFINITY);
|
|
49
|
-
}
|
|
50
|
-
return aTier - bTier;
|
|
116
|
+
// economy
|
|
117
|
+
const aPaid = isIncrementalPaid(a) ? 1 : 0;
|
|
118
|
+
const bPaid = isIncrementalPaid(b) ? 1 : 0;
|
|
119
|
+
const paidOrder = aPaid - bPaid;
|
|
120
|
+
if (paidOrder !== 0)
|
|
121
|
+
return paidOrder;
|
|
122
|
+
const aCost = a.incrementalCostUsd ?? Number.POSITIVE_INFINITY;
|
|
123
|
+
const bCost = b.incrementalCostUsd ?? Number.POSITIVE_INFINITY;
|
|
124
|
+
return aCost - bCost || aTier - bTier;
|
|
51
125
|
});
|
|
52
126
|
}
|
|
53
127
|
export function selectHarness(candidates, ctx) {
|
|
54
128
|
return rankHarnesses(candidates, ctx)[0] ?? null;
|
|
55
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Explain a ranking as a typed record (QA-034): the per-candidate billing/cost
|
|
132
|
+
* tuple, the eligible/dropped split, and the DECISIVE reason. Kept axis-aligned
|
|
133
|
+
* with rankHarnesses so the recorded rationale can never disagree with the
|
|
134
|
+
* order actually taken. Prose-free; a surface projects it, never reconstructs it.
|
|
135
|
+
* The shape is the schema-owned RouteRankingRationale (snake_case) so it can be
|
|
136
|
+
* persisted verbatim as RunTelemetry.routing_rationale.
|
|
137
|
+
*/
|
|
138
|
+
export function explainRanking(candidates, context) {
|
|
139
|
+
// ONE instant for both passes: the sort and the reason walk must read the same
|
|
140
|
+
// clock, or the recorded rationale can disagree with the order it explains.
|
|
141
|
+
const ctx = pinInstant(context);
|
|
142
|
+
const rankedCandidates = rankHarnesses(candidates, ctx);
|
|
143
|
+
const order = rankedCandidates.map((c) => c.harnessId);
|
|
144
|
+
const eligibleIds = new Set(order);
|
|
145
|
+
const entries = candidates.map((c) => ({
|
|
146
|
+
harness_id: c.harnessId,
|
|
147
|
+
billing_knowledge: effectiveBilling(c),
|
|
148
|
+
incremental_cost_usd: c.incrementalCostUsd ?? null,
|
|
149
|
+
eligible: eligibleIds.has(c.harnessId),
|
|
150
|
+
}));
|
|
151
|
+
const dropped = candidates.filter((c) => !eligibleIds.has(c.harnessId)).map((c) => c.harnessId);
|
|
152
|
+
const reason = rankingReason(ctx, rankedCandidates, candidates);
|
|
153
|
+
return { goal: ctx.goal, paid_fallback: ctx.paidFallback, order, dropped, reason, entries };
|
|
154
|
+
}
|
|
155
|
+
/** Do the ranked candidates carry two distinct user-declared tier indices? Only
|
|
156
|
+
* then did the tier comparator (the auto/economy tie-break) actually decide the
|
|
157
|
+
* order; a single candidate or a uniform tier index leaves the declared order. */
|
|
158
|
+
function tiersDistinguish(ranked, ctx) {
|
|
159
|
+
const indices = ranked.map((c) => tierIndex(c, ctx) ?? Number.MAX_SAFE_INTEGER);
|
|
160
|
+
return indices.some((value) => value !== indices[0]);
|
|
161
|
+
}
|
|
162
|
+
/** Do the ranked candidates carry two distinct incremental costs (with the SAME
|
|
163
|
+
* missing-cost fallback the economy sort uses)? Only then did the cost
|
|
164
|
+
* comparator — not the paid/free split above it or the tier tie-break below —
|
|
165
|
+
* actually decide the order. */
|
|
166
|
+
function costsDistinguish(ranked) {
|
|
167
|
+
const costs = ranked.map((c) => c.incrementalCostUsd ?? Number.POSITIVE_INFINITY);
|
|
168
|
+
return costs.some((value) => value !== costs[0]);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The DECISIVE comparator axis. For `auto` the reason is read directly off the
|
|
172
|
+
* SAME traced comparator that ordered the pool (`compareAutoTraced`) — never
|
|
173
|
+
* re-derived from a separately-recomputed condition (round-4 #3, the third
|
|
174
|
+
* recurrence of a rationale that disagreed with `rankHarnesses`): the reason is
|
|
175
|
+
* whichever axis actually separated an adjacent ranked pair, and `declared_order`
|
|
176
|
+
* when the comparator broke no pair (e.g. equal non-null slacks — which do NOT
|
|
177
|
+
* fall through to tiers).
|
|
178
|
+
*/
|
|
179
|
+
function rankingReason(ctx, ranked, candidates) {
|
|
180
|
+
if (ctx.goal === "quality")
|
|
181
|
+
return "quality_tier";
|
|
182
|
+
if (ctx.goal === "auto") {
|
|
183
|
+
// Walk the ranked order through the very comparator that produced it. Since
|
|
184
|
+
// the pool is sorted, any two distinct axis values leave a differing
|
|
185
|
+
// adjacent pair, so this sees every axis the sort actually used.
|
|
186
|
+
const axes = new Set();
|
|
187
|
+
for (let i = 0; i + 1 < ranked.length; i++) {
|
|
188
|
+
const traced = compareAutoTraced(ranked[i], ranked[i + 1], ctx);
|
|
189
|
+
if (traced.axis)
|
|
190
|
+
axes.add(traced.axis);
|
|
191
|
+
}
|
|
192
|
+
if (axes.has("expiring_quota_slack"))
|
|
193
|
+
return "expiring_quota_slack";
|
|
194
|
+
if (axes.has("quality_tier"))
|
|
195
|
+
return "quality_tier";
|
|
196
|
+
return "declared_order";
|
|
197
|
+
}
|
|
198
|
+
// economy: mirror the economy sort tuple EXACTLY — (isIncrementalPaid,
|
|
199
|
+
// incrementalCostUsd, tierIndex) — and report the FIRST axis that actually
|
|
200
|
+
// SEPARATED the pool. Reporting entitlement whenever ANY route is entitled was
|
|
201
|
+
// wrong when they are ALL entitled and cost/tier did the deciding (the same
|
|
202
|
+
// correction made for `auto` in 1d6a2a50).
|
|
203
|
+
const someEntitled = ranked.some((c) => !isIncrementalPaid(c));
|
|
204
|
+
// Entitlement decided when the pool actually WEIGHED an entitled route against
|
|
205
|
+
// a paid one — whether the paid route was ranked BELOW (sort axis 1) or DROPPED
|
|
206
|
+
// by paid_fallback (both are the entitled-preferred-over-paid outcome). When
|
|
207
|
+
// every route CONSIDERED is entitled, entitlement did not decide — cost/tier
|
|
208
|
+
// did (the round-3 correction; matches the pool-wide `auto` mirror in
|
|
209
|
+
// 1d6a2a50). The full candidate set (not just the eligible pool) is consulted
|
|
210
|
+
// so a paid route dropped by paid_fallback:never still attributes the outcome.
|
|
211
|
+
const anyPaidConsidered = candidates.some((c) => isIncrementalPaid(c));
|
|
212
|
+
if (someEntitled && anyPaidConsidered)
|
|
213
|
+
return "subscription_entitlement_first";
|
|
214
|
+
if (costsDistinguish(ranked))
|
|
215
|
+
return "lowest_incremental_cash";
|
|
216
|
+
if (tiersDistinguish(ranked, ctx))
|
|
217
|
+
return "quality_tier";
|
|
218
|
+
// Nothing separated the pool: distinguish "we could not compare cash at all"
|
|
219
|
+
// from a genuine declared-order tie.
|
|
220
|
+
if (ranked.length > 0 && ranked.some((c) => c.incrementalCostUsd == null))
|
|
221
|
+
return "all_incremental_cash_unknown";
|
|
222
|
+
return "declared_order";
|
|
223
|
+
}
|
|
56
224
|
//# sourceMappingURL=router.js.map
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claudexor/budget",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Budget governor: pre-call leases, circuit breaker, evidence-based routing goals, and multi-window quota pacing.",
|
|
6
6
|
"type": "module",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@claudexor/schema": "3.
|
|
20
|
-
"@claudexor/util": "3.
|
|
19
|
+
"@claudexor/schema": "3.1.1",
|
|
20
|
+
"@claudexor/util": "3.1.1"
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|