@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
|
@@ -4,24 +4,21 @@ import { prepareVideoRequest, recordVideoAcceptance } from './native-video-proxy
|
|
|
4
4
|
import { downloadVideo } from './video-download.js';
|
|
5
5
|
import { createServer } from 'node:http';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
|
-
import { buildRoutingServices, findPinnedRoutingService } from './routing-services.js';
|
|
8
7
|
import { readFileSync, watchFile, unwatchFile } from 'node:fs';
|
|
9
8
|
import { readFile, writeFile, rename, mkdir, readdir, stat, unlink } from 'node:fs/promises';
|
|
10
9
|
import { join } from 'node:path';
|
|
11
|
-
import { ANTSEED_BUYER_FAULT_ERROR_CODE, ANTSEED_FAULT_ATTRIBUTION_HEADER, ANTSEED_ATTEST_PATH, adaptPeerFaultErrorResponse, computeTrustScore, decodeSweepRequest, faultAttributionOf, faultCodeOf, isModelRouteCoolingDown, isModelRouteEligible, modelRouteReputationScore, modelRouteTotalPrice, normalizedModelReputationScore, peerSupportsCooperativeClose,
|
|
10
|
+
import { ANTSEED_BUYER_FAULT_ERROR_CODE, ANTSEED_FAULT_ATTRIBUTION_HEADER, ANTSEED_ATTEST_PATH, adaptPeerFaultErrorResponse, computeTrustScore, decodeSweepRequest, faultAttributionOf, faultCodeOf, isModelRouteCoolingDown, isModelRouteEligible, modelRouteReputationScore, modelRouteTotalPrice, normalizedModelReputationScore, peerSupportsCooperativeClose, DEFAULT_BUYER_IDENTITY, rankModelRoutes, sanitizePeerDisplayName, } from '@antseed/node';
|
|
12
11
|
import { canonicalModelKey } from '@antseed/node/model-identity';
|
|
13
12
|
import { createStreamingAdapter, detectRequestServiceApiProtocol, transformRequest, transformResponse, } from './service-api-adapter.js';
|
|
14
|
-
import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, parsePeerPinnedService, rewritePeerPinnedServiceInBody, substituteRoutedModelAlias, overrideRoutedModelInBody, ROUTED_MODEL_ALIAS, SYSTEM_PROXY_SOURCE_HEADER, SYSTEM_ROUTED_MODEL_HEADER, BUYER_IDENTITY_HEADER, normalizePeerId,
|
|
13
|
+
import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, parsePeerPinnedService, rewritePeerPinnedServiceInBody, substituteRoutedModelAlias, overrideRoutedModelInBody, ROUTED_MODEL_ALIAS, SYSTEM_PROXY_SOURCE_HEADER, SYSTEM_ROUTED_MODEL_HEADER, BUYER_IDENTITY_HEADER, normalizePeerId, } from './request-utils.js';
|
|
15
14
|
import { BuyerIdentityLoader } from '../buyer-identities/loader.js';
|
|
16
15
|
import { buildNetworkModels, effectiveModelReputationScore, parseModelTypeFilter, } from './network-models.js';
|
|
17
16
|
import { findMissingRequiredParameters, findUnannouncedRequestParameters, findAdvertisedServiceOffer, findAdvertisedServiceProtocols, getExplicitProviderOverride, getExplicitPeerIdOverride, parseRequiredParametersHeader, REQUIRED_PARAMETERS_HEADER, resolvePeerRoutePlan, selectCandidatePeersForRouting, } from './routing.js';
|
|
18
17
|
import { computeResponseTelemetry, attachAntseedTelemetryHeaders, attachStreamingAntseedHeaders, } from './telemetry.js';
|
|
19
18
|
import { DEFAULT_BUYER_PEER_REFRESH_INTERVAL_MS } from '../config/defaults.js';
|
|
20
19
|
import { extractConversationIdentity, extractFirstUserSnippet, isCompletionRequestPath, isTitleGenerationRequest, parseRequestBodyObject, } from './conversation-identity.js';
|
|
21
|
-
import { ConversationStore } from './conversation-store.js';
|
|
22
|
-
import { eligibleRouterCandidates, executeRouterSelection, NoEligibleCandidatesError, requestForRecommendation, RoutingModelsCache } from './router-execution.js';
|
|
23
|
-
import { recordRouterUsage } from './routing-usage.js';
|
|
24
20
|
import { SPEND_ATTRIBUTION_DB_FILE, SPEND_ATTRIBUTION_HEADER, SpendAttributionFeed, parseSpendAttributionTag } from './spend-attribution.js';
|
|
21
|
+
import { ConversationStore } from './conversation-store.js';
|
|
25
22
|
import { DEPOSIT_WATCH_ACTIVE_TTL_MS } from './deposit-watcher.js';
|
|
26
23
|
import { DepositLog } from './deposit-log.js';
|
|
27
24
|
import { recordPeerFailureEntry, clearPeerHealthEntry, isCoolingDown, parsePersistedPeerHealth, prunePeerHealth, serializePeerHealth, reasonEscalates, } from './peer-health.js';
|
|
@@ -35,9 +32,8 @@ import { TeeControl } from './tee-control.js';
|
|
|
35
32
|
import { ChainReadCache } from './chain-read-cache.js';
|
|
36
33
|
import { isClientCredentialHeader } from './credential-headers.js';
|
|
37
34
|
import { loadConfig } from '../config/loader.js';
|
|
38
|
-
import { ModelRoutingClient } from '@antseed/router-core';
|
|
39
35
|
import { GATEWAY_CONTROL_HEADER, GATEWAY_CONTROL_SECRET_ENV, GATEWAY_CONTROL_SECRET_FILE, ROUTING_POLICY_HEADER, decodePolicyHeader, findModelRoute, meaningfulPolicy, narrowPolicy, normalizePeerId as normalizePolicyPeerId, policyAllowsModel, policyModelKey, } from '../routing-policy/policy.js';
|
|
40
|
-
import { buyerConfigReason, buyerHardPolicy, buyerPreferencePolicy, orderByPolicy, pinnedPeerExclusionReasons, policyExclusionReasons, preferenceExclusionReasons, rankingPreferences, secretsMatch,
|
|
36
|
+
import { buyerConfigReason, buyerHardPolicy, buyerPreferencePolicy, orderByPolicy, pinnedPeerExclusionReasons, policyExclusionReasons, preferenceExclusionReasons, rankingPreferences, secretsMatch, } from './route-policy.js';
|
|
41
37
|
// Re-export for backward compatibility (used by tests and other consumers)
|
|
42
38
|
export { selectCandidatePeersForRouting } from './routing.js';
|
|
43
39
|
export { parsePeerPinnedService, rewritePeerPinnedServiceInBody, substituteRoutedModelAlias, ROUTED_MODEL_ALIAS } from './request-utils.js';
|
|
@@ -95,8 +91,6 @@ function isLoopbackAddress(address) {
|
|
|
95
91
|
// because model_not_found already has dedicated unadvertise handling while a
|
|
96
92
|
// generic 404 would repeat identically on every peer.
|
|
97
93
|
const RETRYABLE_STATUS_CODES = new Set([401, 403, 408, 429, 500, 502, 503, 504]);
|
|
98
|
-
// Moving to another recommendation is a new purchase, so skip timeouts (408/504), where the seller may already have done the work.
|
|
99
|
-
const RECOMMENDATION_FALLBACK_STATUS_CODES = new Set([401, 403, 429, 500, 502, 503]);
|
|
100
94
|
/**
|
|
101
95
|
* Gateway-policy requests on automatic routing also move on after a 402: the
|
|
102
96
|
* buyer could not fund that seller (no deposits for its reserve), but a free
|
|
@@ -153,10 +147,6 @@ function isValidRoutedModelTarget(value) {
|
|
|
153
147
|
return false;
|
|
154
148
|
return !value.includes('@') || parsePeerPinnedService(value) !== null;
|
|
155
149
|
}
|
|
156
|
-
function sameRoutingServiceTarget(a, b) {
|
|
157
|
-
return !!a && !!b && a.peerId.toLowerCase() === b.peerId.toLowerCase()
|
|
158
|
-
&& a.provider === b.provider && a.serviceId === b.serviceId;
|
|
159
|
-
}
|
|
160
150
|
/** Returns `request` with its body's model field rewritten to `serviceId`, or unchanged if nothing rewrote. */
|
|
161
151
|
function withRoutedModel(request, serviceId) {
|
|
162
152
|
const rewritten = overrideRoutedModelInBody(request.body, request.headers, serviceId);
|
|
@@ -264,18 +254,6 @@ function responseFaultAttribution(response) {
|
|
|
264
254
|
? attribution
|
|
265
255
|
: 'peer';
|
|
266
256
|
}
|
|
267
|
-
function dispatchFailure(statusCode, responseHeaders, body) {
|
|
268
|
-
return { done: false, statusCode, responseHeaders, responseBody: Buffer.from(body), errorMessage: null };
|
|
269
|
-
}
|
|
270
|
-
function writeDispatchFailure(res, result) {
|
|
271
|
-
if (res.headersSent || res.writableEnded)
|
|
272
|
-
return;
|
|
273
|
-
res.writeHead(result.statusCode, result.responseHeaders);
|
|
274
|
-
res.end(result.responseBody);
|
|
275
|
-
}
|
|
276
|
-
function isBuyerFault(result) {
|
|
277
|
-
return result.responseHeaders[ANTSEED_FAULT_ATTRIBUTION_HEADER]?.toLowerCase() === 'buyer';
|
|
278
|
-
}
|
|
279
257
|
function adaptOpenAICompatibleErrorResponse(response, requestProtocol) {
|
|
280
258
|
if (response.statusCode !== 402) {
|
|
281
259
|
return response;
|
|
@@ -745,14 +723,12 @@ export class BuyerProxy {
|
|
|
745
723
|
_configFileWatching = false;
|
|
746
724
|
_pinnedPeer;
|
|
747
725
|
/**
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
726
|
+
* Route substituted for the `antseed` model alias (`<service>` for automatic
|
|
727
|
+
* peer selection, or `<peerId>@<service>` for an explicit seller pin).
|
|
728
|
+
* Set via `POST /_antseed/route` (the desktop keeps it on the current VPR
|
|
729
|
+
* selection) and persisted in buyer.state.json like the session peer pin.
|
|
751
730
|
*/
|
|
752
731
|
_defaultRoutedModel = null;
|
|
753
|
-
_defaultRouter = null;
|
|
754
|
-
_routingModels = new RoutingModelsCache();
|
|
755
|
-
_modelRoutingClient;
|
|
756
732
|
_conversations;
|
|
757
733
|
/**
|
|
758
734
|
* Wall-clock of the last model-request activity (dispatch or streamed
|
|
@@ -842,7 +818,6 @@ export class BuyerProxy {
|
|
|
842
818
|
this._restartAllowedAt = Date.now() + (config.minRestartIntervalMs ?? MIN_RESTART_INTERVAL_MS);
|
|
843
819
|
this._settingOverrides = [...(config.settingOverrides ?? [])];
|
|
844
820
|
this._node = config.node;
|
|
845
|
-
this._modelRoutingClient = config.modelRoutingClient ?? new ModelRoutingClient();
|
|
846
821
|
this._verifier = config.verifier;
|
|
847
822
|
this._buyerIdentities = config.buyerIdentities ?? new BuyerIdentityLoader(config.node, config.dataDir);
|
|
848
823
|
this._teeVerification = new TeeVerification(config.verifier);
|
|
@@ -913,15 +888,13 @@ export class BuyerProxy {
|
|
|
913
888
|
const entry = this._requestConversations.get(event.requestId);
|
|
914
889
|
if (!entry)
|
|
915
890
|
return;
|
|
916
|
-
const routing = entry.purpose === 'routing';
|
|
917
891
|
this._conversations.addSpend(entry.convId, {
|
|
918
892
|
amountUsdc: event.amountUsdc,
|
|
919
893
|
inputTokens: event.inputTokens,
|
|
920
894
|
cachedInputTokens: event.cachedInputTokens,
|
|
921
895
|
outputTokens: event.outputTokens,
|
|
922
|
-
}, !
|
|
923
|
-
|
|
924
|
-
entry.counted = true;
|
|
896
|
+
}, !entry.counted);
|
|
897
|
+
entry.counted = true;
|
|
925
898
|
}
|
|
926
899
|
/**
|
|
927
900
|
* Identity named by a request: null for the default, the name when it is
|
|
@@ -1466,12 +1439,8 @@ export class BuyerProxy {
|
|
|
1466
1439
|
* a grace window — the seller-initiated auth path can land just after the
|
|
1467
1440
|
* response is returned. Bounded so a leaked id can't grow the map forever.
|
|
1468
1441
|
*/
|
|
1469
|
-
_trackRequestConversation(requestId, convId
|
|
1470
|
-
|
|
1471
|
-
throw new Error('Request ID already tracked');
|
|
1472
|
-
// Inference retries share one turn counter; recommendation purchases have their own cost entry.
|
|
1473
|
-
const parent = !purpose && parentRequestId ? this._requestConversations.get(parentRequestId) : undefined;
|
|
1474
|
-
this._requestConversations.set(requestId, parent?.convId === convId ? parent : { convId, counted: false, purpose });
|
|
1442
|
+
_trackRequestConversation(requestId, convId) {
|
|
1443
|
+
this._requestConversations.set(requestId, { convId, counted: false });
|
|
1475
1444
|
while (this._requestConversations.size > MAX_TRACKED_REQUEST_CONVERSATIONS) {
|
|
1476
1445
|
const oldest = this._requestConversations.keys().next().value;
|
|
1477
1446
|
if (oldest === undefined)
|
|
@@ -1648,27 +1617,6 @@ export class BuyerProxy {
|
|
|
1648
1617
|
// watcher setup failed; non-fatal
|
|
1649
1618
|
}
|
|
1650
1619
|
}
|
|
1651
|
-
_validateRouterSelection(value) {
|
|
1652
|
-
if (!isRoutingSelection(value) || value.kind !== 'router')
|
|
1653
|
-
throw new Error('Invalid router selection');
|
|
1654
|
-
if (!value.service)
|
|
1655
|
-
throw new Error('Select an exact routing-service target');
|
|
1656
|
-
}
|
|
1657
|
-
/** Parses the `router` field of `POST /_antseed/route` (`{ service, costQualityTradeoff?, allowedModels? }`). */
|
|
1658
|
-
_parseDefaultRouter(value) {
|
|
1659
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1660
|
-
throw new Error('router must be { service, costQualityTradeoff? }');
|
|
1661
|
-
const selection = { ...value, kind: 'router' };
|
|
1662
|
-
this._validateRouterSelection(selection);
|
|
1663
|
-
return structuredClone(selection);
|
|
1664
|
-
}
|
|
1665
|
-
/** The default router in its wire form, without the internal `kind` tag. */
|
|
1666
|
-
get _defaultRouterJson() {
|
|
1667
|
-
if (!this._defaultRouter)
|
|
1668
|
-
return null;
|
|
1669
|
-
const { kind: _kind, ...router } = this._defaultRouter;
|
|
1670
|
-
return router;
|
|
1671
|
-
}
|
|
1672
1620
|
async _reloadSessionOverrides(opts = {}) {
|
|
1673
1621
|
try {
|
|
1674
1622
|
const raw = await readFile(this._stateFile, 'utf-8');
|
|
@@ -1681,16 +1629,7 @@ export class BuyerProxy {
|
|
|
1681
1629
|
}
|
|
1682
1630
|
const routedModel = typeof parsed.defaultRoutedModel === 'string' ? parsed.defaultRoutedModel.trim() : '';
|
|
1683
1631
|
this._defaultRoutedModel = routedModel.length > 0 && isValidRoutedModelTarget(routedModel) ? routedModel : null;
|
|
1684
|
-
|
|
1685
|
-
this._defaultRouter = null;
|
|
1686
|
-
if (parsed.defaultRouter != null && this._defaultRoutedModel === null) {
|
|
1687
|
-
this._defaultRouter = this._parseDefaultRouter(parsed.defaultRouter);
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
catch {
|
|
1691
|
-
this._defaultRouter = null;
|
|
1692
|
-
}
|
|
1693
|
-
log(`Session overrides reloaded: peer=${this._pinnedPeer ?? 'none'} route=${this._defaultRoutedModel ?? 'none'} router=${JSON.stringify(this._defaultRouterJson)}`);
|
|
1632
|
+
log(`Session overrides reloaded: peer=${this._pinnedPeer ?? 'none'} route=${this._defaultRoutedModel ?? 'none'}`);
|
|
1694
1633
|
}
|
|
1695
1634
|
catch {
|
|
1696
1635
|
// state file unreadable; keep current values
|
|
@@ -1769,7 +1708,7 @@ export class BuyerProxy {
|
|
|
1769
1708
|
// in the file — the debounce may have been cancelled before
|
|
1770
1709
|
// _reloadSessionOverrides could commit the latest CLI-written values.
|
|
1771
1710
|
const sessionOverrides = state === 'connected'
|
|
1772
|
-
? { pinnedPeerId: this._pinnedPeer, defaultRoutedModel: this._defaultRoutedModel
|
|
1711
|
+
? { pinnedPeerId: this._pinnedPeer, defaultRoutedModel: this._defaultRoutedModel }
|
|
1773
1712
|
: {};
|
|
1774
1713
|
await this._mergeStateFile({
|
|
1775
1714
|
state,
|
|
@@ -2315,14 +2254,9 @@ export class BuyerProxy {
|
|
|
2315
2254
|
res.end(JSON.stringify({ ok: true, peerId: normalized }));
|
|
2316
2255
|
return;
|
|
2317
2256
|
}
|
|
2318
|
-
if (path === '/_antseed/routing-services' && method === 'GET') {
|
|
2319
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
2320
|
-
res.end(JSON.stringify({ ok: true, services: await buildRoutingServices(await this._getPeers(), this._modelRoutingClient, this._node, this._routingModels) }));
|
|
2321
|
-
return;
|
|
2322
|
-
}
|
|
2323
2257
|
if (path === '/_antseed/route' && method === 'GET') {
|
|
2324
2258
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
2325
|
-
res.end(JSON.stringify({ ok: true, model: this._defaultRoutedModel
|
|
2259
|
+
res.end(JSON.stringify({ ok: true, model: this._defaultRoutedModel }));
|
|
2326
2260
|
return;
|
|
2327
2261
|
}
|
|
2328
2262
|
if (path === '/_antseed/route' && method === 'POST') {
|
|
@@ -2337,10 +2271,9 @@ export class BuyerProxy {
|
|
|
2337
2271
|
}
|
|
2338
2272
|
chunks.push(chunk);
|
|
2339
2273
|
}
|
|
2340
|
-
let body;
|
|
2341
2274
|
let model;
|
|
2342
2275
|
try {
|
|
2343
|
-
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
2276
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
2344
2277
|
model = typeof body.model === 'string' ? body.model.trim() : '';
|
|
2345
2278
|
}
|
|
2346
2279
|
catch {
|
|
@@ -2348,28 +2281,16 @@ export class BuyerProxy {
|
|
|
2348
2281
|
res.end(JSON.stringify({ ok: false, error: 'Invalid JSON body' }));
|
|
2349
2282
|
return;
|
|
2350
2283
|
}
|
|
2351
|
-
|
|
2352
|
-
try {
|
|
2353
|
-
if (body.router != null) {
|
|
2354
|
-
if (model.length > 0)
|
|
2355
|
-
throw new Error('Set model or router, not both');
|
|
2356
|
-
router = this._parseDefaultRouter(body.router);
|
|
2357
|
-
}
|
|
2358
|
-
if (model.length > 0 && !isValidRoutedModelTarget(model)) {
|
|
2359
|
-
throw new Error('model must be "<service>", "<peerId>@<service>", or empty to clear');
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
catch (error) {
|
|
2284
|
+
if (model.length > 0 && !isValidRoutedModelTarget(model)) {
|
|
2363
2285
|
res.writeHead(400, { 'content-type': 'application/json' });
|
|
2364
|
-
res.end(JSON.stringify({ ok: false, error:
|
|
2286
|
+
res.end(JSON.stringify({ ok: false, error: 'model must be "<service>", "<peerId>@<service>", or empty to clear' }));
|
|
2365
2287
|
return;
|
|
2366
2288
|
}
|
|
2367
2289
|
this._defaultRoutedModel = model.length > 0 ? model : null;
|
|
2368
|
-
this.
|
|
2369
|
-
|
|
2370
|
-
log(`Default route set: model=${this._defaultRoutedModel ?? 'none'} router=${JSON.stringify(this._defaultRouterJson)}`);
|
|
2290
|
+
await this._mergeStateFile({ defaultRoutedModel: this._defaultRoutedModel });
|
|
2291
|
+
log(`Default routed model set: ${this._defaultRoutedModel ?? 'none'}`);
|
|
2371
2292
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
2372
|
-
res.end(JSON.stringify({ ok: true, model: this._defaultRoutedModel
|
|
2293
|
+
res.end(JSON.stringify({ ok: true, model: this._defaultRoutedModel }));
|
|
2373
2294
|
return;
|
|
2374
2295
|
}
|
|
2375
2296
|
if (path === '/_antseed/conversations' && method === 'GET') {
|
|
@@ -2410,8 +2331,6 @@ export class BuyerProxy {
|
|
|
2410
2331
|
let parsed;
|
|
2411
2332
|
try {
|
|
2412
2333
|
parsed = JSON.parse(Buffer.concat(chunks).toString());
|
|
2413
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
2414
|
-
throw new Error('Invalid conversation update');
|
|
2415
2334
|
}
|
|
2416
2335
|
catch {
|
|
2417
2336
|
res.writeHead(400, { 'content-type': 'application/json' });
|
|
@@ -2436,21 +2355,6 @@ export class BuyerProxy {
|
|
|
2436
2355
|
res.end(JSON.stringify({ ok: false, error: 'Unknown conversation' }));
|
|
2437
2356
|
return;
|
|
2438
2357
|
}
|
|
2439
|
-
if ('routingSelection' in parsed) {
|
|
2440
|
-
try {
|
|
2441
|
-
if ('pinnedModel' in parsed)
|
|
2442
|
-
throw new Error('Update routingSelection or pinnedModel, not both');
|
|
2443
|
-
if (parsed.routingSelection !== null)
|
|
2444
|
-
this._validateRouterSelection(parsed.routingSelection);
|
|
2445
|
-
}
|
|
2446
|
-
catch (error) {
|
|
2447
|
-
res.writeHead(400, { 'content-type': 'application/json' });
|
|
2448
|
-
res.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
2449
|
-
return;
|
|
2450
|
-
}
|
|
2451
|
-
const selection = parsed.routingSelection;
|
|
2452
|
-
conversation = this._conversations.setRoutingSelection(id, selection);
|
|
2453
|
-
}
|
|
2454
2358
|
if ('pinnedModel' in parsed) {
|
|
2455
2359
|
const pin = typeof parsed.pinnedModel === 'string' ? parsed.pinnedModel.trim() : '';
|
|
2456
2360
|
if (pin.length > 0 && !isValidRoutedModelTarget(pin)) {
|
|
@@ -2826,64 +2730,18 @@ export class BuyerProxy {
|
|
|
2826
2730
|
res.writeHead(400, responseHeaders);
|
|
2827
2731
|
res.end(JSON.stringify({
|
|
2828
2732
|
error: {
|
|
2829
|
-
message: `Unknown model type "${url.searchParams.get('type') ?? ''}" — expected "text", "images", "decisions",
|
|
2733
|
+
message: `Unknown model type "${url.searchParams.get('type') ?? ''}" — expected "text", "images", "decisions", or "videos".`,
|
|
2830
2734
|
type: 'invalid_request_error',
|
|
2831
2735
|
param: 'type',
|
|
2832
2736
|
},
|
|
2833
2737
|
}));
|
|
2834
2738
|
return;
|
|
2835
2739
|
}
|
|
2836
|
-
if (typeFilter === 'router') {
|
|
2837
|
-
const routers = await this._networkRouters(peers, requestPolicy);
|
|
2838
|
-
log(`GET /v1/models?type=router answered locally: ${routers.length} routers`);
|
|
2839
|
-
res.writeHead(200, responseHeaders);
|
|
2840
|
-
res.end(JSON.stringify({ object: 'list', data: routers }));
|
|
2841
|
-
return;
|
|
2842
|
-
}
|
|
2843
2740
|
const data = typeFilter === 'all' ? models : models.filter((entry) => entry.type === typeFilter);
|
|
2844
2741
|
log(`GET /v1/models answered locally: ${data.length} models across ${peers.length} peers${typeFilter ? ` (type=${typeFilter})` : ''}`);
|
|
2845
2742
|
res.writeHead(200, responseHeaders);
|
|
2846
2743
|
res.end(JSON.stringify({ object: 'list', data }));
|
|
2847
2744
|
}
|
|
2848
|
-
/**
|
|
2849
|
-
* Routing services on the network, one entry per service id with each
|
|
2850
|
-
* router peer offering it. A router is listed only when a request could pay
|
|
2851
|
-
* it (see `_routerExclusionReasons`), and under a gateway policy its models
|
|
2852
|
-
* are cut to the ones the policy allows. Model lists are free calls, cached
|
|
2853
|
-
* per router.
|
|
2854
|
-
*/
|
|
2855
|
-
async _networkRouters(peers, requestPolicy) {
|
|
2856
|
-
const policy = this._effectivePolicy(requestPolicy);
|
|
2857
|
-
const services = await buildRoutingServices(peers, this._modelRoutingClient, this._node, this._routingModels);
|
|
2858
|
-
const created = Math.floor(this._now() / 1000);
|
|
2859
|
-
const byId = new Map();
|
|
2860
|
-
for (const service of services) {
|
|
2861
|
-
const peer = peers.find((candidate) => candidate.peerId === service.peerId);
|
|
2862
|
-
if (!peer || this._routerExclusionReasons(peer, service.serviceId, requestPolicy).length > 0)
|
|
2863
|
-
continue;
|
|
2864
|
-
const models = service.catalog
|
|
2865
|
-
? [...new Set(service.catalog.models.map((model) => model.serviceId))]
|
|
2866
|
-
.filter((model) => !policy || policyAllowsModel(policy, model))
|
|
2867
|
-
.sort((a, b) => a.localeCompare(b))
|
|
2868
|
-
: null;
|
|
2869
|
-
const price = Number(service.priceMicroUsdc);
|
|
2870
|
-
let entry = byId.get(service.serviceId);
|
|
2871
|
-
if (!entry) {
|
|
2872
|
-
entry = { id: service.serviceId, name: service.label, object: 'model', created, owned_by: 'antseed', type: 'router', peers: [] };
|
|
2873
|
-
byId.set(service.serviceId, entry);
|
|
2874
|
-
}
|
|
2875
|
-
entry.peers.push({
|
|
2876
|
-
peerId: service.peerId,
|
|
2877
|
-
...(service.sellerName ? { displayName: service.sellerName } : {}),
|
|
2878
|
-
provider: service.provider,
|
|
2879
|
-
serviceId: service.serviceId,
|
|
2880
|
-
model: `${service.peerId}@${service.serviceId}`,
|
|
2881
|
-
priceUsdPerRequest: Number.isFinite(price) ? price / 1_000_000 : null,
|
|
2882
|
-
models,
|
|
2883
|
-
});
|
|
2884
|
-
}
|
|
2885
|
-
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
2886
|
-
}
|
|
2887
2745
|
/**
|
|
2888
2746
|
* Whether a request under the gateway's policy could use this offer, by
|
|
2889
2747
|
* the checks that judge a pinned `peer@model`: the model allow list, the
|
|
@@ -3053,19 +2911,6 @@ export class BuyerProxy {
|
|
|
3053
2911
|
}));
|
|
3054
2912
|
return;
|
|
3055
2913
|
}
|
|
3056
|
-
// Per-request routing options (`routing` in the body) are read here and
|
|
3057
|
-
// stripped, so they never reach a seller, whatever the model. A malformed
|
|
3058
|
-
// `routing` is reported only when a router serves the request.
|
|
3059
|
-
let routingOptions = {};
|
|
3060
|
-
let invalidRouting = null;
|
|
3061
|
-
if (method === 'POST' && isCompletionRequestPath(path)) {
|
|
3062
|
-
const taken = takeRoutingOptions(serializedReq);
|
|
3063
|
-
if (taken.kind === 'options') {
|
|
3064
|
-
serializedReq = taken.request;
|
|
3065
|
-
routingOptions = taken.options;
|
|
3066
|
-
invalidRouting = taken.invalid ?? null;
|
|
3067
|
-
}
|
|
3068
|
-
}
|
|
3069
2914
|
if (nativeVideo) {
|
|
3070
2915
|
// Re-classify with the body: some APIs (Venice) carry the job ID in it.
|
|
3071
2916
|
const prepared = prepareVideoRequest(nativeVideoRoute(serializedReq) ?? nativeVideo, serializedReq.headers, this._resourceRoutes);
|
|
@@ -3091,7 +2936,6 @@ export class BuyerProxy {
|
|
|
3091
2936
|
const conversationIdentity = isConversationRequest
|
|
3092
2937
|
? extractConversationIdentity(serializedReq.headers, conversationBody)
|
|
3093
2938
|
: null;
|
|
3094
|
-
const routingConversationKey = conversationIdentity ? `${conversationIdentity.tool}:${conversationIdentity.sessionKey}` : null;
|
|
3095
2939
|
// Internal marker from the system proxy: source profile used only for
|
|
3096
2940
|
// local conversation attribution. Stripped here so it never reaches a seller.
|
|
3097
2941
|
delete serializedReq.headers[SYSTEM_PROXY_SOURCE_HEADER];
|
|
@@ -3101,10 +2945,6 @@ export class BuyerProxy {
|
|
|
3101
2945
|
const storedConversation = conversationIdentity && trackedConversationKey
|
|
3102
2946
|
? this._conversations.get(`${conversationIdentity.tool}:${trackedConversationKey}`)
|
|
3103
2947
|
: null;
|
|
3104
|
-
// Use this chat's router override when present; otherwise follow the buyer's default choice.
|
|
3105
|
-
const conversationSelection = storedConversation?.routingSelection ?? null;
|
|
3106
|
-
// Keep this request's choice stable; later selection changes affect subsequent requests.
|
|
3107
|
-
const routerSelection = structuredClone(conversationSelection ?? this._defaultRouter);
|
|
3108
2948
|
const storedAutoRoute = storedConversation?.peerSource === 'auto' && storedConversation.pinnedModel
|
|
3109
2949
|
? parsePeerPinnedService(storedConversation.pinnedModel)
|
|
3110
2950
|
: null;
|
|
@@ -3114,242 +2954,6 @@ export class BuyerProxy {
|
|
|
3114
2954
|
const preferredPeerHeader = normalizePeerId(serializedReq.headers['x-antseed-prefer-peer'] ?? '');
|
|
3115
2955
|
const preferredConversationPeerId = preferredPeerHeader ?? storedAutoRoute?.peerId ?? null;
|
|
3116
2956
|
const effectiveRoutedModel = chatPinnedModel ?? this._defaultRoutedModel;
|
|
3117
|
-
// Where a router pin can come from besides the request's own model: the
|
|
3118
|
-
// `antseed` alias resolves to the chat's explicit pin or, when the chat
|
|
3119
|
-
// chose no route of its own (neither a pin nor a router), the default
|
|
3120
|
-
// route; a system-proxied request follows the chat's explicit pin. An
|
|
3121
|
-
// automatic chat route (the seller a router chose last turn) never
|
|
3122
|
-
// counts, so every turn of a router-pinned chat is ranked again.
|
|
3123
|
-
const userChatPin = storedConversation?.peerSource === 'user' ? storedConversation.pinnedModel : null;
|
|
3124
|
-
const aliasTarget = userChatPin ?? (conversationSelection ? null : this._defaultRoutedModel);
|
|
3125
|
-
// Reuse disconnect cancellation for both the recommendation and the eventual inference call.
|
|
3126
|
-
const clientAbortController = new AbortController();
|
|
3127
|
-
const onClientAbort = () => {
|
|
3128
|
-
if (clientAbortController.signal.aborted)
|
|
3129
|
-
return;
|
|
3130
|
-
clientAbortController.abort();
|
|
3131
|
-
log(`Client disconnected; aborting upstream request reqId=${serializedReq.requestId.slice(0, 8)}`);
|
|
3132
|
-
};
|
|
3133
|
-
req.once('close', () => {
|
|
3134
|
-
if (!req.complete && !res.writableEnded)
|
|
3135
|
-
onClientAbort();
|
|
3136
|
-
});
|
|
3137
|
-
res.once('close', () => {
|
|
3138
|
-
if (!res.writableEnded)
|
|
3139
|
-
onClientAbort();
|
|
3140
|
-
});
|
|
3141
|
-
// Explicit model/seller choices bypass recommendations, even when the default is a router.
|
|
3142
|
-
// A system-proxy connection's model was assigned at connect time, not chosen by the tool:
|
|
3143
|
-
// under a router it stands for the automatic alias, unless this chat is pinned. The request
|
|
3144
|
-
// is only rewritten once the router path is certain (below), so a request that goes direct
|
|
3145
|
-
// keeps its connect-time model.
|
|
3146
|
-
const systemRoutedAsAlias = routerSelection !== null && systemRoutedModel && storedConversation?.peerSource !== 'user';
|
|
3147
|
-
const rawService = systemRoutedAsAlias ? ROUTED_MODEL_ALIAS : extractRequestedService(serializedReq);
|
|
3148
|
-
// `<routerPeer>@<routingServiceId>`: the request names a routing service as
|
|
3149
|
-
// its model, so that router picks the model and seller for this request.
|
|
3150
|
-
const routerPinSource = rawService === ROUTED_MODEL_ALIAS
|
|
3151
|
-
? aliasTarget
|
|
3152
|
-
: systemRoutedModel && userChatPin ? userChatPin : rawService;
|
|
3153
|
-
const pinnedRouter = await this._resolvePinnedRouter(routerPinSource);
|
|
3154
|
-
const autoRequested = routerSelection !== null
|
|
3155
|
-
&& rawService === ROUTED_MODEL_ALIAS;
|
|
3156
|
-
const chatModelPin = storedConversation?.peerSource === 'user' ? chatPinnedModel : null;
|
|
3157
|
-
if (autoRequested && chatModelPin) {
|
|
3158
|
-
serializedReq = withRoutedModel(serializedReq, chatModelPin);
|
|
3159
|
-
}
|
|
3160
|
-
const useModelRouter = pinnedRouter !== null || (autoRequested && !chatModelPin
|
|
3161
|
-
&& !getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined));
|
|
3162
|
-
if (useModelRouter && systemRoutedAsAlias) {
|
|
3163
|
-
serializedReq = withRoutedModel(serializedReq, ROUTED_MODEL_ALIAS);
|
|
3164
|
-
}
|
|
3165
|
-
// A request without `routing` uses the saved router settings (tradeoff,
|
|
3166
|
-
// model allow list) when it pins the router those settings belong to.
|
|
3167
|
-
const savedRouter = this._defaultRouter;
|
|
3168
|
-
const savedForPinned = pinnedRouter && savedRouter && sameRoutingServiceTarget(savedRouter.service, pinnedRouter.service)
|
|
3169
|
-
? structuredClone(savedRouter)
|
|
3170
|
-
: null;
|
|
3171
|
-
const baseSelection = pinnedRouter ? savedForPinned ?? pinnedRouter : routerSelection;
|
|
3172
|
-
const activeSelection = baseSelection && routingOptions.costQualityTradeoff !== undefined
|
|
3173
|
-
? { ...baseSelection, costQualityTradeoff: routingOptions.costQualityTradeoff }
|
|
3174
|
-
: baseSelection;
|
|
3175
|
-
const context = {
|
|
3176
|
-
request: serializedReq, requiredParameters, requestPolicy, signal: clientAbortController.signal,
|
|
3177
|
-
systemRoutedModel, conversationIdentity, conversationBody, storedConversation,
|
|
3178
|
-
effectivePinnedPeer, effectiveRoutedModel, chatPinnedModel,
|
|
3179
|
-
preferredConversationPeerId,
|
|
3180
|
-
// Cache-reuse observations only feed a router, so skip them when none is configured.
|
|
3181
|
-
routingConversationKey: activeSelection ? routingConversationKey : null,
|
|
3182
|
-
spendAttributionTag,
|
|
3183
|
-
};
|
|
3184
|
-
// Both paths return an inference answer; only the router path buys a recommendation first.
|
|
3185
|
-
if (activeSelection && useModelRouter) {
|
|
3186
|
-
if (invalidRouting) {
|
|
3187
|
-
sendJson(res, 400, { error: { type: 'invalid_request_error', code: 'invalid_routing', message: invalidRouting, param: 'routing' } });
|
|
3188
|
-
return;
|
|
3189
|
-
}
|
|
3190
|
-
const refusal = await this._routerRefusal(activeSelection, requestPolicy);
|
|
3191
|
-
if (refusal) {
|
|
3192
|
-
log(`Routing service refused before ranking: ${refusal.error.message}`);
|
|
3193
|
-
sendJson(res, 403, refusal);
|
|
3194
|
-
return;
|
|
3195
|
-
}
|
|
3196
|
-
await this._dispatchViaModelRouter(res, context, activeSelection);
|
|
3197
|
-
}
|
|
3198
|
-
else {
|
|
3199
|
-
await this._dispatchDirectModelRequest(res, context);
|
|
3200
|
-
}
|
|
3201
|
-
}
|
|
3202
|
-
/**
|
|
3203
|
-
* Buy ranked recommendations, then send each one through the normal inference path
|
|
3204
|
-
* until one succeeds. A failed exact peer falls back to the next recommendation.
|
|
3205
|
-
*/
|
|
3206
|
-
async _dispatchViaModelRouter(res, context, selection) {
|
|
3207
|
-
const { request, requiredParameters, requestPolicy, signal, routingConversationKey, spendAttributionTag } = context;
|
|
3208
|
-
const conversationId = this._trackRouterConversation(context);
|
|
3209
|
-
let recommendations;
|
|
3210
|
-
try {
|
|
3211
|
-
const client = this._modelRoutingClient;
|
|
3212
|
-
if (!selection.service)
|
|
3213
|
-
throw new Error('Select an exact routing-service target');
|
|
3214
|
-
const peers = await this._getPeers();
|
|
3215
|
-
// The router may only recommend destinations the buyer's own policies allow.
|
|
3216
|
-
const candidates = eligibleRouterCandidates(request, peers, requiredParameters, this._routingPreferences, (policyRequest, peer) => peerAllowedByPolicy(this._node.router, policyRequest, peer)
|
|
3217
|
-
&& this._routerCandidateAllowedByRequestPolicy(requestPolicy, policyRequest, peer)
|
|
3218
|
-
&& !isCoolingDown(this._peerHealth.get(peer.peerId), this._now()));
|
|
3219
|
-
recommendations = await executeRouterSelection({
|
|
3220
|
-
node: this._node, client, request, peers, candidates,
|
|
3221
|
-
conversationKey: routingConversationKey, selection, modelsCache: this._routingModels,
|
|
3222
|
-
...(request.headers[BUYER_IDENTITY_HEADER] ? { buyerIdentity: request.headers[BUYER_IDENTITY_HEADER] } : {}),
|
|
3223
|
-
signal: AbortSignal.any([signal, AbortSignal.timeout(120_000)]),
|
|
3224
|
-
onRoutingRequest: requestId => {
|
|
3225
|
-
if (spendAttributionTag)
|
|
3226
|
-
this._spendAttribution.track(requestId, spendAttributionTag);
|
|
3227
|
-
if (conversationId)
|
|
3228
|
-
this._trackRequestConversation(requestId, conversationId, undefined, 'routing');
|
|
3229
|
-
},
|
|
3230
|
-
});
|
|
3231
|
-
}
|
|
3232
|
-
catch (error) {
|
|
3233
|
-
// Report routing failures rather than silently choosing a model for the user.
|
|
3234
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3235
|
-
writeDispatchFailure(res, error instanceof NoEligibleCandidatesError
|
|
3236
|
-
? dispatchFailure(422, { 'content-type': 'application/json' }, JSON.stringify({
|
|
3237
|
-
error: { type: 'invalid_request_error', code: 'no_eligible_candidates', message },
|
|
3238
|
-
}))
|
|
3239
|
-
: dispatchFailure(502, { 'content-type': 'application/json' }, JSON.stringify({
|
|
3240
|
-
error: { type: 'routing_error', message },
|
|
3241
|
-
})));
|
|
3242
|
-
return;
|
|
3243
|
-
}
|
|
3244
|
-
let lastFailure = null;
|
|
3245
|
-
for (const recommendation of recommendations) {
|
|
3246
|
-
// Never switch sellers once the user has disconnected or a response has started.
|
|
3247
|
-
if (signal.aborted || res.headersSent || res.writableEnded)
|
|
3248
|
-
return;
|
|
3249
|
-
// Each attempt has its own billing ID, but all attempts count as one chat turn.
|
|
3250
|
-
const attempt = requestForRecommendation(request, recommendation, randomUUID());
|
|
3251
|
-
if (spendAttributionTag)
|
|
3252
|
-
this._spendAttribution.track(attempt.requestId, spendAttributionTag);
|
|
3253
|
-
if (conversationId)
|
|
3254
|
-
this._trackRequestConversation(attempt.requestId, conversationId, request.requestId);
|
|
3255
|
-
const result = await this._dispatchInference(res, context, attempt, conversationId);
|
|
3256
|
-
if (result.done)
|
|
3257
|
-
return;
|
|
3258
|
-
lastFailure = result;
|
|
3259
|
-
// Only try the next recommendation after a clear seller refusal, never after an ambiguous
|
|
3260
|
-
// transport failure or timeout (the seller may have done paid work) or a buyer-side fault.
|
|
3261
|
-
if (result.errorMessage !== null || isBuyerFault(result) || !RECOMMENDATION_FALLBACK_STATUS_CODES.has(result.statusCode))
|
|
3262
|
-
break;
|
|
3263
|
-
log(`Recommendation ${recommendation.peerId ? `${recommendation.peerId.slice(0, 12)}...@` : ''}${recommendation.serviceId} failed (${result.statusCode}); trying the next recommendation.`);
|
|
3264
|
-
}
|
|
3265
|
-
if (signal.aborted)
|
|
3266
|
-
return;
|
|
3267
|
-
writeDispatchFailure(res, lastFailure ?? dispatchFailure(502, { 'content-type': 'application/json' }, JSON.stringify({
|
|
3268
|
-
error: { type: 'routing_error', message: 'No recommended destination could serve this request' },
|
|
3269
|
-
})));
|
|
3270
|
-
}
|
|
3271
|
-
/**
|
|
3272
|
-
* The router selection a `<peerId>@<serviceId>` model names, when that
|
|
3273
|
-
* peer's routing service is `<serviceId>`. A peer missing from the cache
|
|
3274
|
-
* gets one refresh first, so a router that just came online is not taken
|
|
3275
|
-
* for a seller. Still missing, the pin goes down the seller-pin path, which
|
|
3276
|
-
* reports the peer as unreachable.
|
|
3277
|
-
*/
|
|
3278
|
-
async _resolvePinnedRouter(model) {
|
|
3279
|
-
const pin = model ? parsePeerPinnedService(model) : null;
|
|
3280
|
-
if (!pin)
|
|
3281
|
-
return null;
|
|
3282
|
-
let peers = await this._getPeers();
|
|
3283
|
-
if (!peers.some((peer) => peer.peerId.toLowerCase() === pin.peerId)) {
|
|
3284
|
-
peers = await this._getPeers({ forceRefresh: true });
|
|
3285
|
-
}
|
|
3286
|
-
const service = findPinnedRoutingService(peers, pin.peerId, pin.service);
|
|
3287
|
-
return service ? { kind: 'router', service } : null;
|
|
3288
|
-
}
|
|
3289
|
-
/**
|
|
3290
|
-
* Why a routing service may not be paid, the way a pinned seller is judged:
|
|
3291
|
-
* the gateway policy's model allow list (the router's service id counts as
|
|
3292
|
-
* its model), its seller rules and strict model routes, and the buyer's
|
|
3293
|
-
* hard limits, all through `routingServicePolicy`. Empty when allowed.
|
|
3294
|
-
*/
|
|
3295
|
-
_routerExclusionReasons(peer, serviceId, requestPolicy) {
|
|
3296
|
-
const facts = this._policyFacts(peer, null);
|
|
3297
|
-
const reasons = requestPolicy ? pinnedPeerExclusionReasons(routingServicePolicy(requestPolicy), facts, serviceId) : [];
|
|
3298
|
-
if (requestPolicy && !policyAllowsModel(this._effectivePolicy(requestPolicy), serviceId))
|
|
3299
|
-
reasons.unshift('model not allowed');
|
|
3300
|
-
addBuyerConfigReasons(reasons, policyExclusionReasons(routingServicePolicy(this._buyerHardPolicy()), facts));
|
|
3301
|
-
return reasons;
|
|
3302
|
-
}
|
|
3303
|
-
/** A 403 body when the policy forbids paying this router for a ranking; null when allowed or its peer is unknown. */
|
|
3304
|
-
async _routerRefusal(selection, requestPolicy) {
|
|
3305
|
-
const service = selection.service;
|
|
3306
|
-
if (!service)
|
|
3307
|
-
return null;
|
|
3308
|
-
const peer = (await this._getPeers()).find((candidate) => candidate.peerId.toLowerCase() === service.peerId.toLowerCase());
|
|
3309
|
-
if (!peer)
|
|
3310
|
-
return null;
|
|
3311
|
-
const reasons = this._routerExclusionReasons(peer, service.serviceId, requestPolicy);
|
|
3312
|
-
if (reasons.length === 0)
|
|
3313
|
-
return null;
|
|
3314
|
-
const label = `${service.peerId.slice(0, 12)}...@${service.serviceId}`;
|
|
3315
|
-
if (reasons[0] === 'model not allowed') {
|
|
3316
|
-
return { error: { type: 'permission_error', code: 'model_not_allowed', param: 'model',
|
|
3317
|
-
message: `Routing service "${service.serviceId}" is not allowed by the routing policy.` } };
|
|
3318
|
-
}
|
|
3319
|
-
return { error: { type: 'permission_error', code: 'peer_not_allowed',
|
|
3320
|
-
message: `Routing service ${label} is not allowed by the routing policy (${reasons.join('; ')}).` } };
|
|
3321
|
-
}
|
|
3322
|
-
_routerCandidateAllowedByRequestPolicy(requestPolicy, request, peer) {
|
|
3323
|
-
if (!requestPolicy)
|
|
3324
|
-
return true;
|
|
3325
|
-
const provider = getExplicitProviderOverride(request);
|
|
3326
|
-
const serviceId = extractRequestedService(request);
|
|
3327
|
-
const offer = provider && serviceId ? findAdvertisedServiceOffer(peer, provider, serviceId) : null;
|
|
3328
|
-
return offer !== null
|
|
3329
|
-
&& this._policyAllowsOffer(requestPolicy, peer, offer)
|
|
3330
|
-
&& policyExclusionReasons(this._buyerHardPolicy(), this._policyFacts(peer, offer)).length === 0;
|
|
3331
|
-
}
|
|
3332
|
-
/** Track a router-served chat before buying its recommendation, so routing costs are attributed. */
|
|
3333
|
-
_trackRouterConversation(context) {
|
|
3334
|
-
const { request, conversationIdentity, conversationBody, storedConversation } = context;
|
|
3335
|
-
if (!conversationIdentity?.isUserThread)
|
|
3336
|
-
return storedConversation?.id ?? null;
|
|
3337
|
-
if (!storedConversation && isTitleGenerationRequest(conversationBody))
|
|
3338
|
-
return null;
|
|
3339
|
-
const { id } = this._conversations.touch({
|
|
3340
|
-
tool: conversationIdentity.tool,
|
|
3341
|
-
sessionKey: conversationIdentity.parentSessionKey ?? conversationIdentity.sessionKey,
|
|
3342
|
-
snippet: storedConversation?.snippet ? null : extractFirstUserSnippet(conversationBody),
|
|
3343
|
-
lastModel: null,
|
|
3344
|
-
});
|
|
3345
|
-
// Inference attempts link to this ID so fallbacks count as one chat turn.
|
|
3346
|
-
this._trackRequestConversation(request.requestId, id);
|
|
3347
|
-
return id;
|
|
3348
|
-
}
|
|
3349
|
-
/** Apply model aliases and chat pins, then attach inference to its conversation for accounting. */
|
|
3350
|
-
_prepareInferenceRequest(res, context) {
|
|
3351
|
-
const { effectiveRoutedModel, chatPinnedModel, systemRoutedModel, storedConversation, conversationIdentity, conversationBody } = context;
|
|
3352
|
-
let serializedReq = context.request;
|
|
3353
2957
|
let trackedConversationId = storedConversation?.id ?? null;
|
|
3354
2958
|
// Resolve the `antseed` model alias to the session's default route first,
|
|
3355
2959
|
// so the regular `<peerId>@<service>` pin rewrite below picks up the
|
|
@@ -3368,7 +2972,7 @@ export class BuyerProxy {
|
|
|
3368
2972
|
param: 'model',
|
|
3369
2973
|
},
|
|
3370
2974
|
}));
|
|
3371
|
-
return
|
|
2975
|
+
return;
|
|
3372
2976
|
}
|
|
3373
2977
|
if (aliasResult.substituted) {
|
|
3374
2978
|
serializedReq = { ...serializedReq, body: aliasResult.body, headers: aliasResult.headers };
|
|
@@ -3431,29 +3035,10 @@ export class BuyerProxy {
|
|
|
3431
3035
|
this._trackRequestConversation(serializedReq.requestId, tracked.id);
|
|
3432
3036
|
}
|
|
3433
3037
|
}
|
|
3434
|
-
return { request: serializedReq, conversationId: trackedConversationId };
|
|
3435
|
-
}
|
|
3436
|
-
/** Send a fixed-model or pinned-seller request without buying a model recommendation. */
|
|
3437
|
-
async _dispatchDirectModelRequest(res, context) {
|
|
3438
|
-
const prepared = this._prepareInferenceRequest(res, context);
|
|
3439
|
-
if (!prepared)
|
|
3440
|
-
return;
|
|
3441
|
-
const result = await this._dispatchInference(res, context, prepared.request, prepared.conversationId);
|
|
3442
|
-
if (!result.done)
|
|
3443
|
-
writeDispatchFailure(res, result);
|
|
3444
|
-
}
|
|
3445
|
-
/**
|
|
3446
|
-
* Select a seller for the requested model (or the pinned seller) and send the request.
|
|
3447
|
-
* Success is written to `res`; failures are returned so callers can fall back or report them.
|
|
3448
|
-
*/
|
|
3449
|
-
async _dispatchInference(res, context, request, trackedConversationId) {
|
|
3450
3038
|
// A chat switched while this request is in flight must keep the new route.
|
|
3451
3039
|
const conversationPinAtRouting = trackedConversationId
|
|
3452
3040
|
? this._conversations.get(trackedConversationId)?.pinnedModel ?? null
|
|
3453
3041
|
: null;
|
|
3454
|
-
const nativeVideo = nativeVideoRoute(request);
|
|
3455
|
-
let serializedReq = request;
|
|
3456
|
-
const { requiredParameters, requestPolicy, signal, effectivePinnedPeer, preferredConversationPeerId, routingConversationKey } = context;
|
|
3457
3042
|
const { body: servicePinBody, headers: servicePinHeaders, pinnedPeerId: bodyPinnedPeer, } = rewritePeerPinnedServiceInBody(serializedReq.body, serializedReq.headers);
|
|
3458
3043
|
if (servicePinBody !== serializedReq.body) {
|
|
3459
3044
|
serializedReq = { ...serializedReq, body: servicePinBody, headers: servicePinHeaders };
|
|
@@ -3461,6 +3046,24 @@ export class BuyerProxy {
|
|
|
3461
3046
|
log(`Model peer pin applied: peer=${bodyPinnedPeer.slice(0, 12)}...`);
|
|
3462
3047
|
}
|
|
3463
3048
|
}
|
|
3049
|
+
const clientAbortController = new AbortController();
|
|
3050
|
+
const onClientAbort = () => {
|
|
3051
|
+
if (clientAbortController.signal.aborted) {
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
clientAbortController.abort();
|
|
3055
|
+
log(`Client disconnected; aborting upstream request reqId=${serializedReq.requestId.slice(0, 8)}`);
|
|
3056
|
+
};
|
|
3057
|
+
req.once('close', () => {
|
|
3058
|
+
if (!req.complete && !res.writableEnded) {
|
|
3059
|
+
onClientAbort();
|
|
3060
|
+
}
|
|
3061
|
+
});
|
|
3062
|
+
res.once('close', () => {
|
|
3063
|
+
if (!res.writableEnded) {
|
|
3064
|
+
onClientAbort();
|
|
3065
|
+
}
|
|
3066
|
+
});
|
|
3464
3067
|
const requestProtocol = detectRequestServiceApiProtocol(serializedReq);
|
|
3465
3068
|
const requestedService = extractRequestedService(serializedReq);
|
|
3466
3069
|
log(`Routing: protocol=${requestProtocol ?? 'null'} service=${requestedService ?? 'null'}`);
|
|
@@ -3475,7 +3078,8 @@ export class BuyerProxy {
|
|
|
3475
3078
|
+ ' • Model name prefix: <peerId>@<model>\n'
|
|
3476
3079
|
+ ' • Session pin: antseed buyer connection set --peer <peerId>\n'
|
|
3477
3080
|
+ 'Discover peers with: antseed network browse';
|
|
3478
|
-
|
|
3081
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
3082
|
+
res.end(JSON.stringify({
|
|
3479
3083
|
error: {
|
|
3480
3084
|
type: 'missing_routing_target',
|
|
3481
3085
|
code: 'missing_routing_target',
|
|
@@ -3489,22 +3093,26 @@ export class BuyerProxy {
|
|
|
3489
3093
|
},
|
|
3490
3094
|
},
|
|
3491
3095
|
}));
|
|
3096
|
+
return;
|
|
3492
3097
|
}
|
|
3493
3098
|
if (requestPolicy && !policyAllowsModel(this._effectivePolicy(requestPolicy), requestedService)) {
|
|
3494
|
-
|
|
3099
|
+
sendJson(res, 403, {
|
|
3495
3100
|
error: {
|
|
3496
3101
|
type: 'permission_error',
|
|
3497
3102
|
code: 'model_not_allowed',
|
|
3498
3103
|
message: `Model "${requestedService ?? 'unknown'}" is not allowed by the routing policy.`,
|
|
3499
3104
|
param: 'model',
|
|
3500
3105
|
},
|
|
3501
|
-
})
|
|
3106
|
+
});
|
|
3107
|
+
return;
|
|
3502
3108
|
}
|
|
3503
3109
|
// Discover peers
|
|
3504
3110
|
const peers = await this._getPeers();
|
|
3505
3111
|
if (peers.length === 0) {
|
|
3506
3112
|
log('No sellers available');
|
|
3507
|
-
|
|
3113
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
3114
|
+
res.end('No sellers available on the network. Is a seeder running?');
|
|
3115
|
+
return;
|
|
3508
3116
|
}
|
|
3509
3117
|
if (!explicitPeerId && requestedService) {
|
|
3510
3118
|
const selectModelPeers = (candidateSources) => selectCandidatePeersForRouting(candidateSources, requestProtocol, requestedService, explicitProvider, 'strict');
|
|
@@ -3539,7 +3147,8 @@ export class BuyerProxy {
|
|
|
3539
3147
|
? ` "${requestedService}" is a decision model; call POST /v1/systemone.`
|
|
3540
3148
|
: '';
|
|
3541
3149
|
log(`Request rejected: model ${requestedService} is served only via ${advertisedProtocols.join(', ')}`);
|
|
3542
|
-
|
|
3150
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
3151
|
+
res.end(JSON.stringify({
|
|
3543
3152
|
error: {
|
|
3544
3153
|
type: 'unsupported_protocol',
|
|
3545
3154
|
code: 'unsupported_protocol',
|
|
@@ -3548,8 +3157,10 @@ export class BuyerProxy {
|
|
|
3548
3157
|
supported_protocols: advertisedProtocols,
|
|
3549
3158
|
},
|
|
3550
3159
|
}));
|
|
3160
|
+
return;
|
|
3551
3161
|
}
|
|
3552
|
-
|
|
3162
|
+
res.writeHead(capabilityRequired ? 422 : 502, { 'content-type': 'application/json' });
|
|
3163
|
+
res.end(JSON.stringify({
|
|
3553
3164
|
error: capabilityRequired
|
|
3554
3165
|
? {
|
|
3555
3166
|
type: 'required_capability_unavailable',
|
|
@@ -3564,6 +3175,7 @@ export class BuyerProxy {
|
|
|
3564
3175
|
param: 'model',
|
|
3565
3176
|
},
|
|
3566
3177
|
}));
|
|
3178
|
+
return;
|
|
3567
3179
|
}
|
|
3568
3180
|
let lastRetry = null;
|
|
3569
3181
|
let lastVerificationError = null;
|
|
@@ -3575,8 +3187,8 @@ export class BuyerProxy {
|
|
|
3575
3187
|
if (unfunded && !isFree(selected))
|
|
3576
3188
|
continue;
|
|
3577
3189
|
if (this._verifier) {
|
|
3578
|
-
const makeReach = (chosenId) => makeVerifierReach(this._node, selected.peer, chosenId, signal);
|
|
3579
|
-
const outcome = await this._verifyPeer(selected.peer, makeReach, signal, requireVerified);
|
|
3190
|
+
const makeReach = (chosenId) => makeVerifierReach(this._node, selected.peer, chosenId, clientAbortController.signal);
|
|
3191
|
+
const outcome = await this._verifyPeer(selected.peer, makeReach, clientAbortController.signal, requireVerified);
|
|
3580
3192
|
if (!outcome.ok) {
|
|
3581
3193
|
lastVerificationError = `Peer ${selected.peer.peerId.slice(0, 12)}... failed required verification (${outcome.reason ?? 'failed'}).`;
|
|
3582
3194
|
log(`${lastVerificationError} Trying the next model peer.`);
|
|
@@ -3588,28 +3200,33 @@ export class BuyerProxy {
|
|
|
3588
3200
|
+ `service="${selected.serviceId}" reputation=${selected.reputation} `
|
|
3589
3201
|
+ `effective=${selected.effectiveReputationScore ?? 'unknown'} peer=${index + 1}/${candidates.length} `
|
|
3590
3202
|
+ `attempt=${peerAttempt + 1}/${MODEL_RATE_LIMIT_MAX_ATTEMPTS_PER_PEER}`);
|
|
3591
|
-
const result = await this._dispatchToPeer(res, selected.request, selected.peer, modelPlans, requestProtocol, selected.serviceId, explicitProvider, router, false, signal, requestPolicy !== null
|
|
3203
|
+
const result = await this._dispatchToPeer(res, selected.request, selected.peer, modelPlans, requestProtocol, selected.serviceId, explicitProvider, router, false, clientAbortController.signal, requestPolicy !== null);
|
|
3592
3204
|
if (result.done) {
|
|
3593
3205
|
// Only a seller that answered becomes the chat's route: an error
|
|
3594
3206
|
// must not pin later turns to it.
|
|
3595
3207
|
if (trackedConversationId && res.statusCode < 400) {
|
|
3596
3208
|
this._conversations.recordRoutedModel(trackedConversationId, `${selected.peer.peerId}@${selected.serviceId}`, conversationPinAtRouting);
|
|
3597
3209
|
}
|
|
3598
|
-
return
|
|
3210
|
+
return;
|
|
3599
3211
|
}
|
|
3600
3212
|
// Never retry a video on another seller: a create may already have
|
|
3601
3213
|
// started (and been charged), and follow-ups only exist on one seller.
|
|
3602
|
-
if (nativeVideo)
|
|
3603
|
-
|
|
3214
|
+
if (nativeVideo) {
|
|
3215
|
+
res.writeHead(result.statusCode, result.responseHeaders);
|
|
3216
|
+
res.end(result.responseBody);
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3604
3219
|
lastRetry = result;
|
|
3605
|
-
if (
|
|
3220
|
+
if (result.responseHeaders[ANTSEED_FAULT_ATTRIBUTION_HEADER]?.toLowerCase() === 'buyer') {
|
|
3606
3221
|
const freeLeft = candidates.slice(index + 1).some(isFree);
|
|
3607
3222
|
if (result.statusCode === 402 && freeLeft) {
|
|
3608
3223
|
unfunded = true;
|
|
3609
3224
|
log(`No deposits to pay peer ${selected.peer.peerId.slice(0, 12)}...; trying free sellers only.`);
|
|
3610
3225
|
break;
|
|
3611
3226
|
}
|
|
3612
|
-
|
|
3227
|
+
res.writeHead(result.statusCode, result.responseHeaders);
|
|
3228
|
+
res.end(result.responseBody);
|
|
3229
|
+
return;
|
|
3613
3230
|
}
|
|
3614
3231
|
const retrySamePeer = result.statusCode === 429
|
|
3615
3232
|
&& peerAttempt + 1 < MODEL_RATE_LIMIT_MAX_ATTEMPTS_PER_PEER;
|
|
@@ -3617,20 +3234,28 @@ export class BuyerProxy {
|
|
|
3617
3234
|
break;
|
|
3618
3235
|
const delayMs = rateLimitRetryDelayMs(result.responseHeaders, peerAttempt);
|
|
3619
3236
|
log(`Peer ${selected.peer.peerId.slice(0, 12)}... is rate-limited; retrying in ${delayMs}ms.`);
|
|
3620
|
-
if (!await waitForRetry(delayMs, signal))
|
|
3621
|
-
return
|
|
3237
|
+
if (!await waitForRetry(delayMs, clientAbortController.signal))
|
|
3238
|
+
return;
|
|
3622
3239
|
}
|
|
3623
3240
|
if (index + 1 < candidates.length) {
|
|
3624
3241
|
log(`Peer ${selected.peer.peerId.slice(0, 12)}... failed retryably; trying next model peer.`);
|
|
3625
3242
|
}
|
|
3626
3243
|
}
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
}
|
|
3633
|
-
|
|
3244
|
+
if (lastRetry) {
|
|
3245
|
+
res.writeHead(lastRetry.statusCode, lastRetry.responseHeaders);
|
|
3246
|
+
res.end(lastRetry.responseBody);
|
|
3247
|
+
}
|
|
3248
|
+
else {
|
|
3249
|
+
res.writeHead(502, { 'content-type': 'application/json' });
|
|
3250
|
+
res.end(JSON.stringify({
|
|
3251
|
+
error: {
|
|
3252
|
+
type: 'peer_verification_failed',
|
|
3253
|
+
code: 'peer_verification_failed',
|
|
3254
|
+
message: lastVerificationError ?? `No verified peer currently serves model "${requestedService}".`,
|
|
3255
|
+
},
|
|
3256
|
+
}));
|
|
3257
|
+
}
|
|
3258
|
+
return;
|
|
3634
3259
|
}
|
|
3635
3260
|
const pinnedPeerId = explicitPeerId;
|
|
3636
3261
|
// Narrow the candidate set to just the pinned peer (if we already know
|
|
@@ -3678,12 +3303,12 @@ export class BuyerProxy {
|
|
|
3678
3303
|
: '--peer flag or session pin';
|
|
3679
3304
|
const diagnostics = this._formatPeerSelectionDiagnostics(discoveredPeers);
|
|
3680
3305
|
log(`Pinned peer ${pinnedPeerId.slice(0, 12)}... not discoverable in DHT (${logSource})`);
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
return dispatchFailure(502, { 'content-type': 'text/plain' }, `Pinned peer ${pinnedPeerId.slice(0, 12)}... (seller or routing service) is not reachable right now. `
|
|
3306
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
3307
|
+
res.end(`Pinned peer ${pinnedPeerId.slice(0, 12)}... is not reachable right now. `
|
|
3684
3308
|
+ 'It may be offline, not announcing, or temporarily unreachable. '
|
|
3685
3309
|
+ 'Pick a different service in Discover or try again later. '
|
|
3686
3310
|
+ diagnostics);
|
|
3311
|
+
return;
|
|
3687
3312
|
}
|
|
3688
3313
|
if (routingPeers.length === 0) {
|
|
3689
3314
|
const pinnedPeer = discoveredPeers.find((peer) => peer.peerId.toLowerCase() === pinnedPeerId) ?? null;
|
|
@@ -3698,16 +3323,20 @@ export class BuyerProxy {
|
|
|
3698
3323
|
if (!providers.includes(explicitProvider)) {
|
|
3699
3324
|
const providerList = providers.length > 0 ? providers.join(', ') : 'none';
|
|
3700
3325
|
log(`Pinned peer ${pinnedPeerId.slice(0, 12)}... does not offer explicit provider=${explicitProvider}`);
|
|
3701
|
-
|
|
3326
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
3327
|
+
res.end(`Pinned peer ${pinnedPeerId.slice(0, 12)}... does not offer provider=${explicitProvider}. `
|
|
3702
3328
|
+ `Available providers: ${providerList}. `
|
|
3703
3329
|
+ 'Remove or change the x-antseed-provider header, or pick a different peer. '
|
|
3704
3330
|
+ diagnostics);
|
|
3331
|
+
return;
|
|
3705
3332
|
}
|
|
3706
3333
|
}
|
|
3707
3334
|
log(`Pinned peer ${pinnedPeerId.slice(0, 12)}... filtered out by protocol/service match`);
|
|
3708
|
-
|
|
3335
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
3336
|
+
res.end(`Pinned peer ${pinnedPeerId.slice(0, 12)}... does not support this request `
|
|
3709
3337
|
+ `(${protocolLabel}, ${providerLabel}, ${serviceLabel}). `
|
|
3710
3338
|
+ `Pick a different service in Discover. ${diagnostics}`);
|
|
3339
|
+
return;
|
|
3711
3340
|
}
|
|
3712
3341
|
log(`Routing candidates: ${routingPeers.length} peer(s)`);
|
|
3713
3342
|
const router = this._node.router;
|
|
@@ -3717,7 +3346,9 @@ export class BuyerProxy {
|
|
|
3717
3346
|
// if an invariant breaks.
|
|
3718
3347
|
if (!selectedPeer) {
|
|
3719
3348
|
log(`Invariant: pinned peer ${pinnedPeerId.slice(0, 12)}... present in DHT but missing from narrowed candidate list`);
|
|
3720
|
-
|
|
3349
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
3350
|
+
res.end(`Pinned peer ${pinnedPeerId.slice(0, 12)}... is currently unreachable. Try again in a moment.`);
|
|
3351
|
+
return;
|
|
3721
3352
|
}
|
|
3722
3353
|
const policyRouter = router;
|
|
3723
3354
|
const selectedPlan = routingPlans.get(selectedPeer.peerId)
|
|
@@ -3727,7 +3358,8 @@ export class BuyerProxy {
|
|
|
3727
3358
|
? findMissingRequiredParameters(selectedPeer, selectedPlan.provider, pinnedServiceId, requiredParameters)
|
|
3728
3359
|
: requiredParameters;
|
|
3729
3360
|
if (missingRequired.length > 0) {
|
|
3730
|
-
|
|
3361
|
+
res.writeHead(422, { 'content-type': 'application/json' });
|
|
3362
|
+
res.end(JSON.stringify({
|
|
3731
3363
|
error: {
|
|
3732
3364
|
type: 'required_capability_unavailable',
|
|
3733
3365
|
code: 'required_capability_unavailable',
|
|
@@ -3735,6 +3367,7 @@ export class BuyerProxy {
|
|
|
3735
3367
|
param: REQUIRED_PARAMETERS_HEADER,
|
|
3736
3368
|
},
|
|
3737
3369
|
}));
|
|
3370
|
+
return;
|
|
3738
3371
|
}
|
|
3739
3372
|
const pinnedRequest = pinnedServiceId ? withRoutedModel(serializedReq, pinnedServiceId) : serializedReq;
|
|
3740
3373
|
// A hard pin cannot escape the gateway's policy: an excluded seller is
|
|
@@ -3752,23 +3385,26 @@ export class BuyerProxy {
|
|
|
3752
3385
|
addBuyerConfigReasons(reasons, policyExclusionReasons(this._buyerHardPolicy(), facts));
|
|
3753
3386
|
if (reasons.length > 0) {
|
|
3754
3387
|
log(`Pinned peer ${selectedPeer.peerId.slice(0, 12)}... refused by routing policy: ${reasons.join('; ')}`);
|
|
3755
|
-
|
|
3388
|
+
sendJson(res, 403, {
|
|
3756
3389
|
error: {
|
|
3757
3390
|
type: 'permission_error',
|
|
3758
3391
|
code: 'peer_not_allowed',
|
|
3759
3392
|
message: `Seller ${selectedPeer.peerId.slice(0, 12)}... is not allowed by the routing policy (${reasons.join('; ')}).`,
|
|
3760
3393
|
},
|
|
3761
|
-
})
|
|
3394
|
+
});
|
|
3395
|
+
return;
|
|
3762
3396
|
}
|
|
3763
3397
|
}
|
|
3764
3398
|
if (!peerAllowedByPolicy(policyRouter, pinnedRequest, selectedPeer)) {
|
|
3765
3399
|
log(`Pinned peer ${selectedPeer.peerId.slice(0, 12)}... filtered out by buyer routing policy`);
|
|
3766
|
-
|
|
3400
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
3401
|
+
res.end(`Pinned peer ${selectedPeer.peerId.slice(0, 12)}... is outside your buyer routing policy. `
|
|
3767
3402
|
+ 'Pick a different service in Discover or adjust your buyer pricing/reputation limits.');
|
|
3403
|
+
return;
|
|
3768
3404
|
}
|
|
3769
3405
|
if (this._verifier) {
|
|
3770
|
-
const makeReach = (chosenId) => makeVerifierReach(this._node, selectedPeer, chosenId, signal);
|
|
3771
|
-
const outcome = await this._verifyPeer(selectedPeer, makeReach, signal, pinnedPolicy?.requireVerified === true);
|
|
3406
|
+
const makeReach = (chosenId) => makeVerifierReach(this._node, selectedPeer, chosenId, clientAbortController.signal);
|
|
3407
|
+
const outcome = await this._verifyPeer(selectedPeer, makeReach, clientAbortController.signal, pinnedPolicy?.requireVerified === true);
|
|
3772
3408
|
const short = selectedPeer.peerId.slice(0, 12);
|
|
3773
3409
|
if (outcome.verified) {
|
|
3774
3410
|
log(`Verified ${short}... via ${outcome.sdk}`);
|
|
@@ -3777,17 +3413,23 @@ export class BuyerProxy {
|
|
|
3777
3413
|
log(`Verification ${outcome.sdk ? `(${outcome.sdk}) ` : ''}did not pass for ${short}...: ${outcome.reason ?? 'failed'}${outcome.ok ? ' (optional — routing anyway)' : ''}`);
|
|
3778
3414
|
}
|
|
3779
3415
|
if (!outcome.ok) {
|
|
3780
|
-
|
|
3416
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
3417
|
+
res.end(`Pinned peer ${short}... failed required verification (${outcome.reason ?? 'failed'}). `
|
|
3781
3418
|
+ 'Pick a different peer, or run without --require-verifier.');
|
|
3419
|
+
return;
|
|
3782
3420
|
}
|
|
3783
3421
|
}
|
|
3784
3422
|
log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
3785
|
-
const result = await this._dispatchToPeer(res, pinnedRequest, selectedPeer, routingPlans, requestProtocol, requestedService, explicitProvider, router, true, signal
|
|
3423
|
+
const result = await this._dispatchToPeer(res, pinnedRequest, selectedPeer, routingPlans, requestProtocol, requestedService, explicitProvider, router, true, clientAbortController.signal);
|
|
3786
3424
|
if (result.done && trackedConversationId && pinnedServiceId) {
|
|
3787
3425
|
this._conversations.recordRoutedModel(trackedConversationId, `${selectedPeer.peerId}@${pinnedServiceId}`, conversationPinAtRouting);
|
|
3788
3426
|
}
|
|
3789
|
-
|
|
3790
|
-
|
|
3427
|
+
if (!result.done) {
|
|
3428
|
+
// Pinned peer returned a retryable error. We never retry against another
|
|
3429
|
+
// peer for an explicit pin, so surface the error to the client.
|
|
3430
|
+
res.writeHead(result.statusCode, result.responseHeaders);
|
|
3431
|
+
res.end(result.responseBody);
|
|
3432
|
+
}
|
|
3791
3433
|
}
|
|
3792
3434
|
async _verifyPeer(peer, makeReach, signal, requireVerified = false) {
|
|
3793
3435
|
const configured = this._verifier;
|
|
@@ -3872,7 +3514,7 @@ export class BuyerProxy {
|
|
|
3872
3514
|
* was sent to the client (success or non-retryable error), or retry info if the
|
|
3873
3515
|
* caller should try another peer.
|
|
3874
3516
|
*/
|
|
3875
|
-
async _dispatchToPeer(res, serializedReq, selectedPeer, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, pinned, requestSignal, gatewayRouted = false
|
|
3517
|
+
async _dispatchToPeer(res, serializedReq, selectedPeer, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, pinned, requestSignal, gatewayRouted = false) {
|
|
3876
3518
|
const gatewayAutoRoute = gatewayRouted && !pinned;
|
|
3877
3519
|
const selectedRoutePlan = routePlanByPeerId.get(selectedPeer.peerId)
|
|
3878
3520
|
?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider, 'lenient');
|
|
@@ -4009,7 +3651,6 @@ export class BuyerProxy {
|
|
|
4009
3651
|
}
|
|
4010
3652
|
},
|
|
4011
3653
|
}, { signal: requestSignal, pinned, ...(buyerIdentity ? { buyerIdentity } : {}) });
|
|
4012
|
-
recordRouterUsage(this._modelRoutingClient, routingConversationKey, requestForPeer, response, selectedPeer, requestSignal);
|
|
4013
3654
|
let responseForClient = adaptBuyerFaultErrorResponse(response, requestProtocol);
|
|
4014
3655
|
responseForClient = adaptPeerResponse(responseForClient);
|
|
4015
3656
|
if (!streamed
|
|
@@ -4085,7 +3726,6 @@ export class BuyerProxy {
|
|
|
4085
3726
|
if (upstreamResponse.statusCode >= 400 && !adaptResponse) {
|
|
4086
3727
|
log(`Upstream raw error detail: ${summarizeErrorResponse(upstreamResponse)}`);
|
|
4087
3728
|
}
|
|
4088
|
-
recordRouterUsage(this._modelRoutingClient, routingConversationKey, requestForPeer, upstreamResponse, selectedPeer, requestSignal);
|
|
4089
3729
|
// Only the seller that accepted a video job knows its ID. Remember and
|
|
4090
3730
|
// persist job ID -> seller so later retrieve requests (even
|
|
4091
3731
|
// after a proxy restart) are pinned back to that seller.
|