@antseed/cli 0.1.196 → 0.1.198
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/buyer/start.d.ts.map +1 -1
- package/dist/cli/commands/buyer/start.js +1 -0
- package/dist/cli/commands/buyer/start.js.map +1 -1
- package/dist/cli/commands/gateway/activity.d.ts.map +1 -1
- package/dist/cli/commands/gateway/activity.js +26 -5
- package/dist/cli/commands/gateway/activity.js.map +1 -1
- package/dist/config/types.d.ts +2 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/gateway/accounting.d.ts +118 -6
- package/dist/gateway/accounting.d.ts.map +1 -1
- package/dist/gateway/accounting.js +319 -21
- package/dist/gateway/accounting.js.map +1 -1
- package/dist/gateway/console-api/handlers/favorites.d.ts +9 -0
- package/dist/gateway/console-api/handlers/favorites.d.ts.map +1 -0
- package/dist/gateway/console-api/handlers/favorites.js +44 -0
- package/dist/gateway/console-api/handlers/favorites.js.map +1 -0
- package/dist/gateway/console-api/handlers/favorites.test.d.ts +2 -0
- package/dist/gateway/console-api/handlers/favorites.test.d.ts.map +1 -0
- package/dist/gateway/console-api/handlers/favorites.test.js +41 -0
- package/dist/gateway/console-api/handlers/favorites.test.js.map +1 -0
- package/dist/gateway/console-api/handlers/index.d.ts.map +1 -1
- package/dist/gateway/console-api/handlers/index.js +2 -0
- package/dist/gateway/console-api/handlers/index.js.map +1 -1
- package/dist/gateway/console-api/handlers/network-mapping.d.ts +7 -1
- package/dist/gateway/console-api/handlers/network-mapping.d.ts.map +1 -1
- package/dist/gateway/console-api/handlers/network-mapping.js +46 -0
- package/dist/gateway/console-api/handlers/network-mapping.js.map +1 -1
- package/dist/gateway/console-api/handlers/network.d.ts.map +1 -1
- package/dist/gateway/console-api/handlers/network.js +19 -3
- package/dist/gateway/console-api/handlers/network.js.map +1 -1
- package/dist/gateway/console-api/handlers/network.test.js +70 -8
- package/dist/gateway/console-api/handlers/network.test.js.map +1 -1
- package/dist/gateway/console-api/handlers/usage.d.ts.map +1 -1
- package/dist/gateway/console-api/handlers/usage.js +2 -1
- package/dist/gateway/console-api/handlers/usage.js.map +1 -1
- package/dist/gateway/console-api/types.d.ts +75 -1
- package/dist/gateway/console-api/types.d.ts.map +1 -1
- package/dist/gateway/console-api/types.js.map +1 -1
- package/dist/gateway/gateway.test.js +829 -3
- package/dist/gateway/gateway.test.js.map +1 -1
- package/dist/gateway/request-errors.test.js +30 -0
- package/dist/gateway/request-errors.test.js.map +1 -1
- package/dist/gateway/routing-through-gateway.test.js +120 -4
- package/dist/gateway/routing-through-gateway.test.js.map +1 -1
- package/dist/gateway/runtime.d.ts.map +1 -1
- package/dist/gateway/runtime.js +4 -0
- package/dist/gateway/runtime.js.map +1 -1
- package/dist/gateway/server.d.ts +20 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +181 -25
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/services/network.d.ts +6 -1
- package/dist/gateway/services/network.d.ts.map +1 -1
- package/dist/gateway/services/network.js +12 -1
- package/dist/gateway/services/network.js.map +1 -1
- package/dist/gateway/services/usage.d.ts +2 -0
- package/dist/gateway/services/usage.d.ts.map +1 -1
- package/dist/gateway/services/usage.js +19 -5
- package/dist/gateway/services/usage.js.map +1 -1
- package/dist/gateway/spend-feed.d.ts +4 -1
- package/dist/gateway/spend-feed.d.ts.map +1 -1
- package/dist/gateway/spend-feed.js +7 -0
- package/dist/gateway/spend-feed.js.map +1 -1
- package/dist/gateway/store-data.test.js +1 -1
- package/dist/gateway/store-data.test.js.map +1 -1
- package/dist/gateway/store-migrations.d.ts.map +1 -1
- package/dist/gateway/store-migrations.js +40 -0
- package/dist/gateway/store-migrations.js.map +1 -1
- package/dist/gateway/store.d.ts +63 -1
- package/dist/gateway/store.d.ts.map +1 -1
- package/dist/gateway/store.js +142 -21
- package/dist/gateway/store.js.map +1 -1
- package/dist/gateway/video-jobs.d.ts +12 -0
- package/dist/gateway/video-jobs.d.ts.map +1 -0
- package/dist/gateway/video-jobs.js +24 -0
- package/dist/gateway/video-jobs.js.map +1 -0
- package/dist/proxy/buyer-proxy.d.ts +15 -0
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +166 -19
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +158 -1
- package/dist/proxy/buyer-proxy.test.js.map +1 -1
- package/dist/proxy/gateway-policy.test.js +75 -1
- package/dist/proxy/gateway-policy.test.js.map +1 -1
- package/dist/proxy/request-utils.d.ts +4 -0
- package/dist/proxy/request-utils.d.ts.map +1 -1
- package/dist/proxy/request-utils.js +4 -0
- package/dist/proxy/request-utils.js.map +1 -1
- package/dist/proxy/resource-routes.d.ts +2 -0
- package/dist/proxy/resource-routes.d.ts.map +1 -1
- package/dist/proxy/resource-routes.js +4 -0
- package/dist/proxy/resource-routes.js.map +1 -1
- package/dist/proxy/route-selection.d.ts +79 -0
- package/dist/proxy/route-selection.d.ts.map +1 -0
- package/dist/proxy/route-selection.js +79 -0
- package/dist/proxy/route-selection.js.map +1 -0
- package/dist/proxy/spend-attribution.d.ts +17 -1
- package/dist/proxy/spend-attribution.d.ts.map +1 -1
- package/dist/proxy/spend-attribution.js +45 -10
- package/dist/proxy/spend-attribution.js.map +1 -1
- package/dist/proxy/spend-attribution.test.d.ts +2 -0
- package/dist/proxy/spend-attribution.test.d.ts.map +1 -0
- package/dist/proxy/spend-attribution.test.js +63 -0
- package/dist/proxy/spend-attribution.test.js.map +1 -0
- package/dist/proxy/telemetry.d.ts +15 -0
- package/dist/proxy/telemetry.d.ts.map +1 -1
- package/dist/proxy/telemetry.js +26 -3
- package/dist/proxy/telemetry.js.map +1 -1
- package/dist/proxy/video-download.d.ts +9 -1
- package/dist/proxy/video-download.d.ts.map +1 -1
- package/dist/proxy/video-download.js +41 -6
- package/dist/proxy/video-download.js.map +1 -1
- package/dist/proxy/video-download.test.js +33 -3
- package/dist/proxy/video-download.test.js.map +1 -1
- package/package.json +7 -7
|
@@ -8,14 +8,15 @@ import { buildRoutingServices, findPinnedRoutingService } from './routing-servic
|
|
|
8
8
|
import { readFileSync, watchFile, unwatchFile } from 'node:fs';
|
|
9
9
|
import { readFile, writeFile, rename, mkdir, readdir, stat, unlink } from 'node:fs/promises';
|
|
10
10
|
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, isRoutingSelection, DEFAULT_BUYER_IDENTITY, rankModelRoutes, SellerHealthBook, orderAdaptiveRoutes, adaptiveFirstTokenTimeoutMs, sanitizePeerDisplayName, } from '@antseed/node';
|
|
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, isRoutingSelection, DEFAULT_BUYER_IDENTITY, rankModelRoutes, SellerHealthBook, orderAdaptiveRoutes, adaptiveFirstTokenTimeoutMs, sanitizePeerDisplayName, VIDEO_JOB_RETIRED_HEADER, } from '@antseed/node';
|
|
12
|
+
import { encodeServedSelection, ROUTE_SELECTION_HEADER } from './route-selection.js';
|
|
12
13
|
import { canonicalModelKey } from '@antseed/node/model-identity';
|
|
13
14
|
import { createStreamingAdapter, detectRequestServiceApiProtocol, extractRequestBodyFields, 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, takeRoutingOptions, } from './request-utils.js';
|
|
15
|
+
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, MAX_VIDEO_COST_HEADER, normalizePeerId, takeRoutingOptions, } from './request-utils.js';
|
|
15
16
|
import { BuyerIdentityLoader } from '../buyer-identities/loader.js';
|
|
16
17
|
import { buildNetworkModels, effectiveModelReputationScore, parseModelTypeFilter, } from './network-models.js';
|
|
17
18
|
import { findMissingRequiredParameters, findUnannouncedRequestParameters, findAdvertisedServiceOffer, findAdvertisedServiceProtocols, getExplicitProviderOverride, getExplicitPeerIdOverride, parseRequiredParametersHeader, REQUIRED_PARAMETERS_HEADER, resolvePeerRoutePlan, selectCandidatePeersForRouting, } from './routing.js';
|
|
18
|
-
import { computeResponseTelemetry, attachAntseedTelemetryHeaders, attachStreamingAntseedHeaders, } from './telemetry.js';
|
|
19
|
+
import { computeResponseTelemetry, attachAntseedTelemetryHeaders, ROUTER_PEER_ID_HEADER, ROUTER_RANK_HEADER, ROUTER_SERVICE_HEADER, withoutRouterHeaders, attachStreamingAntseedHeaders, } from './telemetry.js';
|
|
19
20
|
import { DEFAULT_BUYER_PEER_REFRESH_INTERVAL_MS } from '../config/defaults.js';
|
|
20
21
|
import { extractConversationIdentity, extractFirstUserSnippet, isCompletionRequestPath, isTitleGenerationRequest, parseRequestBodyObject, } from './conversation-identity.js';
|
|
21
22
|
import { ConversationStore } from './conversation-store.js';
|
|
@@ -310,7 +311,8 @@ function dispatchFailure(statusCode, responseHeaders, body) {
|
|
|
310
311
|
function writeDispatchFailure(res, result) {
|
|
311
312
|
if (res.headersSent || res.writableEnded)
|
|
312
313
|
return;
|
|
313
|
-
|
|
314
|
+
// A refusal can carry the seller's own headers: never let it name a router.
|
|
315
|
+
res.writeHead(result.statusCode, withoutRouterHeaders(result.responseHeaders));
|
|
314
316
|
res.end(result.responseBody);
|
|
315
317
|
}
|
|
316
318
|
function isBuyerFault(result) {
|
|
@@ -434,6 +436,10 @@ export function sanitizePeerBuyerFaultMarker(response) {
|
|
|
434
436
|
? { ...response, body: Buffer.from(JSON.stringify(parsed)) }
|
|
435
437
|
: response;
|
|
436
438
|
}
|
|
439
|
+
/** A non-negative integer amount of USDC base units, or null when absent or malformed. */
|
|
440
|
+
function parseUsdcBaseUnits(value) {
|
|
441
|
+
return value && /^\d{1,15}$/.test(value) ? BigInt(value) : null;
|
|
442
|
+
}
|
|
437
443
|
function buyerFaultStatusCode(faultCode) {
|
|
438
444
|
switch (faultCode) {
|
|
439
445
|
case 'invalid-request': return 400;
|
|
@@ -810,6 +816,12 @@ export class BuyerProxy {
|
|
|
810
816
|
_routingPreferences;
|
|
811
817
|
_stateWriteChain = Promise.resolve();
|
|
812
818
|
_resourceRoutes = new ResourceRoutes();
|
|
819
|
+
/** Retrieves of one video job run one at a time, so a job retired as failed cannot be delivered by a retrieve already under way. */
|
|
820
|
+
_videoJobQueues = new Map();
|
|
821
|
+
/** Jobs whose retirement is being saved: left out of the saved routes, still routable in memory until the save succeeds. */
|
|
822
|
+
/** The saved video routes existed but could not be read at start (see `start`). */
|
|
823
|
+
_resourceRoutesUnread = false;
|
|
824
|
+
_retiringVideoJobs = new Set();
|
|
813
825
|
_cachedPeers = [];
|
|
814
826
|
_cacheLastUpdatedAtMs = 0;
|
|
815
827
|
_cacheMutationEpoch = 0;
|
|
@@ -1311,7 +1323,9 @@ export class BuyerProxy {
|
|
|
1311
1323
|
now,
|
|
1312
1324
|
affinityKey: input.affinityKey ?? null,
|
|
1313
1325
|
...this._adaptiveRouting,
|
|
1314
|
-
|
|
1326
|
+
// The preview keeps the real sharing pool (so it can show who shares the
|
|
1327
|
+
// traffic) but always draws its first seller and never probes.
|
|
1328
|
+
...(input.deterministic ? { random: () => 0, explorationRate: 0 } : {}),
|
|
1315
1329
|
});
|
|
1316
1330
|
// Without routing preferences the buyer ranks by reputation, as before;
|
|
1317
1331
|
// health only moves sellers that fail often behind the others.
|
|
@@ -1340,6 +1354,7 @@ export class BuyerProxy {
|
|
|
1340
1354
|
// Conversation affinity is a soft preference: it only reorders sellers the
|
|
1341
1355
|
// policy already allows, and yields to an explicit per-model route.
|
|
1342
1356
|
const preferredPeerId = input.preferredPeerId;
|
|
1357
|
+
let conversationFirst = false;
|
|
1343
1358
|
if (preferredPeerId && !findModelRoute(effectivePolicy, requestedService)) {
|
|
1344
1359
|
const preferredIndex = candidates.findIndex((candidate) => (candidate.peer.peerId.toLowerCase() === preferredPeerId
|
|
1345
1360
|
&& !isCoolingDown(this._peerHealth.get(candidate.peer.peerId), now)
|
|
@@ -1349,7 +1364,17 @@ export class BuyerProxy {
|
|
|
1349
1364
|
if (preferred)
|
|
1350
1365
|
candidates.unshift(preferred);
|
|
1351
1366
|
}
|
|
1352
|
-
|
|
1367
|
+
conversationFirst = preferredIndex >= 0;
|
|
1368
|
+
}
|
|
1369
|
+
// How the eligible sellers were put in order. Only the buyer's adaptive
|
|
1370
|
+
// order, unsorted by the policy and with no route chain, spreads traffic.
|
|
1371
|
+
const policySort = effectivePolicy?.sort && effectivePolicy.sort !== 'balanced' ? effectivePolicy.sort : null;
|
|
1372
|
+
const selectionOrdering = policySort ?? (routingPreferences ? 'expected-cost' : 'trust');
|
|
1373
|
+
const spreads = selectionOrdering === 'expected-cost' && !chainPosition?.size;
|
|
1374
|
+
const pool = spreads ? adaptive.pool : [];
|
|
1375
|
+
const poolWeight = (candidate) => 1 / Math.max(healthOf(candidate).score, 1e-9) ** 2;
|
|
1376
|
+
const poolTotal = pool.reduce((sum, candidate) => sum + poolWeight(candidate), 0);
|
|
1377
|
+
const selections = new Map();
|
|
1353
1378
|
candidates.forEach((candidate, index) => {
|
|
1354
1379
|
const verdict = verdicts.get(candidate.peerId);
|
|
1355
1380
|
if (!verdict)
|
|
@@ -1364,8 +1389,8 @@ export class BuyerProxy {
|
|
|
1364
1389
|
verdict.reasons.push('cooling down (tried last)');
|
|
1365
1390
|
else if (health.unhealthy)
|
|
1366
1391
|
verdict.reasons.push(`failing lately: ${health.stats?.lastFailureReason ?? 'failed requests'} (tried after healthy sellers)`);
|
|
1367
|
-
if (
|
|
1368
|
-
verdict.reasons.push(`shares traffic with ${
|
|
1392
|
+
if (pool.length > 1 && pool.includes(candidate))
|
|
1393
|
+
verdict.reasons.push(`shares traffic with ${pool.length - 1} other seller(s)`);
|
|
1369
1394
|
if (modelRouteTotalPrice(candidate) === 0)
|
|
1370
1395
|
verdict.reasons.push('free');
|
|
1371
1396
|
verdict.reasons.push(`success ~${Math.round(health.successProbability * 100)}%${health.stats ? '' : ' (no data yet)'}`);
|
|
@@ -1375,8 +1400,49 @@ export class BuyerProxy {
|
|
|
1375
1400
|
const throughput = this._peerThroughput(candidate.peerId, candidate.serviceId);
|
|
1376
1401
|
if (throughput !== null)
|
|
1377
1402
|
verdict.reasons.push(`~${Math.round(throughput)} tok/s`);
|
|
1403
|
+
const healthKey = this._healthServiceKey(candidate.serviceId);
|
|
1404
|
+
const stats = health.stats;
|
|
1405
|
+
const inPool = pool.includes(candidate);
|
|
1406
|
+
const maxConcurrency = typeof candidate.peer.maxConcurrency === 'number' && candidate.peer.maxConcurrency > 0 ? candidate.peer.maxConcurrency : null;
|
|
1407
|
+
const selection = {
|
|
1408
|
+
expectedCostUsd: health.score,
|
|
1409
|
+
expectedPriceUsd: health.expectedPriceUsd,
|
|
1410
|
+
expectedSeconds: health.expectedSeconds,
|
|
1411
|
+
successRate: health.successProbability,
|
|
1412
|
+
observations: stats ? stats.successes + stats.failures : 0,
|
|
1413
|
+
record: !stats ? 'none' : this._sellerHealth.hasRecord(candidate.peerId, healthKey) ? 'model' : 'other-models',
|
|
1414
|
+
firstTokenMs: latency,
|
|
1415
|
+
tokensPerSecond: throughput,
|
|
1416
|
+
lastSuccessAt: stats && stats.lastSuccessAt > 0 ? stats.lastSuccessAt : null,
|
|
1417
|
+
lastFailureAt: stats && stats.lastFailureAt > 0 ? stats.lastFailureAt : null,
|
|
1418
|
+
lastFailureReason: stats?.lastFailureReason ?? null,
|
|
1419
|
+
failing: health.unhealthy,
|
|
1420
|
+
coolingDown: isCooling(candidate),
|
|
1421
|
+
stale: health.stale,
|
|
1422
|
+
ordering: selectionOrdering,
|
|
1423
|
+
sharesTraffic: inPool && pool.length > 1,
|
|
1424
|
+
trafficShare: inPool && pool.length > 1 && poolTotal > 0 ? poolWeight(candidate) / poolTotal : null,
|
|
1425
|
+
poolSize: pool.length,
|
|
1426
|
+
channel: modelRouteTotalPrice(candidate) === 0 ? 'free' : openChannels ? (openChannels.has(candidate.peerId.toLowerCase()) ? 'open' : 'new') : null,
|
|
1427
|
+
inFlight: this._inFlightBySeller.get(candidate.peerId.toLowerCase()) ?? 0,
|
|
1428
|
+
maxConcurrency,
|
|
1429
|
+
routeChainPosition: position !== undefined ? position + 1 : null,
|
|
1430
|
+
};
|
|
1431
|
+
verdict.selection = selection;
|
|
1432
|
+
selections.set(candidate, selection);
|
|
1378
1433
|
});
|
|
1379
|
-
|
|
1434
|
+
// How the first seller came to be first, for the request's record.
|
|
1435
|
+
const first = candidates[0];
|
|
1436
|
+
let firstPick = 'order';
|
|
1437
|
+
if (first) {
|
|
1438
|
+
if (first === adaptive.explored)
|
|
1439
|
+
firstPick = 'exploration';
|
|
1440
|
+
else if (conversationFirst || (input.affinityKey && pool.length > 1 && pool.includes(first) && first === adaptive.ordered[0]))
|
|
1441
|
+
firstPick = 'conversation';
|
|
1442
|
+
else if (first === adaptive.ordered[0] && pool.includes(first))
|
|
1443
|
+
firstPick = pool.length > 1 ? 'spread' : 'best';
|
|
1444
|
+
}
|
|
1445
|
+
return { candidates, verdicts: [...verdicts.values()], effectivePolicy, explored: adaptive.explored, selections, firstPick };
|
|
1380
1446
|
}
|
|
1381
1447
|
/**
|
|
1382
1448
|
* Without routing preferences: best effective reputation first, and only
|
|
@@ -1434,6 +1500,7 @@ export class BuyerProxy {
|
|
|
1434
1500
|
inputUsdPerMillion: verdict.offer.inputUsdPerMillion ?? null,
|
|
1435
1501
|
outputUsdPerMillion: verdict.offer.outputUsdPerMillion ?? null,
|
|
1436
1502
|
trustScore: normalizedModelReputationScore(verdict.peer),
|
|
1503
|
+
selection: modelAllowed && verdict.eligible ? verdict.selection ?? null : null,
|
|
1437
1504
|
}))
|
|
1438
1505
|
.sort((a, b) => (a.rank ?? Number.POSITIVE_INFINITY) - (b.rank ?? Number.POSITIVE_INFINITY)
|
|
1439
1506
|
|| a.peerId.localeCompare(b.peerId));
|
|
@@ -1648,7 +1715,13 @@ export class BuyerProxy {
|
|
|
1648
1715
|
const state = JSON.parse(await readFile(this._stateFile, 'utf8'));
|
|
1649
1716
|
this._resourceRoutes.hydrate(state.resourceRoutes);
|
|
1650
1717
|
}
|
|
1651
|
-
catch {
|
|
1718
|
+
catch (error) {
|
|
1719
|
+
// Routes that could not be read may come back on a later start: until then no job is said to be retired.
|
|
1720
|
+
if (error.code !== 'ENOENT') {
|
|
1721
|
+
this._resourceRoutesUnread = true;
|
|
1722
|
+
console.warn(`[buyer-proxy] Could not read saved video routes: ${error instanceof Error ? error.message : String(error)}`);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1652
1725
|
// Adopt persisted session overrides (peer pin, default routed model) so
|
|
1653
1726
|
// they survive daemon restart. A --peer CLI flag beats the persisted pin
|
|
1654
1727
|
// at startup; runtime `connection set` writes still take over via the
|
|
@@ -1909,8 +1982,50 @@ export class BuyerProxy {
|
|
|
1909
1982
|
}).catch(() => { });
|
|
1910
1983
|
return this._stateWriteChain;
|
|
1911
1984
|
}
|
|
1985
|
+
async _serializeVideoJob(job, run) {
|
|
1986
|
+
const previous = this._videoJobQueues.get(job) ?? Promise.resolve();
|
|
1987
|
+
const current = previous.then(run, run);
|
|
1988
|
+
const tail = current.catch(() => { });
|
|
1989
|
+
this._videoJobQueues.set(job, tail);
|
|
1990
|
+
try {
|
|
1991
|
+
await current;
|
|
1992
|
+
}
|
|
1993
|
+
finally {
|
|
1994
|
+
if (this._videoJobQueues.get(job) === tail)
|
|
1995
|
+
this._videoJobQueues.delete(job);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Retires a video job that failed or that its seller no longer has: later
|
|
2000
|
+
* retrieves get `video_route_not_found` and never reach the seller, so it can
|
|
2001
|
+
* never be delivered or charged, and a gateway may release the price it held
|
|
2002
|
+
* for it. False (route kept) when the retirement could not be saved, so a
|
|
2003
|
+
* restart cannot bring the job back.
|
|
2004
|
+
*/
|
|
2005
|
+
async _retireVideoJob(protocol, resourceId) {
|
|
2006
|
+
if (!this._resourceRoutes.resolve(protocol, resourceId))
|
|
2007
|
+
return true;
|
|
2008
|
+
// The route stays usable until the retirement is saved: a retrieve arriving meanwhile waits its turn
|
|
2009
|
+
// (retrieves of one job run one at a time) instead of seeing the job as gone before that is durable.
|
|
2010
|
+
const key = `${protocol}\n${resourceId}`;
|
|
2011
|
+
this._retiringVideoJobs.add(key);
|
|
2012
|
+
try {
|
|
2013
|
+
await this._persistResourceRoutes();
|
|
2014
|
+
this._resourceRoutes.forget(protocol, resourceId);
|
|
2015
|
+
return true;
|
|
2016
|
+
}
|
|
2017
|
+
catch (error) {
|
|
2018
|
+
console.error('[proxy] Retired video route was not saved; the job is kept:', error);
|
|
2019
|
+
return false;
|
|
2020
|
+
}
|
|
2021
|
+
finally {
|
|
2022
|
+
this._retiringVideoJobs.delete(key);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
1912
2025
|
async _persistResourceRoutes() {
|
|
1913
|
-
const write = this._stateWriteChain.then(() => mergeJsonStateFile(this._stateDir, this._stateFile, {
|
|
2026
|
+
const write = this._stateWriteChain.then(() => mergeJsonStateFile(this._stateDir, this._stateFile, {
|
|
2027
|
+
resourceRoutes: this._resourceRoutes.snapshot().filter((route) => !this._retiringVideoJobs.has(`${route.protocol}\n${route.resourceId}`)),
|
|
2028
|
+
}));
|
|
1914
2029
|
this._stateWriteChain = write.catch(() => { });
|
|
1915
2030
|
return write;
|
|
1916
2031
|
}
|
|
@@ -3247,7 +3362,10 @@ export class BuyerProxy {
|
|
|
3247
3362
|
// Re-classify with the body: some APIs (Venice) carry the job ID in it.
|
|
3248
3363
|
const prepared = prepareVideoRequest(nativeVideoRoute(serializedReq) ?? nativeVideo, serializedReq.headers, this._resourceRoutes);
|
|
3249
3364
|
if ('error' in prepared) {
|
|
3250
|
-
|
|
3365
|
+
// No route for the job: this buyer can never deliver or charge it. Unless its saved routes could not be
|
|
3366
|
+
// read at start, when the job may still be there on the next one.
|
|
3367
|
+
const retired = prepared.error.body.error.code === 'video_route_not_found' && !this._resourceRoutesUnread ? { [VIDEO_JOB_RETIRED_HEADER]: '1' } : {};
|
|
3368
|
+
res.writeHead(prepared.error.statusCode, { 'content-type': 'application/json', ...retired });
|
|
3251
3369
|
res.end(JSON.stringify(prepared.error.body));
|
|
3252
3370
|
return;
|
|
3253
3371
|
}
|
|
@@ -3386,6 +3504,11 @@ export class BuyerProxy {
|
|
|
3386
3504
|
const conversationId = this._trackRouterConversation(context);
|
|
3387
3505
|
if (conversationId && selection.service)
|
|
3388
3506
|
this._conversations.setLastRouter(conversationId, `${selection.service.peerId}@${selection.service.serviceId}`);
|
|
3507
|
+
// Merged into whatever answer this request gets, so a gateway can tell routed requests (and failed rankings) apart.
|
|
3508
|
+
if (selection.service) {
|
|
3509
|
+
res.setHeader(ROUTER_PEER_ID_HEADER, selection.service.peerId.toLowerCase());
|
|
3510
|
+
res.setHeader(ROUTER_SERVICE_HEADER, selection.service.serviceId);
|
|
3511
|
+
}
|
|
3389
3512
|
let recommendations;
|
|
3390
3513
|
try {
|
|
3391
3514
|
const client = this._modelRoutingClient;
|
|
@@ -3403,7 +3526,7 @@ export class BuyerProxy {
|
|
|
3403
3526
|
signal: AbortSignal.any([signal, AbortSignal.timeout(120_000)]),
|
|
3404
3527
|
onRoutingRequest: requestId => {
|
|
3405
3528
|
if (spendAttributionTag)
|
|
3406
|
-
this._spendAttribution.track(requestId, spendAttributionTag);
|
|
3529
|
+
this._spendAttribution.track(requestId, spendAttributionTag, 'routing');
|
|
3407
3530
|
if (conversationId)
|
|
3408
3531
|
this._trackRequestConversation(requestId, conversationId, undefined, 'routing');
|
|
3409
3532
|
},
|
|
@@ -3426,10 +3549,11 @@ export class BuyerProxy {
|
|
|
3426
3549
|
return;
|
|
3427
3550
|
}
|
|
3428
3551
|
let lastFailure = null;
|
|
3429
|
-
for (const recommendation of recommendations) {
|
|
3552
|
+
for (const [index, recommendation] of recommendations.entries()) {
|
|
3430
3553
|
// Never switch sellers once the user has disconnected or a response has started.
|
|
3431
3554
|
if (signal.aborted || res.headersSent || res.writableEnded)
|
|
3432
3555
|
return;
|
|
3556
|
+
res.setHeader(ROUTER_RANK_HEADER, String(index + 1));
|
|
3433
3557
|
// Each attempt has its own billing ID, but all attempts count as one chat turn.
|
|
3434
3558
|
const attempt = requestForRecommendation(request, recommendation, randomUUID());
|
|
3435
3559
|
if (spendAttributionTag)
|
|
@@ -3703,7 +3827,7 @@ export class BuyerProxy {
|
|
|
3703
3827
|
({ candidatePeers: modelPeers, routePlanByPeerId: modelPlans } = selectModelPeers(discoveredPeers));
|
|
3704
3828
|
}
|
|
3705
3829
|
const router = this._node.router;
|
|
3706
|
-
const { candidates, effectivePolicy, explored } = this._planModelRoutes({
|
|
3830
|
+
const { candidates, effectivePolicy, explored, selections, firstPick } = this._planModelRoutes({
|
|
3707
3831
|
modelPeers,
|
|
3708
3832
|
modelPlans,
|
|
3709
3833
|
request: serializedReq,
|
|
@@ -3798,6 +3922,18 @@ export class BuyerProxy {
|
|
|
3798
3922
|
continue;
|
|
3799
3923
|
}
|
|
3800
3924
|
}
|
|
3925
|
+
// Why this seller serves the request, for the gateway's request log. A
|
|
3926
|
+
// seller that fails is replaced by the next one's before any header goes out.
|
|
3927
|
+
const selection = selections.get(selected);
|
|
3928
|
+
if (selection && !res.headersSent) {
|
|
3929
|
+
res.setHeader(ROUTE_SELECTION_HEADER, encodeServedSelection({
|
|
3930
|
+
...selection,
|
|
3931
|
+
rank: index + 1,
|
|
3932
|
+
of: candidates.length,
|
|
3933
|
+
attempt: index,
|
|
3934
|
+
pick: index === 0 ? firstPick : failedSellers.size > 0 || lastRetry !== null ? 'failover' : 'order',
|
|
3935
|
+
}));
|
|
3936
|
+
}
|
|
3801
3937
|
for (let peerAttempt = 0; peerAttempt < MODEL_RATE_LIMIT_MAX_ATTEMPTS_PER_PEER; peerAttempt += 1) {
|
|
3802
3938
|
log(`Auto-selected peer ${selected.peer.peerId.slice(0, 12)}... for model="${requestedService}" `
|
|
3803
3939
|
+ `service="${selected.serviceId}" reputation=${selected.reputation} `
|
|
@@ -4139,7 +4275,7 @@ export class BuyerProxy {
|
|
|
4139
4275
|
+ `did not announce for this service: ${unannounced.join(', ')} — the upstream may ignore or reject them`);
|
|
4140
4276
|
}
|
|
4141
4277
|
}
|
|
4142
|
-
const { 'x-antseed-pin-peer': _pinPeer, 'x-antseed-prefer-peer': _preferPeer, [BUYER_IDENTITY_HEADER]: buyerIdentity, [REQUIRED_PARAMETERS_HEADER]: _requiredParameters, 'x-vpr-session-id': _vprSession,
|
|
4278
|
+
const { 'x-antseed-pin-peer': _pinPeer, 'x-antseed-prefer-peer': _preferPeer, [BUYER_IDENTITY_HEADER]: buyerIdentity, [MAX_VIDEO_COST_HEADER]: maxVideoCostHeader, [REQUIRED_PARAMETERS_HEADER]: _requiredParameters, 'x-vpr-session-id': _vprSession,
|
|
4143
4279
|
// Legacy desktop builds (pre AntStation → VPR rename) still send this.
|
|
4144
4280
|
'x-antstation-session-id': _antstationSession, ...headersForPeer } = serializedReq.headers;
|
|
4145
4281
|
let requestForPeer = {
|
|
@@ -4213,9 +4349,18 @@ export class BuyerProxy {
|
|
|
4213
4349
|
const wantsStreaming = clientWantsStreaming && !videoRoute;
|
|
4214
4350
|
if (videoRoute?.action === 'retrieve') {
|
|
4215
4351
|
// The seller hands a job only to the buyer that created it: retrieve with the same wallet.
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4352
|
+
const { protocol, resourceId } = videoRoute;
|
|
4353
|
+
await this._serializeVideoJob(`${protocol}\n${resourceId ?? ''}`, async () => {
|
|
4354
|
+
// Checked again in turn: a retrieve queued behind one that retired the job must not reach the seller.
|
|
4355
|
+
if (!resourceId || !this._resourceRoutes.resolve(protocol, resourceId)) {
|
|
4356
|
+
res.writeHead(404, { 'content-type': 'application/json', [VIDEO_JOB_RETIRED_HEADER]: '1' });
|
|
4357
|
+
res.end(JSON.stringify({ error: { code: 'video_route_not_found', message: 'Unknown video job' } }));
|
|
4358
|
+
return;
|
|
4359
|
+
}
|
|
4360
|
+
await downloadVideo(requestForPeer, res, (request, callbacks, signal) => this._node.sendRequestStream(selectedPeer, request, callbacks, {
|
|
4361
|
+
signal, pinned: true, ...(buyerIdentity ? { buyerIdentity } : {}),
|
|
4362
|
+
}), requestSignal, () => this._retireVideoJob(protocol, resourceId));
|
|
4363
|
+
});
|
|
4219
4364
|
return { done: true };
|
|
4220
4365
|
}
|
|
4221
4366
|
const peerResponseProtocol = selectedRoutePlan.selection?.targetProtocol ?? requestProtocol;
|
|
@@ -4370,10 +4515,12 @@ export class BuyerProxy {
|
|
|
4370
4515
|
return { done: true, emptyAnswer: answeredEmpty };
|
|
4371
4516
|
}
|
|
4372
4517
|
else {
|
|
4518
|
+
const maxVideoCostUsdc = videoRoute?.action === 'create' ? parseUsdcBaseUnits(maxVideoCostHeader) : null;
|
|
4373
4519
|
const upstreamResponse = await this._node.sendRequest(selectedPeer, requestForPeer, {
|
|
4374
4520
|
signal: attemptSignal,
|
|
4375
4521
|
pinned,
|
|
4376
4522
|
...(buyerIdentity ? { buyerIdentity } : {}),
|
|
4523
|
+
...(maxVideoCostUsdc !== null ? { maxVideoCostUsdc } : {}),
|
|
4377
4524
|
});
|
|
4378
4525
|
if (upstreamResponse.statusCode >= 400 && !adaptResponse) {
|
|
4379
4526
|
log(`Upstream raw error detail: ${summarizeErrorResponse(upstreamResponse)}`);
|