@tokenbuddy/tokenbuddy 1.0.40 → 1.0.41
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/src/buyer-store.d.ts +22 -0
- package/dist/src/buyer-store.js +46 -7
- package/dist/src/daemon.d.ts +16 -0
- package/dist/src/daemon.js +421 -17
- package/dist/src/provider-install.d.ts +3 -5
- package/dist/src/provider-install.js +33 -425
- package/dist/src/route-failover.js +10 -0
- package/dist/src/seller-pool.d.ts +5 -5
- package/dist/src/seller-pool.js +5 -7
- package/dist/src/seller-route-recommendations.d.ts +110 -0
- package/dist/src/seller-route-recommendations.js +334 -0
- package/package.json +2 -2
- package/static/ui/assets/index-Ca_IcEY6.js +271 -0
- package/static/ui/assets/index-DAq0t0SA.css +1 -0
- package/static/ui/index.html +2 -2
- package/static/ui/assets/index-BAwWDK4H.js +0 -271
- package/static/ui/assets/index-DM9SnAfj.css +0 -1
|
@@ -74,6 +74,7 @@ export class RouteFailover {
|
|
|
74
74
|
const isSoft = context.errorKind === "soft_5xx" || context.errorKind === "deadline";
|
|
75
75
|
const isBusyCapacity = context.errorKind === "busy_capacity";
|
|
76
76
|
const isPurchaseFailure = context.errorKind === "purchase_failed";
|
|
77
|
+
const isStreamAborted = context.errorKind === "stream_aborted";
|
|
77
78
|
const info = this.pool.inspect(context.sellerId);
|
|
78
79
|
const freshPurchase = info.freshPurchase;
|
|
79
80
|
const budgetExceeded = !this.creditTracker.canAutoPurchase(this.now());
|
|
@@ -91,6 +92,15 @@ export class RouteFailover {
|
|
|
91
92
|
budgetExceeded
|
|
92
93
|
};
|
|
93
94
|
}
|
|
95
|
+
if (isStreamAborted) {
|
|
96
|
+
return {
|
|
97
|
+
action: "abort",
|
|
98
|
+
reason: "stream_aborted_after_response_started",
|
|
99
|
+
freshPurchase,
|
|
100
|
+
retryAttemptsBeforeFailover: context.attempt,
|
|
101
|
+
budgetExceeded
|
|
102
|
+
};
|
|
103
|
+
}
|
|
94
104
|
if (isPurchaseFailure) {
|
|
95
105
|
return {
|
|
96
106
|
action: "failover_next",
|
|
@@ -37,10 +37,10 @@ export interface PoolEntry {
|
|
|
37
37
|
lastFailAt: number;
|
|
38
38
|
/** 最近一次被 probe 的 unix 毫秒时间戳(即 cache.warmedAt) */
|
|
39
39
|
lastProbeAt: number;
|
|
40
|
-
/** 综合健康分 0-100
|
|
41
|
-
healthScore
|
|
42
|
-
/**
|
|
43
|
-
avgLatencyMs
|
|
40
|
+
/** 综合健康分 0-100;registry-only fallback 尚未探测时为空 */
|
|
41
|
+
healthScore?: number;
|
|
42
|
+
/** 平均延迟(毫秒);registry-only fallback 尚未探测时为空 */
|
|
43
|
+
avgLatencyMs?: number;
|
|
44
44
|
/** health probe 延迟(毫秒),可选 */
|
|
45
45
|
healthProbeLatencyMs?: number;
|
|
46
46
|
/** TTFT(毫秒),可选 */
|
|
@@ -166,7 +166,7 @@ export declare class SellerPool {
|
|
|
166
166
|
* may be selected before prewarm has produced a cache entry; failures
|
|
167
167
|
* from that first live request still need to affect the next route plan.
|
|
168
168
|
*/
|
|
169
|
-
ensureRegistrySellers(sellers: RegistrySeller[]
|
|
169
|
+
ensureRegistrySellers(sellers: RegistrySeller[]): void;
|
|
170
170
|
/**
|
|
171
171
|
* Pick up to `limit` candidates for a (model, protocol, payment) triple.
|
|
172
172
|
* Sellers in the `open` circuit are skipped unless their open state has
|
package/dist/src/seller-pool.js
CHANGED
|
@@ -87,7 +87,7 @@ export class SellerPool {
|
|
|
87
87
|
* may be selected before prewarm has produced a cache entry; failures
|
|
88
88
|
* from that first live request still need to affect the next route plan.
|
|
89
89
|
*/
|
|
90
|
-
ensureRegistrySellers(sellers
|
|
90
|
+
ensureRegistrySellers(sellers) {
|
|
91
91
|
for (const seller of sellers) {
|
|
92
92
|
const previous = this.entries.get(seller.id);
|
|
93
93
|
if (previous) {
|
|
@@ -107,9 +107,7 @@ export class SellerPool {
|
|
|
107
107
|
recentFailures: [],
|
|
108
108
|
lastSuccessAt: 0,
|
|
109
109
|
lastFailAt: 0,
|
|
110
|
-
lastProbeAt:
|
|
111
|
-
healthScore: 60,
|
|
112
|
-
avgLatencyMs: 0
|
|
110
|
+
lastProbeAt: 0
|
|
113
111
|
});
|
|
114
112
|
}
|
|
115
113
|
}
|
|
@@ -149,7 +147,7 @@ export class SellerPool {
|
|
|
149
147
|
})
|
|
150
148
|
.filter((row) => row.entry.circuit !== "open")
|
|
151
149
|
.filter((row) => !isCapacityBlocked(row.entry, now))
|
|
152
|
-
.sort((a, b) => b.entry.healthScore - a.entry.healthScore)
|
|
150
|
+
.sort((a, b) => (b.entry.healthScore ?? 0) - (a.entry.healthScore ?? 0))
|
|
153
151
|
.slice(0, limit);
|
|
154
152
|
return {
|
|
155
153
|
candidates,
|
|
@@ -173,7 +171,7 @@ export class SellerPool {
|
|
|
173
171
|
consecutiveFailures: 0,
|
|
174
172
|
recentFailures: [],
|
|
175
173
|
lastSuccessAt: now,
|
|
176
|
-
healthScore: Math.min(100, Math.max(entry.healthScore, 60)),
|
|
174
|
+
healthScore: Math.min(100, Math.max(entry.healthScore ?? 0, 60)),
|
|
177
175
|
capacityBlockedUntil: undefined
|
|
178
176
|
};
|
|
179
177
|
this.entries.set(sellerId, next);
|
|
@@ -196,7 +194,7 @@ export class SellerPool {
|
|
|
196
194
|
const next = {
|
|
197
195
|
...entry,
|
|
198
196
|
lastSuccessAt: now,
|
|
199
|
-
healthScore: Math.min(100, Math.max(entry.healthScore, 60)),
|
|
197
|
+
healthScore: Math.min(100, Math.max(entry.healthScore ?? 0, 60)),
|
|
200
198
|
avgLatencyMs: avgInferenceMs ?? entry.avgLatencyMs,
|
|
201
199
|
ttftMs: ttftMs ?? entry.ttftMs,
|
|
202
200
|
avgInferenceMs: avgInferenceMs ?? entry.avgInferenceMs,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type RegistrySeller, type RouteState } from "./seller-catalog.js";
|
|
2
|
+
import { type SellerRouteMetric, type SellerRoutePlan, type SellerRoutePrewarmCandidate } from "./seller-route-planner.js";
|
|
3
|
+
import type { SellerRoutingScorer } from "./seller-routing-strategy.js";
|
|
4
|
+
export type RecommendationScope = {
|
|
5
|
+
type: "recommended";
|
|
6
|
+
} | {
|
|
7
|
+
type: "custom";
|
|
8
|
+
sellerIds: string[];
|
|
9
|
+
};
|
|
10
|
+
export type UserNodeStatus = "ready" | "limited" | "unavailable";
|
|
11
|
+
export type RecommendationReasonCode = "top_route" | "unique_route" | "failover_route" | "routable" | "no_recent_probe" | "not_compatible" | "runtime_blocked" | "empty_focus_set";
|
|
12
|
+
export interface BlockedModelReason {
|
|
13
|
+
modelId: string;
|
|
14
|
+
reason: "runtime_blocked";
|
|
15
|
+
}
|
|
16
|
+
export interface SellerSetContribution {
|
|
17
|
+
setScore: number;
|
|
18
|
+
coverageCount: number;
|
|
19
|
+
top1Count: number;
|
|
20
|
+
topKCount: number;
|
|
21
|
+
uniqueModelCoverageCount: number;
|
|
22
|
+
averagePlannerRank?: number;
|
|
23
|
+
averagePlannerScore?: number;
|
|
24
|
+
healthPenalty: number;
|
|
25
|
+
missingMetricPenalty: number;
|
|
26
|
+
}
|
|
27
|
+
export interface RecommendationSellerMetrics {
|
|
28
|
+
healthScore?: number;
|
|
29
|
+
avgLatencyMs?: number;
|
|
30
|
+
ttftMs?: number;
|
|
31
|
+
avgInferenceMs?: number;
|
|
32
|
+
avgTokensPerSecond?: number;
|
|
33
|
+
discountRatio?: number;
|
|
34
|
+
routeStates: RouteState[];
|
|
35
|
+
}
|
|
36
|
+
export interface RecommendedSeller {
|
|
37
|
+
sellerId: string;
|
|
38
|
+
name?: string;
|
|
39
|
+
url: string;
|
|
40
|
+
rank: number;
|
|
41
|
+
status: UserNodeStatus;
|
|
42
|
+
reasonCode: RecommendationReasonCode;
|
|
43
|
+
reasonText: string;
|
|
44
|
+
routableModels: string[];
|
|
45
|
+
top1Models: string[];
|
|
46
|
+
topKModels: string[];
|
|
47
|
+
unsupportedModels: string[];
|
|
48
|
+
blockedModels: BlockedModelReason[];
|
|
49
|
+
aggregate: SellerSetContribution;
|
|
50
|
+
metrics: RecommendationSellerMetrics;
|
|
51
|
+
}
|
|
52
|
+
export interface ExcludedSeller {
|
|
53
|
+
sellerId: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
url: string;
|
|
56
|
+
status: UserNodeStatus;
|
|
57
|
+
reasonCode: RecommendationReasonCode;
|
|
58
|
+
reasonText: string;
|
|
59
|
+
unsupportedModels: string[];
|
|
60
|
+
blockedModels: BlockedModelReason[];
|
|
61
|
+
}
|
|
62
|
+
export interface ModelRecommendationSummary {
|
|
63
|
+
modelId: string;
|
|
64
|
+
protocol: string;
|
|
65
|
+
routeCount: number;
|
|
66
|
+
topSellerId?: string;
|
|
67
|
+
hasRoutableSeller: boolean;
|
|
68
|
+
warningCode?: "no_routable_seller" | "only_limited_sellers" | "custom_missing_coverage";
|
|
69
|
+
routes: Array<{
|
|
70
|
+
sellerId: string;
|
|
71
|
+
rank: number;
|
|
72
|
+
status: UserNodeStatus;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
export interface RecommendationDiagnostics {
|
|
76
|
+
registryVisibleCount: number;
|
|
77
|
+
candidateSellerCount: number;
|
|
78
|
+
focusModelCount: number;
|
|
79
|
+
unknownSellerIds: string[];
|
|
80
|
+
modelPlanCount: number;
|
|
81
|
+
unroutableModelIds: string[];
|
|
82
|
+
}
|
|
83
|
+
export interface SellerRouteRecommendations {
|
|
84
|
+
focusModelSet: string[];
|
|
85
|
+
protocol: string;
|
|
86
|
+
protocolByModelId: Record<string, string>;
|
|
87
|
+
paymentMethod: string;
|
|
88
|
+
scorer: SellerRoutingScorer;
|
|
89
|
+
scope: RecommendationScope;
|
|
90
|
+
sellers: RecommendedSeller[];
|
|
91
|
+
excludedSellers: ExcludedSeller[];
|
|
92
|
+
modelSummaries: ModelRecommendationSummary[];
|
|
93
|
+
plansByModelId: Record<string, SellerRoutePlan>;
|
|
94
|
+
diagnostics: RecommendationDiagnostics;
|
|
95
|
+
}
|
|
96
|
+
export interface BuildSellerRouteRecommendationsInput {
|
|
97
|
+
focusModelSet: string[];
|
|
98
|
+
protocol: string;
|
|
99
|
+
protocolByModelId?: Record<string, string>;
|
|
100
|
+
paymentMethod: string;
|
|
101
|
+
scorer: SellerRoutingScorer;
|
|
102
|
+
scope: RecommendationScope;
|
|
103
|
+
registrySellers: RegistrySeller[];
|
|
104
|
+
prewarmCandidatesForModel?: (modelId: string, protocol: string) => SellerRoutePrewarmCandidate[] | undefined;
|
|
105
|
+
sellerMetrics?: SellerRouteMetric[];
|
|
106
|
+
topK?: number;
|
|
107
|
+
now?: number;
|
|
108
|
+
}
|
|
109
|
+
export declare function buildSellerRouteRecommendations(input: BuildSellerRouteRecommendationsInput): SellerRouteRecommendations;
|
|
110
|
+
//# sourceMappingURL=seller-route-recommendations.d.ts.map
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { isBuyerVisibleRegistrySeller } from "./seller-catalog.js";
|
|
2
|
+
import { planSellerRouteSet } from "./seller-route-planner.js";
|
|
3
|
+
export function buildSellerRouteRecommendations(input) {
|
|
4
|
+
const focusModelSet = uniqueStrings(input.focusModelSet);
|
|
5
|
+
const topK = Math.max(1, Math.floor(input.topK ?? 3));
|
|
6
|
+
const protocolByModelId = Object.fromEntries(focusModelSet.map((modelId) => [
|
|
7
|
+
modelId,
|
|
8
|
+
input.protocolByModelId?.[modelId]?.trim() || input.protocol
|
|
9
|
+
]));
|
|
10
|
+
const visibleRegistrySellers = input.registrySellers
|
|
11
|
+
.filter((seller) => Boolean(seller?.id && seller.url))
|
|
12
|
+
.filter(isBuyerVisibleRegistrySeller);
|
|
13
|
+
const scopeSellerIds = input.scope.type === "custom"
|
|
14
|
+
? new Set(input.scope.sellerIds.map(normalizeSellerId).filter(Boolean))
|
|
15
|
+
: undefined;
|
|
16
|
+
const candidateSellers = scopeSellerIds
|
|
17
|
+
? visibleRegistrySellers.filter((seller) => scopeSellerIds.has(normalizeSellerId(seller.id)))
|
|
18
|
+
: visibleRegistrySellers;
|
|
19
|
+
const candidateOrder = new Map(candidateSellers.map((seller, index) => [seller.id, index]));
|
|
20
|
+
const unknownSellerIds = input.scope.type === "custom"
|
|
21
|
+
? uniqueStrings(input.scope.sellerIds.map((sellerId) => sellerId.trim()).filter(Boolean))
|
|
22
|
+
.filter((sellerId) => !visibleRegistrySellers.some((seller) => normalizeSellerId(seller.id) === normalizeSellerId(sellerId)))
|
|
23
|
+
: [];
|
|
24
|
+
const accumulators = new Map();
|
|
25
|
+
const plansByModelId = {};
|
|
26
|
+
for (const seller of candidateSellers) {
|
|
27
|
+
accumulators.set(seller.id, {
|
|
28
|
+
seller,
|
|
29
|
+
routes: [],
|
|
30
|
+
unsupportedModels: new Set(),
|
|
31
|
+
blockedModels: []
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const modelSummaries = [];
|
|
35
|
+
for (const modelId of focusModelSet) {
|
|
36
|
+
const protocol = protocolByModelId[modelId] ?? input.protocol;
|
|
37
|
+
const plan = planSellerRouteSet({
|
|
38
|
+
modelId,
|
|
39
|
+
protocol,
|
|
40
|
+
paymentMethod: input.paymentMethod,
|
|
41
|
+
registrySellers: candidateSellers,
|
|
42
|
+
routing: input.scope.type === "custom"
|
|
43
|
+
? { mode: "fixedSet", scorer: input.scorer, sellerIds: input.scope.sellerIds }
|
|
44
|
+
: { mode: "fullAuto", scorer: input.scorer },
|
|
45
|
+
prewarmCandidates: input.prewarmCandidatesForModel?.(modelId, protocol),
|
|
46
|
+
sellerMetrics: input.sellerMetrics,
|
|
47
|
+
now: input.now
|
|
48
|
+
});
|
|
49
|
+
plansByModelId[modelId] = plan;
|
|
50
|
+
const routeSellerIds = new Set(plan.routes.map((route) => route.seller.id));
|
|
51
|
+
const blockedSellerIds = new Set(plan.diagnostics.blockedSellerIds);
|
|
52
|
+
plan.routes.forEach((route, index) => {
|
|
53
|
+
const accumulator = accumulators.get(route.seller.id);
|
|
54
|
+
if (!accumulator)
|
|
55
|
+
return;
|
|
56
|
+
accumulator.routes.push({
|
|
57
|
+
modelId,
|
|
58
|
+
route,
|
|
59
|
+
rank: index + 1,
|
|
60
|
+
status: statusFromRoute(route)
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
for (const seller of candidateSellers) {
|
|
64
|
+
if (routeSellerIds.has(seller.id))
|
|
65
|
+
continue;
|
|
66
|
+
const accumulator = accumulators.get(seller.id);
|
|
67
|
+
if (!accumulator)
|
|
68
|
+
continue;
|
|
69
|
+
if (blockedSellerIds.has(seller.id)) {
|
|
70
|
+
accumulator.blockedModels.push({ modelId, reason: "runtime_blocked" });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
accumulator.unsupportedModels.add(modelId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const routeStatuses = plan.routes.map(statusFromRoute);
|
|
77
|
+
modelSummaries.push({
|
|
78
|
+
modelId,
|
|
79
|
+
protocol,
|
|
80
|
+
routeCount: plan.routes.length,
|
|
81
|
+
topSellerId: plan.routes[0]?.seller.id,
|
|
82
|
+
hasRoutableSeller: plan.routes.length > 0,
|
|
83
|
+
warningCode: warningCodeForModel(plan.routes.length, routeStatuses, input.scope.type),
|
|
84
|
+
routes: plan.routes.map((route, index) => ({
|
|
85
|
+
sellerId: route.seller.id,
|
|
86
|
+
rank: index + 1,
|
|
87
|
+
status: routeStatuses[index] ?? "limited"
|
|
88
|
+
}))
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const uniqueTopSellerByModel = new Map(modelSummaries
|
|
92
|
+
.filter((summary) => summary.routeCount === 1 && summary.topSellerId)
|
|
93
|
+
.map((summary) => [summary.modelId, summary.topSellerId]));
|
|
94
|
+
const recommended = [];
|
|
95
|
+
const excluded = [];
|
|
96
|
+
for (const accumulator of accumulators.values()) {
|
|
97
|
+
if (accumulator.routes.length === 0) {
|
|
98
|
+
excluded.push(excludedSellerFromAccumulator(accumulator, focusModelSet));
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
recommended.push(recommendedSellerFromAccumulator({
|
|
102
|
+
accumulator,
|
|
103
|
+
focusModelSet,
|
|
104
|
+
topK,
|
|
105
|
+
uniqueTopSellerByModel,
|
|
106
|
+
registryOrder: candidateOrder.get(accumulator.seller.id) ?? Number.MAX_SAFE_INTEGER
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
recommended.sort((left, right) => right.aggregate.setScore - left.aggregate.setScore ||
|
|
110
|
+
(candidateOrder.get(left.sellerId) ?? Number.MAX_SAFE_INTEGER) - (candidateOrder.get(right.sellerId) ?? Number.MAX_SAFE_INTEGER) ||
|
|
111
|
+
left.sellerId.localeCompare(right.sellerId));
|
|
112
|
+
recommended.forEach((seller, index) => {
|
|
113
|
+
seller.rank = index + 1;
|
|
114
|
+
});
|
|
115
|
+
excluded.sort((left, right) => (candidateOrder.get(left.sellerId) ?? Number.MAX_SAFE_INTEGER) - (candidateOrder.get(right.sellerId) ?? Number.MAX_SAFE_INTEGER) ||
|
|
116
|
+
left.sellerId.localeCompare(right.sellerId));
|
|
117
|
+
return {
|
|
118
|
+
focusModelSet,
|
|
119
|
+
protocol: input.protocol,
|
|
120
|
+
protocolByModelId,
|
|
121
|
+
paymentMethod: input.paymentMethod,
|
|
122
|
+
scorer: input.scorer,
|
|
123
|
+
scope: input.scope,
|
|
124
|
+
sellers: recommended,
|
|
125
|
+
excludedSellers: excluded,
|
|
126
|
+
modelSummaries,
|
|
127
|
+
plansByModelId,
|
|
128
|
+
diagnostics: {
|
|
129
|
+
registryVisibleCount: visibleRegistrySellers.length,
|
|
130
|
+
candidateSellerCount: candidateSellers.length,
|
|
131
|
+
focusModelCount: focusModelSet.length,
|
|
132
|
+
unknownSellerIds,
|
|
133
|
+
modelPlanCount: Object.keys(plansByModelId).length,
|
|
134
|
+
unroutableModelIds: modelSummaries
|
|
135
|
+
.filter((summary) => !summary.hasRoutableSeller)
|
|
136
|
+
.map((summary) => summary.modelId)
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function recommendedSellerFromAccumulator(input) {
|
|
141
|
+
const { accumulator, focusModelSet, topK, uniqueTopSellerByModel } = input;
|
|
142
|
+
const routeStatuses = accumulator.routes.map((entry) => entry.status);
|
|
143
|
+
const status = aggregateStatus(routeStatuses);
|
|
144
|
+
const routableModels = accumulator.routes.map((entry) => entry.modelId);
|
|
145
|
+
const top1Models = accumulator.routes
|
|
146
|
+
.filter((entry) => entry.rank === 1)
|
|
147
|
+
.map((entry) => entry.modelId);
|
|
148
|
+
const topKModels = accumulator.routes
|
|
149
|
+
.filter((entry) => entry.rank <= topK)
|
|
150
|
+
.map((entry) => entry.modelId);
|
|
151
|
+
const uniqueModels = accumulator.routes
|
|
152
|
+
.filter((entry) => uniqueTopSellerByModel.get(entry.modelId) === accumulator.seller.id)
|
|
153
|
+
.map((entry) => entry.modelId);
|
|
154
|
+
const averagePlannerRank = average(accumulator.routes.map((entry) => entry.rank));
|
|
155
|
+
const averagePlannerScore = average(accumulator.routes.map((entry) => rankScore(entry.rank)));
|
|
156
|
+
const healthPenalty = status === "ready" ? 0 : status === "limited" ? 15 : 100;
|
|
157
|
+
const missingMetricPenalty = accumulator.routes.reduce((total, entry) => total + missingMetricPenaltyForRoute(entry.route), 0);
|
|
158
|
+
const aggregate = {
|
|
159
|
+
setScore: top1Models.length * 100 +
|
|
160
|
+
topKModels.length * 40 +
|
|
161
|
+
uniqueModels.length * 60 +
|
|
162
|
+
(averagePlannerScore ?? 0) * 0.5 +
|
|
163
|
+
routableModels.length * 10 -
|
|
164
|
+
healthPenalty -
|
|
165
|
+
missingMetricPenalty,
|
|
166
|
+
coverageCount: routableModels.length,
|
|
167
|
+
top1Count: top1Models.length,
|
|
168
|
+
topKCount: topKModels.length,
|
|
169
|
+
uniqueModelCoverageCount: uniqueModels.length,
|
|
170
|
+
averagePlannerRank,
|
|
171
|
+
averagePlannerScore,
|
|
172
|
+
healthPenalty,
|
|
173
|
+
missingMetricPenalty
|
|
174
|
+
};
|
|
175
|
+
const bestRoute = accumulator.routes
|
|
176
|
+
.slice()
|
|
177
|
+
.sort((left, right) => left.rank - right.rank)[0]?.route;
|
|
178
|
+
const reason = reasonForRecommendedSeller({
|
|
179
|
+
status,
|
|
180
|
+
top1Models,
|
|
181
|
+
topKModels,
|
|
182
|
+
uniqueModels,
|
|
183
|
+
routableModels,
|
|
184
|
+
focusModelSet,
|
|
185
|
+
routes: accumulator.routes
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
sellerId: accumulator.seller.id,
|
|
189
|
+
name: accumulator.seller.name,
|
|
190
|
+
url: accumulator.seller.url,
|
|
191
|
+
rank: input.registryOrder + 1,
|
|
192
|
+
status,
|
|
193
|
+
reasonCode: reason.code,
|
|
194
|
+
reasonText: reason.text,
|
|
195
|
+
routableModels,
|
|
196
|
+
top1Models,
|
|
197
|
+
topKModels,
|
|
198
|
+
unsupportedModels: focusModelSet.filter((modelId) => accumulator.unsupportedModels.has(modelId)),
|
|
199
|
+
blockedModels: accumulator.blockedModels,
|
|
200
|
+
aggregate,
|
|
201
|
+
metrics: {
|
|
202
|
+
healthScore: bestRoute?.metrics.healthScore,
|
|
203
|
+
avgLatencyMs: bestRoute?.metrics.avgLatencyMs,
|
|
204
|
+
ttftMs: bestRoute?.metrics.ttftMs,
|
|
205
|
+
avgInferenceMs: bestRoute?.metrics.avgInferenceMs,
|
|
206
|
+
avgTokensPerSecond: bestRoute?.metrics.avgTokensPerSecond,
|
|
207
|
+
discountRatio: bestRoute?.metrics.discountRatio,
|
|
208
|
+
routeStates: uniqueRouteStates(accumulator.routes.map((entry) => entry.route.metrics.routeState))
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function excludedSellerFromAccumulator(accumulator, focusModelSet) {
|
|
213
|
+
const blockedModels = accumulator.blockedModels;
|
|
214
|
+
const reasonCode = blockedModels.length > 0 ? "runtime_blocked" : "not_compatible";
|
|
215
|
+
return {
|
|
216
|
+
sellerId: accumulator.seller.id,
|
|
217
|
+
name: accumulator.seller.name,
|
|
218
|
+
url: accumulator.seller.url,
|
|
219
|
+
status: "unavailable",
|
|
220
|
+
reasonCode,
|
|
221
|
+
reasonText: reasonCode === "runtime_blocked"
|
|
222
|
+
? `Unavailable for ${blockedModels.length} selected model${blockedModels.length === 1 ? "" : "s"} due to runtime limits`
|
|
223
|
+
: "Not compatible with the selected model set",
|
|
224
|
+
unsupportedModels: focusModelSet.filter((modelId) => accumulator.unsupportedModels.has(modelId)),
|
|
225
|
+
blockedModels
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function statusFromRoute(route) {
|
|
229
|
+
const routeState = route.metrics.routeState;
|
|
230
|
+
if (routeState === "error" || routeState === "cooldown" || routeState === "full") {
|
|
231
|
+
return "unavailable";
|
|
232
|
+
}
|
|
233
|
+
if (routeState === "degraded" || routeState === "unknown") {
|
|
234
|
+
return "limited";
|
|
235
|
+
}
|
|
236
|
+
if (routeState === "ok") {
|
|
237
|
+
return "ready";
|
|
238
|
+
}
|
|
239
|
+
if (Number.isFinite(route.metrics.healthScore)) {
|
|
240
|
+
return route.metrics.healthScore >= 70 ? "ready" : "limited";
|
|
241
|
+
}
|
|
242
|
+
return "limited";
|
|
243
|
+
}
|
|
244
|
+
function aggregateStatus(statuses) {
|
|
245
|
+
if (statuses.length === 0)
|
|
246
|
+
return "unavailable";
|
|
247
|
+
if (statuses.some((status) => status === "ready"))
|
|
248
|
+
return "ready";
|
|
249
|
+
if (statuses.some((status) => status === "limited"))
|
|
250
|
+
return "limited";
|
|
251
|
+
return "unavailable";
|
|
252
|
+
}
|
|
253
|
+
function warningCodeForModel(routeCount, statuses, scopeType) {
|
|
254
|
+
if (routeCount === 0) {
|
|
255
|
+
return scopeType === "custom" ? "custom_missing_coverage" : "no_routable_seller";
|
|
256
|
+
}
|
|
257
|
+
if (statuses.length > 0 && statuses.every((status) => status !== "ready")) {
|
|
258
|
+
return "only_limited_sellers";
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
function reasonForRecommendedSeller(input) {
|
|
263
|
+
if (input.uniqueModels.length > 0) {
|
|
264
|
+
return {
|
|
265
|
+
code: "unique_route",
|
|
266
|
+
text: `Only route for ${formatModelList(input.uniqueModels)}`
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (input.top1Models.length > 0) {
|
|
270
|
+
return {
|
|
271
|
+
code: "top_route",
|
|
272
|
+
text: `Top route for ${formatModelList(input.top1Models)}`
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (input.topKModels.length > 0) {
|
|
276
|
+
return {
|
|
277
|
+
code: "failover_route",
|
|
278
|
+
text: `Failover route for ${formatModelList(input.topKModels)}`
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (input.status === "limited" && input.routes.every((entry) => entry.status === "limited")) {
|
|
282
|
+
return {
|
|
283
|
+
code: "no_recent_probe",
|
|
284
|
+
text: `Routable for ${input.routableModels.length}/${input.focusModelSet.length} selected models, but metrics are limited`
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
code: "routable",
|
|
289
|
+
text: `Routable for ${input.routableModels.length}/${input.focusModelSet.length} selected models`
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function missingMetricPenaltyForRoute(route) {
|
|
293
|
+
let missing = 0;
|
|
294
|
+
if (!Number.isFinite(route.metrics.healthScore))
|
|
295
|
+
missing += 1;
|
|
296
|
+
if (!Number.isFinite(route.metrics.ttftMs) && !Number.isFinite(route.metrics.avgLatencyMs))
|
|
297
|
+
missing += 1;
|
|
298
|
+
if (!Number.isFinite(route.metrics.discountRatio))
|
|
299
|
+
missing += 1;
|
|
300
|
+
return missing * 5;
|
|
301
|
+
}
|
|
302
|
+
function rankScore(rank) {
|
|
303
|
+
return Math.max(0, 100 - Math.max(0, rank - 1) * 25);
|
|
304
|
+
}
|
|
305
|
+
function average(values) {
|
|
306
|
+
const finite = values.filter(Number.isFinite);
|
|
307
|
+
if (finite.length === 0)
|
|
308
|
+
return undefined;
|
|
309
|
+
return finite.reduce((sum, value) => sum + value, 0) / finite.length;
|
|
310
|
+
}
|
|
311
|
+
function uniqueStrings(values) {
|
|
312
|
+
const seen = new Set();
|
|
313
|
+
const result = [];
|
|
314
|
+
for (const value of values) {
|
|
315
|
+
const trimmed = value.trim();
|
|
316
|
+
if (!trimmed || seen.has(trimmed))
|
|
317
|
+
continue;
|
|
318
|
+
seen.add(trimmed);
|
|
319
|
+
result.push(trimmed);
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
function uniqueRouteStates(values) {
|
|
324
|
+
return Array.from(new Set(values.filter((value) => Boolean(value))));
|
|
325
|
+
}
|
|
326
|
+
function normalizeSellerId(value) {
|
|
327
|
+
return value.trim().toLowerCase();
|
|
328
|
+
}
|
|
329
|
+
function formatModelList(modelIds) {
|
|
330
|
+
if (modelIds.length <= 2)
|
|
331
|
+
return modelIds.join(" and ");
|
|
332
|
+
return `${modelIds.slice(0, 2).join(", ")} +${modelIds.length - 2}`;
|
|
333
|
+
}
|
|
334
|
+
//# sourceMappingURL=seller-route-recommendations.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenbuddy/tokenbuddy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.41",
|
|
4
4
|
"description": "TokenBuddy Client CLI and Daemon",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@clack/prompts": "^0.7.0",
|
|
31
|
-
"@tokenbuddy/contracts": "^1.0.
|
|
31
|
+
"@tokenbuddy/contracts": "^1.0.41",
|
|
32
32
|
"@tokenbuddy/logging": "^1.0.0",
|
|
33
33
|
"cli-table3": "^0.6.4",
|
|
34
34
|
"commander": "^12.0.0",
|