@antseed/cli 0.1.189 → 0.1.190
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/README.md +0 -9
- package/dist/config/service-metadata.test.js +0 -4
- package/dist/config/service-metadata.test.js.map +1 -1
- package/dist/gateway/gateway-policy.test.js +0 -19
- package/dist/gateway/gateway-policy.test.js.map +1 -1
- package/dist/plugins/registry.d.ts.map +1 -1
- package/dist/plugins/registry.js +0 -6
- package/dist/plugins/registry.js.map +1 -1
- package/dist/proxy/buyer-proxy.d.ts +4 -56
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +120 -480
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +5 -1331
- package/dist/proxy/buyer-proxy.test.js.map +1 -1
- package/dist/proxy/conversation-store.d.ts +0 -8
- package/dist/proxy/conversation-store.d.ts.map +1 -1
- package/dist/proxy/conversation-store.js +0 -20
- package/dist/proxy/conversation-store.js.map +1 -1
- package/dist/proxy/conversation-store.test.js +1 -54
- package/dist/proxy/conversation-store.test.js.map +1 -1
- package/dist/proxy/gateway-policy.test.js +0 -110
- package/dist/proxy/gateway-policy.test.js.map +1 -1
- package/dist/proxy/network-models.d.ts +1 -21
- package/dist/proxy/network-models.d.ts.map +1 -1
- package/dist/proxy/network-models.js +0 -2
- package/dist/proxy/network-models.js.map +1 -1
- package/dist/proxy/request-utils.d.ts +0 -20
- package/dist/proxy/request-utils.d.ts.map +1 -1
- package/dist/proxy/request-utils.js +1 -46
- package/dist/proxy/request-utils.js.map +1 -1
- package/dist/proxy/route-policy.d.ts +0 -7
- package/dist/proxy/route-policy.d.ts.map +1 -1
- package/dist/proxy/route-policy.js +0 -10
- package/dist/proxy/route-policy.js.map +1 -1
- package/package.json +8 -10
- package/dist/proxy/router-execution.d.ts +0 -63
- package/dist/proxy/router-execution.d.ts.map +0 -1
- package/dist/proxy/router-execution.js +0 -220
- package/dist/proxy/router-execution.js.map +0 -1
- package/dist/proxy/router-execution.test.d.ts +0 -2
- package/dist/proxy/router-execution.test.d.ts.map +0 -1
- package/dist/proxy/router-execution.test.js +0 -269
- package/dist/proxy/router-execution.test.js.map +0 -1
- package/dist/proxy/routing-services.d.ts +0 -37
- package/dist/proxy/routing-services.d.ts.map +0 -1
- package/dist/proxy/routing-services.js +0 -58
- package/dist/proxy/routing-services.js.map +0 -1
- package/dist/proxy/routing-services.test.d.ts +0 -2
- package/dist/proxy/routing-services.test.d.ts.map +0 -1
- package/dist/proxy/routing-services.test.js +0 -95
- package/dist/proxy/routing-services.test.js.map +0 -1
- package/dist/proxy/routing-usage.d.ts +0 -4
- package/dist/proxy/routing-usage.d.ts.map +0 -1
- package/dist/proxy/routing-usage.js +0 -19
- package/dist/proxy/routing-usage.js.map +0 -1
- package/dist/proxy/routing-usage.test.d.ts +0 -2
- package/dist/proxy/routing-usage.test.d.ts.map +0 -1
- package/dist/proxy/routing-usage.test.js +0 -32
- package/dist/proxy/routing-usage.test.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antseed/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.190",
|
|
4
4
|
"description": "Antseed Network CLI and Web Dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -32,23 +32,21 @@
|
|
|
32
32
|
"qrcode": "^1.5.4",
|
|
33
33
|
"reflect-metadata": "^0.2.2",
|
|
34
34
|
"zod": "^4",
|
|
35
|
+
"@antseed/ants": "0.1.25",
|
|
35
36
|
"@antseed/ant-agent": "0.1.35",
|
|
36
|
-
"@antseed/
|
|
37
|
-
"@antseed/
|
|
38
|
-
"@antseed/
|
|
37
|
+
"@antseed/api-adapter": "0.1.52",
|
|
38
|
+
"@antseed/connected-apps": "0.1.2",
|
|
39
|
+
"@antseed/node": "0.2.142",
|
|
39
40
|
"@antseed/gateway-console": "0.1.7",
|
|
40
|
-
"@antseed/
|
|
41
|
-
"@antseed/
|
|
42
|
-
"@antseed/provider-core": "0.2.58",
|
|
43
|
-
"@antseed/payments": "0.1.68"
|
|
41
|
+
"@antseed/provider-core": "0.2.59",
|
|
42
|
+
"@antseed/payments": "0.1.69"
|
|
44
43
|
},
|
|
45
44
|
"devDependencies": {
|
|
46
45
|
"@types/better-sqlite3": "^7.6.13",
|
|
47
46
|
"@types/geoip-country": "^5",
|
|
48
47
|
"@types/node": "^20.11.0",
|
|
49
48
|
"tsx": "^4.7.0",
|
|
50
|
-
"typescript": "^5.3.0"
|
|
51
|
-
"@antseed/router-local": "0.1.48"
|
|
49
|
+
"typescript": "^5.3.0"
|
|
52
50
|
},
|
|
53
51
|
"scripts": {
|
|
54
52
|
"prebuild": "node ../../scripts/remove-paths.mjs dist",
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { type AntseedNode, type RoutingModelEntryV1, type ModelRoutingPreferences, type PeerInfo, type RouteCandidate, type RouteRecommendation, type SerializedHttpRequest } from '@antseed/node';
|
|
2
|
-
import type { RoutingSelection, RoutingServiceTarget } from '@antseed/node';
|
|
3
|
-
import { type ModelRoutingClientApi } from '@antseed/router-core';
|
|
4
|
-
/**
|
|
5
|
-
* Router model lists (IRP `GET /v1/routing/models`) in a TanStack Query Core cache: reused for
|
|
6
|
-
* `ttlMs`, shared by concurrent callers so a burst of requests sends one models request, and
|
|
7
|
-
* removed when a routing attempt fails.
|
|
8
|
-
*/
|
|
9
|
-
export declare class RoutingModelsCache {
|
|
10
|
-
private readonly ttlMs;
|
|
11
|
-
private readonly queries;
|
|
12
|
-
constructor(ttlMs?: number);
|
|
13
|
-
get(client: Pick<ModelRoutingClientApi, 'listModels'>, target: RoutingServiceTarget, peers: PeerInfo[], node: Pick<AntseedNode, 'sendRequest'>): Promise<RoutingModelEntryV1[]>;
|
|
14
|
-
invalidate(target: RoutingServiceTarget): void;
|
|
15
|
-
expiresAt(target: RoutingServiceTarget): number;
|
|
16
|
-
}
|
|
17
|
-
type ExecutionCandidate = RouteCandidate & {
|
|
18
|
-
peer: PeerInfo;
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Build the text-model destinations this buyer's policies allow. The router chooses among them.
|
|
22
|
-
* `preferences` apply as for automatic routing: the peer allow and block lists and `minTrustScore`
|
|
23
|
-
* exclude, judged on the same effective reputation (`effectiveModelReputationScore`) automatic
|
|
24
|
-
* routing uses; `maxInputUsdPerMillion` is a soft ranking limit there and never excludes here.
|
|
25
|
-
*/
|
|
26
|
-
export declare function eligibleRouterCandidates(request: SerializedHttpRequest, peers: PeerInfo[], requiredParameters: readonly string[], preferences: ModelRoutingPreferences | null, allowsPeer: (request: SerializedHttpRequest, peer: PeerInfo) => boolean): ExecutionCandidate[];
|
|
27
|
-
/**
|
|
28
|
-
* Keep recommendations that match allowed destinations, in the router's order.
|
|
29
|
-
* Each result names its provider, so a model-only entry becomes one entry per allowed
|
|
30
|
-
* provider and the normal dispatch path can never pick a provider the buyer did not allow.
|
|
31
|
-
* Invalid entries and duplicates are discarded; a suggested `reasoningEffort` is kept but not yet applied.
|
|
32
|
-
*/
|
|
33
|
-
export declare function resolveRouterRecommendations(routes: readonly RouteRecommendation[], candidates: readonly RouteCandidate[]): RouteRecommendation[];
|
|
34
|
-
/**
|
|
35
|
-
* Turn one recommendation into an ordinary inference request: set the model, and pin
|
|
36
|
-
* the seller/provider when the router chose them. The normal dispatch path does the rest.
|
|
37
|
-
*/
|
|
38
|
-
export declare function requestForRecommendation(request: SerializedHttpRequest, route: RouteRecommendation, requestId: string): SerializedHttpRequest;
|
|
39
|
-
/**
|
|
40
|
-
* Buy recommendations from the selected routing service and keep only allowed ones.
|
|
41
|
-
* This does not send inference; BuyerProxy dispatches each recommendation normally.
|
|
42
|
-
*/
|
|
43
|
-
export declare function executeRouterSelection(args: {
|
|
44
|
-
node: Pick<AntseedNode, 'sendRequest'>;
|
|
45
|
-
client: ModelRoutingClientApi;
|
|
46
|
-
request: SerializedHttpRequest;
|
|
47
|
-
peers: PeerInfo[];
|
|
48
|
-
candidates: ExecutionCandidate[];
|
|
49
|
-
conversationKey: string | null;
|
|
50
|
-
selection: Extract<RoutingSelection, {
|
|
51
|
-
kind: 'router';
|
|
52
|
-
}>;
|
|
53
|
-
signal: AbortSignal;
|
|
54
|
-
modelsCache?: RoutingModelsCache;
|
|
55
|
-
onRoutingRequest?: (requestId: string) => void;
|
|
56
|
-
/** Buyer identity that pays for the ranking (a gateway key's wallet); the default identity when unset. */
|
|
57
|
-
buyerIdentity?: string;
|
|
58
|
-
}): Promise<RouteRecommendation[]>;
|
|
59
|
-
/** No candidate is left to rank once the buyer's, the key's and the router's model limits apply; nothing was paid. */
|
|
60
|
-
export declare class NoEligibleCandidatesError extends Error {
|
|
61
|
-
}
|
|
62
|
-
export {};
|
|
63
|
-
//# sourceMappingURL=router-execution.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"router-execution.d.ts","sourceRoot":"","sources":["../../src/proxy/router-execution.ts"],"names":[],"mappings":"AACA,OAAO,EAAkH,KAAK,WAAW,EAAE,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,KAAK,mBAAmB,EAAE,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAIlT,OAAO,KAAK,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAgE,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAI/H;;;;GAIG;AACH,qBAAa,kBAAkB;IAKjB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAJlC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAEtB;gBAE2B,KAAK,SAAS;IAErC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE,oBAAoB,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAkBrL,UAAU,CAAC,MAAM,EAAE,oBAAoB,GAAG,IAAI;IAI9C,SAAS,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM;CAGhD;AAMD,KAAK,kBAAkB,GAAG,cAAc,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAA;AAE7D;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,qBAAqB,EAC9B,KAAK,EAAE,QAAQ,EAAE,EACjB,kBAAkB,EAAE,SAAS,MAAM,EAAE,EACrC,WAAW,EAAE,uBAAuB,GAAG,IAAI,EAC3C,UAAU,EAAE,CAAC,OAAO,EAAE,qBAAqB,EAAE,IAAI,EAAE,QAAQ,KAAK,OAAO,GACtE,kBAAkB,EAAE,CAkCtB;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE,EAAE,UAAU,EAAE,SAAS,cAAc,EAAE,GAAG,mBAAmB,EAAE,CAoBjJ;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,qBAAqB,EAAE,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,qBAAqB,CAU7I;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IACvC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,OAAO,EAAE,qBAAqB,CAAC;IAC/B,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACjC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC,gBAAgB,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;IACzD,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,0GAA0G;IAC1G,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAsDjC;AAED,sHAAsH;AACtH,qBAAa,yBAA0B,SAAQ,KAAK;CAAG"}
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { QueryClient } from '@tanstack/query-core';
|
|
2
|
-
import { buildNetworkServiceOffers, effectiveModelReputationScore, isModelRouteEligible, normalizedModelReputationScore } from '@antseed/node';
|
|
3
|
-
import { detectRequestServiceApiProtocol } from './service-api-adapter.js';
|
|
4
|
-
import { findMissingRequiredParameters, getExplicitProviderOverride, resolvePeerRoutePlan } from './routing.js';
|
|
5
|
-
import { overrideRoutedModelInBody } from './request-utils.js';
|
|
6
|
-
import { candidatePricing, RouterCannotRankError, routerModelResolver } from '@antseed/router-core';
|
|
7
|
-
const MODELS_TIMEOUT_MS = 5_000;
|
|
8
|
-
/**
|
|
9
|
-
* Router model lists (IRP `GET /v1/routing/models`) in a TanStack Query Core cache: reused for
|
|
10
|
-
* `ttlMs`, shared by concurrent callers so a burst of requests sends one models request, and
|
|
11
|
-
* removed when a routing attempt fails.
|
|
12
|
-
*/
|
|
13
|
-
export class RoutingModelsCache {
|
|
14
|
-
ttlMs;
|
|
15
|
-
queries = new QueryClient({
|
|
16
|
-
defaultOptions: { queries: { retry: false, networkMode: 'always', gcTime: Infinity, structuralSharing: false } },
|
|
17
|
-
});
|
|
18
|
-
constructor(ttlMs = 60_000) {
|
|
19
|
-
this.ttlMs = ttlMs;
|
|
20
|
-
}
|
|
21
|
-
async get(client, target, peers, node) {
|
|
22
|
-
const models = await this.queries.fetchQuery({
|
|
23
|
-
queryKey: modelsKey(target),
|
|
24
|
-
staleTime: this.ttlMs,
|
|
25
|
-
queryFn: () => {
|
|
26
|
-
const signal = AbortSignal.timeout(MODELS_TIMEOUT_MS);
|
|
27
|
-
return client.listModels(target, peers, {
|
|
28
|
-
signal,
|
|
29
|
-
sendRequest: (peer, request) => {
|
|
30
|
-
if (peer.peerId !== target.peerId)
|
|
31
|
-
throw new Error('Router models must come from the selected routing-service peer');
|
|
32
|
-
return node.sendRequest(peer, request, { signal, controlPlane: true });
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
return structuredClone(models);
|
|
38
|
-
}
|
|
39
|
-
invalidate(target) {
|
|
40
|
-
this.queries.removeQueries({ queryKey: modelsKey(target), exact: true });
|
|
41
|
-
}
|
|
42
|
-
expiresAt(target) {
|
|
43
|
-
return (this.queries.getQueryState(modelsKey(target))?.dataUpdatedAt ?? 0) + this.ttlMs;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function modelsKey(target) {
|
|
47
|
-
return [target.peerId, target.provider, target.serviceId];
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Build the text-model destinations this buyer's policies allow. The router chooses among them.
|
|
51
|
-
* `preferences` apply as for automatic routing: the peer allow and block lists and `minTrustScore`
|
|
52
|
-
* exclude, judged on the same effective reputation (`effectiveModelReputationScore`) automatic
|
|
53
|
-
* routing uses; `maxInputUsdPerMillion` is a soft ranking limit there and never excludes here.
|
|
54
|
-
*/
|
|
55
|
-
export function eligibleRouterCandidates(request, peers, requiredParameters, preferences, allowsPeer) {
|
|
56
|
-
const protocol = detectRequestServiceApiProtocol(request);
|
|
57
|
-
const explicitProvider = getExplicitProviderOverride(request);
|
|
58
|
-
const candidates = [];
|
|
59
|
-
const offers = buildNetworkServiceOffers(peers);
|
|
60
|
-
// As in automatic routing, a missing cached-input price lowers a seller's effective reputation
|
|
61
|
-
// only when some seller of the same request does advertise one.
|
|
62
|
-
const preferCachedPricing = offers.some(offer => offer.type === 'text' && offer.cachedInputUsdPerMillion !== undefined);
|
|
63
|
-
for (const offer of offers) {
|
|
64
|
-
// Keep only available, priced text services compatible with the request and required parameters.
|
|
65
|
-
const peer = peers.find(candidate => candidate.peerId === offer.peerId);
|
|
66
|
-
if (!peer || offer.type !== 'text' || (explicitProvider && offer.provider !== explicitProvider))
|
|
67
|
-
continue;
|
|
68
|
-
if (offer.inputUsdPerMillion === undefined || offer.outputUsdPerMillion === undefined)
|
|
69
|
-
continue;
|
|
70
|
-
const plan = resolvePeerRoutePlan(peer, protocol, offer.serviceId, offer.provider, 'strict');
|
|
71
|
-
if (!plan?.serviceId || (requiredParameters.length && plan.selection?.requiresTransform))
|
|
72
|
-
continue;
|
|
73
|
-
if (findMissingRequiredParameters(peer, offer.provider, plan.serviceId, requiredParameters).length)
|
|
74
|
-
continue;
|
|
75
|
-
const rewritten = overrideRoutedModelInBody(request.body, request.headers, plan.serviceId);
|
|
76
|
-
// Check peer policy against the model/provider we would actually send, not the automatic alias.
|
|
77
|
-
const policyRequest = { ...request, body: rewritten.body, headers: { ...rewritten.headers, 'x-antseed-provider': offer.provider } };
|
|
78
|
-
if (!allowsPeer(policyRequest, peer))
|
|
79
|
-
continue;
|
|
80
|
-
const candidate = {
|
|
81
|
-
peer, peerId: peer.peerId, provider: offer.provider, serviceId: plan.serviceId,
|
|
82
|
-
inputUsdPerMillion: offer.inputUsdPerMillion, outputUsdPerMillion: offer.outputUsdPerMillion,
|
|
83
|
-
...(offer.cachedInputUsdPerMillion !== undefined ? { cachedInputUsdPerMillion: offer.cachedInputUsdPerMillion } : {}),
|
|
84
|
-
};
|
|
85
|
-
const effectiveReputationScore = effectiveModelReputationScore(normalizedModelReputationScore(peer), offer.cachedInputUsdPerMillion !== undefined, preferCachedPricing, offer.inputUsdPerMillion === 0 && offer.outputUsdPerMillion === 0);
|
|
86
|
-
if (!preferences || isModelRouteEligible({ ...candidate, effectiveReputationScore }, preferences))
|
|
87
|
-
candidates.push(candidate);
|
|
88
|
-
}
|
|
89
|
-
return candidates;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Keep recommendations that match allowed destinations, in the router's order.
|
|
93
|
-
* Each result names its provider, so a model-only entry becomes one entry per allowed
|
|
94
|
-
* provider and the normal dispatch path can never pick a provider the buyer did not allow.
|
|
95
|
-
* Invalid entries and duplicates are discarded; a suggested `reasoningEffort` is kept but not yet applied.
|
|
96
|
-
*/
|
|
97
|
-
export function resolveRouterRecommendations(routes, candidates) {
|
|
98
|
-
if (!Array.isArray(routes) || routes.length > 512)
|
|
99
|
-
return [];
|
|
100
|
-
const resolved = [];
|
|
101
|
-
const seen = new Set();
|
|
102
|
-
for (const route of routes) {
|
|
103
|
-
if (!route || typeof route.serviceId !== 'string' || !route.serviceId
|
|
104
|
-
|| (route.provider !== undefined && (typeof route.provider !== 'string' || !route.provider.trim()))
|
|
105
|
-
|| (route.peerId !== undefined && (typeof route.peerId !== 'string' || !/^[0-9a-f]{40}$/.test(route.peerId))))
|
|
106
|
-
continue;
|
|
107
|
-
for (const candidate of candidates) {
|
|
108
|
-
if (candidate.serviceId !== route.serviceId || (route.peerId !== undefined && candidate.peerId !== route.peerId)
|
|
109
|
-
|| (route.provider !== undefined && candidate.provider !== route.provider))
|
|
110
|
-
continue;
|
|
111
|
-
const entry = { serviceId: route.serviceId, provider: candidate.provider, ...(route.peerId ? { peerId: route.peerId } : {}),
|
|
112
|
-
...(typeof route.reasoningEffort === 'string' && route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {}) };
|
|
113
|
-
const key = JSON.stringify([entry.peerId ?? null, entry.provider, entry.serviceId]);
|
|
114
|
-
if (seen.has(key))
|
|
115
|
-
continue;
|
|
116
|
-
seen.add(key);
|
|
117
|
-
resolved.push(entry);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return resolved;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Turn one recommendation into an ordinary inference request: set the model, and pin
|
|
124
|
-
* the seller/provider when the router chose them. The normal dispatch path does the rest.
|
|
125
|
-
*/
|
|
126
|
-
export function requestForRecommendation(request, route, requestId) {
|
|
127
|
-
const rewritten = overrideRoutedModelInBody(request.body, request.headers, route.serviceId);
|
|
128
|
-
if (!rewritten.overridden)
|
|
129
|
-
throw new Error('Could not apply router recommendation');
|
|
130
|
-
const headers = { ...rewritten.headers };
|
|
131
|
-
delete headers['x-antseed-pin-peer'];
|
|
132
|
-
delete headers['x-antseed-prefer-peer'];
|
|
133
|
-
if (route.peerId)
|
|
134
|
-
headers['x-antseed-pin-peer'] = route.peerId;
|
|
135
|
-
// Keep the caller's provider restriction unless the router named an exact provider.
|
|
136
|
-
if (route.provider)
|
|
137
|
-
headers['x-antseed-provider'] = route.provider;
|
|
138
|
-
return { ...request, requestId, body: rewritten.body, headers };
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Buy recommendations from the selected routing service and keep only allowed ones.
|
|
142
|
-
* This does not send inference; BuyerProxy dispatches each recommendation normally.
|
|
143
|
-
*/
|
|
144
|
-
export async function executeRouterSelection(args) {
|
|
145
|
-
const { node, client, request, peers, conversationKey, signal } = args;
|
|
146
|
-
const routingService = args.selection.service;
|
|
147
|
-
if (!routingService)
|
|
148
|
-
throw new Error('Select an exact routing-service target');
|
|
149
|
-
const allowedModels = args.selection.allowedModels;
|
|
150
|
-
const modelsCache = args.modelsCache ?? new RoutingModelsCache(0);
|
|
151
|
-
const attempt = async () => {
|
|
152
|
-
const routerModel = routerModelResolver(await untilAborted(signal, modelsCache.get(client, routingService, peers, node)));
|
|
153
|
-
const allowed = args.candidates
|
|
154
|
-
.filter(candidate => allowedModels === undefined || allowedModels.some(model => model.provider === candidate.provider && model.serviceId === candidate.serviceId));
|
|
155
|
-
if (!allowed.length && allowedModels !== undefined)
|
|
156
|
-
throw new NoEligibleCandidatesError('None of the allowed models is in this router’s model allow list.');
|
|
157
|
-
// Send only models the router lists, within its price limits, named the way the router names them.
|
|
158
|
-
const candidates = allowed.flatMap(({ peer: _peer, ...candidate }) => {
|
|
159
|
-
const model = routerModel(candidate.serviceId, candidatePricing(candidate));
|
|
160
|
-
return model ? [{ ...candidate, routerModel: model }] : [];
|
|
161
|
-
});
|
|
162
|
-
if (!candidates.length)
|
|
163
|
-
throw new NoEligibleCandidatesError('None of the allowed models can be ranked by this router.');
|
|
164
|
-
const { costQualityTradeoff } = args.selection;
|
|
165
|
-
const routes = await untilAborted(signal, client.selectRoute(request, peers, {
|
|
166
|
-
signal, conversationKey, candidates, routingService,
|
|
167
|
-
...(costQualityTradeoff !== undefined ? { costQualityTradeoff } : {}),
|
|
168
|
-
// Pay for the recommendation only if it names at least one allowed destination.
|
|
169
|
-
acceptRecommendations: recommendations => resolveRouterRecommendations(recommendations, candidates).length > 0,
|
|
170
|
-
sendRequest: (peer, serviceRequest, options) => {
|
|
171
|
-
if (peer.peerId !== routingService.peerId)
|
|
172
|
-
throw new Error('Routing request must use the selected routing-service peer');
|
|
173
|
-
// Routing and inference are separate purchases, so they need separate billing IDs.
|
|
174
|
-
if (!serviceRequest.requestId || serviceRequest.requestId === request.requestId)
|
|
175
|
-
throw new Error('Routing purchases require a distinct request ID');
|
|
176
|
-
args.onRoutingRequest?.(serviceRequest.requestId);
|
|
177
|
-
return node.sendRequest(peer, serviceRequest, { ...options, signal, ...(args.buyerIdentity ? { buyerIdentity: args.buyerIdentity } : {}) });
|
|
178
|
-
},
|
|
179
|
-
}));
|
|
180
|
-
signal.throwIfAborted();
|
|
181
|
-
const resolved = routes ? resolveRouterRecommendations(routes, candidates) : [];
|
|
182
|
-
if (!resolved.length)
|
|
183
|
-
throw new Error('Selected router returned no eligible recommendation');
|
|
184
|
-
return resolved;
|
|
185
|
-
};
|
|
186
|
-
try {
|
|
187
|
-
return await attempt();
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
// Nothing was left to rank before any call: the cached model list is not at fault, so keep it.
|
|
191
|
-
if (error instanceof NoEligibleCandidatesError)
|
|
192
|
-
throw error;
|
|
193
|
-
modelsCache.invalidate(routingService);
|
|
194
|
-
// IRP 422: our cached model list may be stale. Refresh it and retry once; failed ranks are not charged.
|
|
195
|
-
if (error instanceof RouterCannotRankError) {
|
|
196
|
-
try {
|
|
197
|
-
return await attempt();
|
|
198
|
-
}
|
|
199
|
-
catch (retryError) {
|
|
200
|
-
modelsCache.invalidate(routingService);
|
|
201
|
-
throw retryError;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
throw error;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
/** No candidate is left to rank once the buyer's, the key's and the router's model limits apply; nothing was paid. */
|
|
208
|
-
export class NoEligibleCandidatesError extends Error {
|
|
209
|
-
}
|
|
210
|
-
/** Stop waiting when `signal` aborts, even if the client ignores it (so timeouts always apply). */
|
|
211
|
-
function untilAborted(signal, promise) {
|
|
212
|
-
signal.throwIfAborted();
|
|
213
|
-
let onAbort = () => { };
|
|
214
|
-
const aborted = new Promise((_resolve, reject) => {
|
|
215
|
-
onAbort = () => reject(signal.reason);
|
|
216
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
217
|
-
});
|
|
218
|
-
return Promise.race([promise, aborted]).finally(() => signal.removeEventListener('abort', onAbort));
|
|
219
|
-
}
|
|
220
|
-
//# sourceMappingURL=router-execution.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"router-execution.js","sourceRoot":"","sources":["../../src/proxy/router-execution.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"router-execution.test.d.ts","sourceRoot":"","sources":["../../src/proxy/router-execution.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { DEFAULT_MODEL_ROUTING_PREFERENCES } from '@antseed/node';
|
|
4
|
-
import { RouterCannotRankError } from '@antseed/router-core';
|
|
5
|
-
import { eligibleRouterCandidates, executeRouterSelection, requestForRecommendation, resolveRouterRecommendations, RoutingModelsCache } from './router-execution.js';
|
|
6
|
-
const peer = {
|
|
7
|
-
peerId: 'a'.repeat(40), providers: ['openai'], lastSeen: Date.now(), reputationScore: 90,
|
|
8
|
-
providerPricing: { openai: { defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 2 }, services: { 'model-a': { inputUsdPerMillion: 1, outputUsdPerMillion: 2 } } } },
|
|
9
|
-
providerServiceApiProtocols: { openai: { services: { 'model-a': ['openai-chat-completions'] } } },
|
|
10
|
-
};
|
|
11
|
-
const request = {
|
|
12
|
-
requestId: 'original', method: 'POST', path: '/v1/chat/completions', headers: { 'content-type': 'application/json' },
|
|
13
|
-
body: Buffer.from(JSON.stringify({ model: 'antseed', messages: [{ role: 'user', content: 'Hello' }], stream: true })),
|
|
14
|
-
};
|
|
15
|
-
const candidates = () => eligibleRouterCandidates(request, [peer], [], null, () => true);
|
|
16
|
-
const listAll = async () => [{ id: 'model-a' }, { id: 'model-b' }];
|
|
17
|
-
const routingService = { peerId: peer.peerId, provider: 'routing-vendor', serviceId: 'route' };
|
|
18
|
-
const routerSelection = { kind: 'router', service: routingService };
|
|
19
|
-
const unusedNode = { sendRequest: async () => { throw new Error('unused'); } };
|
|
20
|
-
test('recommendations keep allowed destinations in router order, each naming its provider', () => {
|
|
21
|
-
const first = candidates()[0];
|
|
22
|
-
const available = [first, { ...first, serviceId: 'model-b' }, { ...first, peerId: 'b'.repeat(40), provider: 'other' }];
|
|
23
|
-
assert.deepEqual(resolveRouterRecommendations([{ serviceId: 'model-a', peerId: 'c'.repeat(40) }], available), []);
|
|
24
|
-
assert.deepEqual(resolveRouterRecommendations([{ serviceId: 'model-a', peerId: first.peerId, provider: 'other' }], available), []);
|
|
25
|
-
assert.deepEqual(resolveRouterRecommendations([
|
|
26
|
-
{ serviceId: 'missing' },
|
|
27
|
-
{ serviceId: 'model-b', peerId: first.peerId, provider: 'openai' },
|
|
28
|
-
{ serviceId: 'model-a' },
|
|
29
|
-
{ serviceId: 'model-a', provider: 'openai' },
|
|
30
|
-
], available), [
|
|
31
|
-
{ serviceId: 'model-b', peerId: first.peerId, provider: 'openai' },
|
|
32
|
-
{ serviceId: 'model-a', provider: 'openai' },
|
|
33
|
-
{ serviceId: 'model-a', provider: 'other' },
|
|
34
|
-
]);
|
|
35
|
-
});
|
|
36
|
-
test('a recommendation becomes an ordinary inference request with its own ID', () => {
|
|
37
|
-
const pinned = { ...request, headers: { ...request.headers, 'x-antseed-pin-peer': 'c'.repeat(40), 'x-antseed-prefer-peer': 'd'.repeat(40), 'x-antseed-provider': 'openai' } };
|
|
38
|
-
const exact = requestForRecommendation(pinned, { serviceId: 'model-a', peerId: peer.peerId, provider: 'anthropic' }, 'attempt-1');
|
|
39
|
-
assert.equal(exact.requestId, 'attempt-1');
|
|
40
|
-
assert.equal(exact.headers['x-antseed-pin-peer'], peer.peerId);
|
|
41
|
-
assert.equal(exact.headers['x-antseed-provider'], 'anthropic');
|
|
42
|
-
assert.equal(exact.headers['x-antseed-prefer-peer'], undefined);
|
|
43
|
-
assert.deepEqual(JSON.parse(Buffer.from(exact.body).toString()), { model: 'model-a', messages: [{ role: 'user', content: 'Hello' }], stream: true });
|
|
44
|
-
const modelOnly = requestForRecommendation(pinned, { serviceId: 'model-a' }, 'attempt-2');
|
|
45
|
-
assert.equal(modelOnly.headers['x-antseed-pin-peer'], undefined);
|
|
46
|
-
assert.equal(modelOnly.headers['x-antseed-provider'], 'openai');
|
|
47
|
-
assert.equal(JSON.parse(Buffer.from(request.body).toString()).model, 'antseed');
|
|
48
|
-
});
|
|
49
|
-
test('router model lists restrict candidates, are cached, and refresh once after a 422', async () => {
|
|
50
|
-
let models = ['model-a'];
|
|
51
|
-
let listCalls = 0;
|
|
52
|
-
let unrankableOnce = false;
|
|
53
|
-
const seen = [];
|
|
54
|
-
const client = {
|
|
55
|
-
async listModels() { listCalls++; return models.map(id => ({ id })); },
|
|
56
|
-
async selectRoute(_request, _peers, context) {
|
|
57
|
-
seen.push(context.candidates.map(candidate => candidate.serviceId));
|
|
58
|
-
if (unrankableOnce) {
|
|
59
|
-
unrankableOnce = false;
|
|
60
|
-
throw new RouterCannotRankError('cannot rank');
|
|
61
|
-
}
|
|
62
|
-
return [{ serviceId: 'model-a' }];
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
const modelsCache = new RoutingModelsCache();
|
|
66
|
-
const args = { node: unusedNode, client, request, modelsCache, peers: [peer],
|
|
67
|
-
candidates: [candidates()[0], { ...candidates()[0], serviceId: 'model-b' }], conversationKey: null,
|
|
68
|
-
signal: new AbortController().signal, selection: routerSelection };
|
|
69
|
-
assert.deepEqual(await executeRouterSelection(args), [{ serviceId: 'model-a', provider: 'openai' }]);
|
|
70
|
-
await executeRouterSelection(args);
|
|
71
|
-
assert.equal(listCalls, 1);
|
|
72
|
-
assert.deepEqual(seen, [['model-a'], ['model-a']]);
|
|
73
|
-
unrankableOnce = true;
|
|
74
|
-
assert.deepEqual(await executeRouterSelection(args), [{ serviceId: 'model-a', provider: 'openai' }]);
|
|
75
|
-
assert.equal(listCalls, 2);
|
|
76
|
-
models = ['model-z'];
|
|
77
|
-
const fresh = new RoutingModelsCache();
|
|
78
|
-
await assert.rejects(executeRouterSelection({ ...args, modelsCache: fresh }), /can be ranked by this router/);
|
|
79
|
-
// Nothing to rank is not the model list's fault: the cached list is kept, not refetched.
|
|
80
|
-
const callsBefore = listCalls;
|
|
81
|
-
await assert.rejects(executeRouterSelection({ ...args, modelsCache: fresh }), /can be ranked by this router/);
|
|
82
|
-
assert.equal(listCalls, callsBefore);
|
|
83
|
-
});
|
|
84
|
-
test('a * entry with max_pricing sends only candidates within the limit', async () => {
|
|
85
|
-
const sent = [];
|
|
86
|
-
const client = {
|
|
87
|
-
listModels: async () => [{ id: '*', max_pricing: { input: 0, cache_read: 0, output: 0 } }],
|
|
88
|
-
async selectRoute(_request, _peers, context) {
|
|
89
|
-
sent.push(...context.candidates.map(candidate => candidate.serviceId));
|
|
90
|
-
return [{ serviceId: 'free-model' }];
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
const base = candidates()[0];
|
|
94
|
-
const free = { ...base, serviceId: 'free-model', inputUsdPerMillion: 0, outputUsdPerMillion: 0 };
|
|
95
|
-
const paidCache = { ...free, serviceId: 'paid-cache', cachedInputUsdPerMillion: 0.1 };
|
|
96
|
-
const args = { node: unusedNode, client, request, peers: [peer], conversationKey: null, signal: new AbortController().signal, selection: routerSelection };
|
|
97
|
-
await executeRouterSelection({ ...args, candidates: [base, free, paidCache] });
|
|
98
|
-
assert.deepEqual(sent, ['free-model']);
|
|
99
|
-
await assert.rejects(executeRouterSelection({ ...args, candidates: [base] }), /can be ranked by this router/);
|
|
100
|
-
});
|
|
101
|
-
test('seller model names are sent under the router\'s own name for that model', async () => {
|
|
102
|
-
const sent = [];
|
|
103
|
-
const client = {
|
|
104
|
-
listModels: async () => [{ id: 'anthropic/claude-opus-5' }, { id: 'openai/gpt-5' }],
|
|
105
|
-
async selectRoute(_request, _peers, context) {
|
|
106
|
-
for (const candidate of context.candidates)
|
|
107
|
-
sent.push([candidate.serviceId, candidate.routerModel]);
|
|
108
|
-
return [{ serviceId: 'claude-opus-5' }];
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
const base = candidates()[0];
|
|
112
|
-
const available = [{ ...base, serviceId: 'claude-opus-5' }, { ...base, serviceId: 'openai/gpt-5' }, { ...base, serviceId: 'mistral-large' }];
|
|
113
|
-
assert.deepEqual(await executeRouterSelection({ node: unusedNode, client, request, peers: [peer], candidates: available,
|
|
114
|
-
conversationKey: null, signal: new AbortController().signal, selection: routerSelection }), [{ serviceId: 'claude-opus-5', provider: 'openai' }]);
|
|
115
|
-
assert.deepEqual(sent, [['claude-opus-5', 'anthropic/claude-opus-5'], ['openai/gpt-5', 'openai/gpt-5']]);
|
|
116
|
-
});
|
|
117
|
-
test('concurrent model lists share one request and refresh after invalidation or expiry', async () => {
|
|
118
|
-
let calls = 0;
|
|
119
|
-
let release;
|
|
120
|
-
const gate = new Promise(resolve => { release = resolve; });
|
|
121
|
-
const client = { async listModels() { calls++; await gate; return [{ id: 'model-a' }]; } };
|
|
122
|
-
const modelsCache = new RoutingModelsCache();
|
|
123
|
-
const pending = Promise.all([1, 2, 3].map(() => modelsCache.get(client, routingService, [peer], unusedNode)));
|
|
124
|
-
release();
|
|
125
|
-
const results = await pending;
|
|
126
|
-
assert.equal(calls, 1);
|
|
127
|
-
results[0].push({ id: 'mutated' });
|
|
128
|
-
assert.deepEqual(await modelsCache.get(client, routingService, [peer], unusedNode), [{ id: 'model-a' }]);
|
|
129
|
-
assert.equal(calls, 1);
|
|
130
|
-
modelsCache.invalidate(routingService);
|
|
131
|
-
await modelsCache.get(client, routingService, [peer], unusedNode);
|
|
132
|
-
assert.equal(calls, 2);
|
|
133
|
-
const uncached = new RoutingModelsCache(0);
|
|
134
|
-
await uncached.get(client, routingService, [peer], unusedNode);
|
|
135
|
-
await uncached.get(client, routingService, [peer], unusedNode);
|
|
136
|
-
assert.equal(calls, 4);
|
|
137
|
-
});
|
|
138
|
-
test('model-list requests are free control-plane calls to the selected routing peer', async () => {
|
|
139
|
-
const sent = [];
|
|
140
|
-
const client = {
|
|
141
|
-
async listModels(target, peers, context) {
|
|
142
|
-
await context.sendRequest(peers.find(entry => entry.peerId === target.peerId), { ...request, requestId: 'models', method: 'GET', path: '/v1/routing/models' });
|
|
143
|
-
return [{ id: 'model-a' }];
|
|
144
|
-
},
|
|
145
|
-
async selectRoute() { return [{ serviceId: 'model-a' }]; },
|
|
146
|
-
};
|
|
147
|
-
const node = { sendRequest: async (target, serviceRequest, options) => {
|
|
148
|
-
sent.push({ peerId: target.peerId, options });
|
|
149
|
-
return { requestId: serviceRequest.requestId, statusCode: 200, headers: {}, body: new Uint8Array() };
|
|
150
|
-
} };
|
|
151
|
-
await executeRouterSelection({ node, client, request, peers: [peer], candidates: candidates(), conversationKey: null,
|
|
152
|
-
signal: new AbortController().signal, selection: routerSelection });
|
|
153
|
-
assert.equal(sent.length, 1);
|
|
154
|
-
assert.equal(sent[0].peerId, peer.peerId);
|
|
155
|
-
assert.equal(sent[0].options.controlPlane, true);
|
|
156
|
-
});
|
|
157
|
-
test('model allowlists restrict routing candidates and returned recommendations', async () => {
|
|
158
|
-
const first = candidates()[0];
|
|
159
|
-
const available = [first, { ...first, serviceId: 'model-b' }, { ...first, provider: 'other' }];
|
|
160
|
-
const allowedModels = [{ provider: 'openai', serviceId: 'model-a' }];
|
|
161
|
-
let calls = 0;
|
|
162
|
-
let forbiddenOnly = false;
|
|
163
|
-
const client = { listModels: listAll,
|
|
164
|
-
async selectRoute(_request, _peers, context) {
|
|
165
|
-
calls++;
|
|
166
|
-
assert.deepEqual(context.candidates.map(({ provider, serviceId }) => ({ provider, serviceId })), allowedModels);
|
|
167
|
-
assert.equal(context.acceptRecommendations([{ serviceId: 'model-b' }]), false);
|
|
168
|
-
assert.equal(context.acceptRecommendations([{ serviceId: 'model-a' }]), true);
|
|
169
|
-
return forbiddenOnly ? [{ serviceId: 'model-b' }] : [{ serviceId: 'model-b' }, { serviceId: 'model-a' }];
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
const args = { node: unusedNode, client, request, peers: [peer], candidates: available, conversationKey: null, signal: new AbortController().signal };
|
|
173
|
-
assert.deepEqual(await executeRouterSelection({ ...args, selection: { ...routerSelection, allowedModels } }), [{ serviceId: 'model-a', provider: 'openai' }]);
|
|
174
|
-
forbiddenOnly = true;
|
|
175
|
-
await assert.rejects(executeRouterSelection({ ...args, selection: { ...routerSelection, allowedModels } }), /no eligible recommendation/);
|
|
176
|
-
const before = calls;
|
|
177
|
-
for (const blocked of [[], [{ provider: 'missing', serviceId: 'model-a' }]]) {
|
|
178
|
-
await assert.rejects(executeRouterSelection({ ...args, selection: { ...routerSelection, allowedModels: blocked } }), /model allow list/);
|
|
179
|
-
}
|
|
180
|
-
assert.equal(calls, before);
|
|
181
|
-
});
|
|
182
|
-
test('the selection tradeoff is passed to the routing client, and omitted when unset', async () => {
|
|
183
|
-
const seen = [];
|
|
184
|
-
const client = {
|
|
185
|
-
listModels: async () => [{ id: 'model-a' }],
|
|
186
|
-
async selectRoute(_request, _peers, context) {
|
|
187
|
-
seen.push(context.costQualityTradeoff);
|
|
188
|
-
assert.equal('costQualityTradeoff' in context, context.costQualityTradeoff !== undefined);
|
|
189
|
-
return [{ serviceId: 'model-a' }];
|
|
190
|
-
},
|
|
191
|
-
};
|
|
192
|
-
const args = { node: unusedNode, client, request, peers: [peer], candidates: candidates(), conversationKey: null, signal: new AbortController().signal };
|
|
193
|
-
await executeRouterSelection({ ...args, selection: { ...routerSelection, costQualityTradeoff: 0 } });
|
|
194
|
-
await executeRouterSelection({ ...args, selection: routerSelection });
|
|
195
|
-
assert.deepEqual(seen, [0, undefined]);
|
|
196
|
-
});
|
|
197
|
-
test('a declined recommendation fails closed and cancellation applies even if the client ignores it', async () => {
|
|
198
|
-
const client = { listModels: listAll, selectRoute: async () => null };
|
|
199
|
-
const args = { node: unusedNode, client, request, peers: [peer], candidates: candidates(), conversationKey: null, signal: new AbortController().signal, selection: routerSelection };
|
|
200
|
-
await assert.rejects(executeRouterSelection(args), /no eligible/);
|
|
201
|
-
const abort = new AbortController();
|
|
202
|
-
client.selectRoute = () => new Promise(() => { });
|
|
203
|
-
const pending = executeRouterSelection({ ...args, signal: abort.signal });
|
|
204
|
-
abort.abort(new Error('client disconnected'));
|
|
205
|
-
await assert.rejects(pending, /client disconnected/);
|
|
206
|
-
});
|
|
207
|
-
test('routing purchases cannot substitute a different routing-service peer', async () => {
|
|
208
|
-
let sent = false;
|
|
209
|
-
const client = {
|
|
210
|
-
listModels: listAll,
|
|
211
|
-
async selectRoute(_request, _peers, context) {
|
|
212
|
-
await context.sendRequest(peer, { ...request, requestId: 'routing-request' }, {});
|
|
213
|
-
return [{ serviceId: 'model-a' }];
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
await assert.rejects(executeRouterSelection({
|
|
217
|
-
node: { sendRequest: async () => { sent = true; throw new Error('must not send'); } }, client, request,
|
|
218
|
-
peers: [peer], candidates: candidates(), conversationKey: null, signal: new AbortController().signal,
|
|
219
|
-
selection: { kind: 'router', service: { ...routingService, peerId: 'b'.repeat(40) } },
|
|
220
|
-
}), /selected routing-service peer/);
|
|
221
|
-
assert.equal(sent, false);
|
|
222
|
-
});
|
|
223
|
-
test('candidate construction enforces buyer restrictions and required parameters', () => {
|
|
224
|
-
assert.equal(eligibleRouterCandidates(request, [peer], [], null, () => false).length, 0);
|
|
225
|
-
assert.equal(eligibleRouterCandidates(request, [peer], ['tools'], null, () => true).length, 0);
|
|
226
|
-
});
|
|
227
|
-
test('candidate construction applies buyer routing preferences like automatic routing', () => {
|
|
228
|
-
// The default preferences (min trust 60) keep a reputable seller: its reputation is read from the peer.
|
|
229
|
-
assert.equal(eligibleRouterCandidates(request, [peer], [], DEFAULT_MODEL_ROUTING_PREFERENCES, () => true).length, 1);
|
|
230
|
-
// Trust below the minimum, a block list and an allow list exclude.
|
|
231
|
-
assert.equal(eligibleRouterCandidates(request, [peer], [], { ...DEFAULT_MODEL_ROUTING_PREFERENCES, minTrustScore: 95 }, () => true).length, 0);
|
|
232
|
-
assert.equal(eligibleRouterCandidates(request, [{ ...peer, reputationScore: undefined }], [], DEFAULT_MODEL_ROUTING_PREFERENCES, () => true).length, 0);
|
|
233
|
-
assert.equal(eligibleRouterCandidates(request, [peer], [], { ...DEFAULT_MODEL_ROUTING_PREFERENCES, blockedPeerIds: [peer.peerId] }, () => true).length, 0);
|
|
234
|
-
assert.equal(eligibleRouterCandidates(request, [peer], [], { ...DEFAULT_MODEL_ROUTING_PREFERENCES, allowedPeerIds: ['b'.repeat(40)] }, () => true).length, 0);
|
|
235
|
-
// The soft input-price limit only ranks automatic routing; it never excludes a router candidate.
|
|
236
|
-
assert.equal(eligibleRouterCandidates(request, [peer], [], { ...DEFAULT_MODEL_ROUTING_PREFERENCES, maxInputUsdPerMillion: 0.5 }, () => true).length, 1);
|
|
237
|
-
});
|
|
238
|
-
test('routing purchases are registered before dispatch with their own request ID', async () => {
|
|
239
|
-
const tracked = [];
|
|
240
|
-
const client = {
|
|
241
|
-
listModels: listAll,
|
|
242
|
-
async selectRoute(_request, _peers, context) {
|
|
243
|
-
await context.sendRequest(peer, { ...request, requestId: 'routing-purchase' }, {});
|
|
244
|
-
return [{ serviceId: 'model-a' }];
|
|
245
|
-
},
|
|
246
|
-
};
|
|
247
|
-
await executeRouterSelection({
|
|
248
|
-
node: { sendRequest: async (_peer, serviceRequest) => {
|
|
249
|
-
assert.deepEqual(tracked, ['routing-purchase']);
|
|
250
|
-
assert.equal(serviceRequest.requestId, 'routing-purchase');
|
|
251
|
-
return { requestId: serviceRequest.requestId, statusCode: 200, headers: {}, body: new Uint8Array() };
|
|
252
|
-
} }, client, request, peers: [peer], candidates: candidates(), conversationKey: 'chat',
|
|
253
|
-
signal: new AbortController().signal, onRoutingRequest: requestId => tracked.push(requestId), selection: routerSelection,
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
test('routing purchases cannot reuse the parent inference request ID', async () => {
|
|
257
|
-
const client = {
|
|
258
|
-
listModels: listAll,
|
|
259
|
-
async selectRoute(_request, _peers, context) {
|
|
260
|
-
await context.sendRequest(peer, request, {});
|
|
261
|
-
return [{ serviceId: 'model-a' }];
|
|
262
|
-
},
|
|
263
|
-
};
|
|
264
|
-
await assert.rejects(executeRouterSelection({
|
|
265
|
-
node: { sendRequest: async () => { throw new Error('must not dispatch'); } }, client, request,
|
|
266
|
-
peers: [peer], candidates: candidates(), conversationKey: null, signal: new AbortController().signal, selection: routerSelection,
|
|
267
|
-
}), /distinct request ID/);
|
|
268
|
-
});
|
|
269
|
-
//# sourceMappingURL=router-execution.test.js.map
|