@antseed/cli 0.1.141 → 0.1.143
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/sweep.d.ts.map +1 -1
- package/dist/cli/commands/buyer/sweep.js +11 -5
- package/dist/cli/commands/buyer/sweep.js.map +1 -1
- package/dist/cli/commands/payments.d.ts.map +1 -1
- package/dist/cli/commands/payments.js +37 -20
- package/dist/cli/commands/payments.js.map +1 -1
- package/dist/cli/commands/system-proxy/ca-trust-state.d.ts +8 -0
- package/dist/cli/commands/system-proxy/ca-trust-state.d.ts.map +1 -0
- package/dist/cli/commands/system-proxy/ca-trust-state.js +20 -0
- package/dist/cli/commands/system-proxy/ca-trust-state.js.map +1 -0
- package/dist/cli/commands/system-proxy/index.d.ts.map +1 -1
- package/dist/cli/commands/system-proxy/index.js +2 -0
- package/dist/cli/commands/system-proxy/index.js.map +1 -1
- package/dist/cli/commands/system-proxy/install-ca.d.ts.map +1 -1
- package/dist/cli/commands/system-proxy/install-ca.js +9 -0
- package/dist/cli/commands/system-proxy/install-ca.js.map +1 -1
- package/dist/cli/commands/system-proxy/start.d.ts.map +1 -1
- package/dist/cli/commands/system-proxy/start.js +3 -1
- package/dist/cli/commands/system-proxy/start.js.map +1 -1
- package/dist/cli/commands/wrapped-tools.d.ts.map +1 -1
- package/dist/cli/commands/wrapped-tools.js +7 -0
- package/dist/cli/commands/wrapped-tools.js.map +1 -1
- package/dist/cli/commands/wrapped-tools.test.js +6 -1
- package/dist/cli/commands/wrapped-tools.test.js.map +1 -1
- package/dist/proxy/buyer-proxy.d.ts +38 -1
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +340 -23
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +382 -11
- package/dist/proxy/buyer-proxy.test.js.map +1 -1
- package/dist/proxy/conversation-identity.d.ts +79 -0
- package/dist/proxy/conversation-identity.d.ts.map +1 -0
- package/dist/proxy/conversation-identity.js +302 -0
- package/dist/proxy/conversation-identity.js.map +1 -0
- package/dist/proxy/conversation-identity.test.d.ts +2 -0
- package/dist/proxy/conversation-identity.test.d.ts.map +1 -0
- package/dist/proxy/conversation-identity.test.js +236 -0
- package/dist/proxy/conversation-identity.test.js.map +1 -0
- package/dist/proxy/conversation-store.d.ts +96 -0
- package/dist/proxy/conversation-store.d.ts.map +1 -0
- package/dist/proxy/conversation-store.js +242 -0
- package/dist/proxy/conversation-store.js.map +1 -0
- package/dist/proxy/conversation-store.test.d.ts +2 -0
- package/dist/proxy/conversation-store.test.d.ts.map +1 -0
- package/dist/proxy/conversation-store.test.js +277 -0
- package/dist/proxy/conversation-store.test.js.map +1 -0
- package/dist/proxy/count-tokens.d.ts +20 -0
- package/dist/proxy/count-tokens.d.ts.map +1 -0
- package/dist/proxy/count-tokens.js +75 -0
- package/dist/proxy/count-tokens.js.map +1 -0
- package/dist/proxy/count-tokens.test.d.ts +2 -0
- package/dist/proxy/count-tokens.test.d.ts.map +1 -0
- package/dist/proxy/count-tokens.test.js +56 -0
- package/dist/proxy/count-tokens.test.js.map +1 -0
- package/dist/proxy/request-utils.d.ts +38 -0
- package/dist/proxy/request-utils.d.ts.map +1 -1
- package/dist/proxy/request-utils.js +84 -8
- package/dist/proxy/request-utils.js.map +1 -1
- package/dist/proxy/telemetry.d.ts +6 -0
- package/dist/proxy/telemetry.d.ts.map +1 -1
- package/dist/proxy/telemetry.js +53 -13
- package/dist/proxy/telemetry.js.map +1 -1
- package/dist/system-proxy/ca-manager.d.ts +67 -0
- package/dist/system-proxy/ca-manager.d.ts.map +1 -1
- package/dist/system-proxy/ca-manager.js +258 -39
- package/dist/system-proxy/ca-manager.js.map +1 -1
- package/dist/system-proxy/ca-manager.test.js +50 -1
- package/dist/system-proxy/ca-manager.test.js.map +1 -1
- package/dist/system-proxy/profiles.d.ts +2 -1
- package/dist/system-proxy/profiles.d.ts.map +1 -1
- package/dist/system-proxy/profiles.js +55 -4
- package/dist/system-proxy/profiles.js.map +1 -1
- package/dist/system-proxy/proxy-server.d.ts +18 -1
- package/dist/system-proxy/proxy-server.d.ts.map +1 -1
- package/dist/system-proxy/proxy-server.js +91 -38
- package/dist/system-proxy/proxy-server.js.map +1 -1
- package/dist/system-proxy/proxy-server.test.js +142 -13
- package/dist/system-proxy/proxy-server.test.js.map +1 -1
- package/dist/system-proxy/system-proxy.d.ts +1 -0
- package/dist/system-proxy/system-proxy.d.ts.map +1 -1
- package/dist/system-proxy/system-proxy.js +57 -15
- package/dist/system-proxy/system-proxy.js.map +1 -1
- package/dist/system-proxy/types.d.ts +17 -4
- package/dist/system-proxy/types.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buyer-proxy.d.ts","sourceRoot":"","sources":["../../src/proxy/buyer-proxy.ts"],"names":[],"mappings":"AAKA,OAAO,EAIL,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"buyer-proxy.d.ts","sourceRoot":"","sources":["../../src/proxy/buyer-proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,14 +5,17 @@ import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { ANTSEED_ATTEST_PATH, computeOnChainReputationScore, decodeSweepRequest, } from '@antseed/node';
|
|
7
7
|
import { createStreamingAdapter, detectRequestServiceApiProtocol, transformRequest, transformResponse, } from './service-api-adapter.js';
|
|
8
|
-
import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, rewritePeerPinnedServiceInBody, } from './request-utils.js';
|
|
8
|
+
import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, parsePeerPinnedService, rewritePeerPinnedServiceInBody, substituteRoutedModelAlias, overrideRoutedModelInBody, ROUTED_MODEL_ALIAS, SYSTEM_PROXY_SOURCE_HEADER, SYSTEM_ROUTED_MODEL_HEADER, } from './request-utils.js';
|
|
9
9
|
import { getExplicitProviderOverride, getExplicitPeerIdOverride, resolvePeerRoutePlan, selectCandidatePeersForRouting, } from './routing.js';
|
|
10
10
|
import { computeResponseTelemetry, attachAntseedTelemetryHeaders, attachStreamingAntseedHeaders, } from './telemetry.js';
|
|
11
11
|
import { DEFAULT_BUYER_PEER_REFRESH_INTERVAL_MS } from '../config/defaults.js';
|
|
12
|
+
import { extractConversationIdentity, extractFirstUserSnippet, isCompletionRequestPath, isTitleGenerationRequest, parseRequestBodyObject, } from './conversation-identity.js';
|
|
13
|
+
import { ConversationStore } from './conversation-store.js';
|
|
14
|
+
import { estimateAnthropicPromptTokens, isCountTokensPath } from './count-tokens.js';
|
|
12
15
|
import { getCachedVerdict, runVerifier, verifierSupportFingerprint } from '../plugins/verifier.js';
|
|
13
16
|
// Re-export for backward compatibility (used by tests and other consumers)
|
|
14
17
|
export { selectCandidatePeersForRouting } from './routing.js';
|
|
15
|
-
export { parsePeerPinnedService, rewritePeerPinnedServiceInBody } from './request-utils.js';
|
|
18
|
+
export { parsePeerPinnedService, rewritePeerPinnedServiceInBody, substituteRoutedModelAlias, ROUTED_MODEL_ALIAS } from './request-utils.js';
|
|
16
19
|
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
17
20
|
function isControlPlaneServicesPath(path) {
|
|
18
21
|
return path.toLowerCase().startsWith('/v1/models');
|
|
@@ -48,6 +51,12 @@ export function isModelNotFoundResponse(response) {
|
|
|
48
51
|
*/
|
|
49
52
|
const CARRY_FORWARD_TTL_MS = 2 * 60 * 60_000;
|
|
50
53
|
const PEER_FAILURE_WINDOW_MS = 5 * 60_000;
|
|
54
|
+
/**
|
|
55
|
+
* Requests kept in the spend-attribution map. Entries outlive their request on
|
|
56
|
+
* purpose (a seller-initiated auth can land after the response), so this is
|
|
57
|
+
* sized for in-flight plus recently-finished traffic, not for concurrency.
|
|
58
|
+
*/
|
|
59
|
+
const MAX_TRACKED_REQUEST_CONVERSATIONS = 512;
|
|
51
60
|
/** Min gap between background peer refreshes triggered by model_not_found responses. */
|
|
52
61
|
const MODEL_NOT_FOUND_REFRESH_THROTTLE_MS = 30_000;
|
|
53
62
|
/** Verification is expensive; bound how many verdicts we retain (TTL = peer-cache TTL). */
|
|
@@ -310,6 +319,20 @@ export class BuyerProxy {
|
|
|
310
319
|
_stateFile;
|
|
311
320
|
_stateFileWatching = false;
|
|
312
321
|
_pinnedPeer;
|
|
322
|
+
/**
|
|
323
|
+
* Route substituted for the `antseed` model alias (`<peerId>@<service>`).
|
|
324
|
+
* Set via `POST /_antseed/route` (the desktop keeps it on the current VPR
|
|
325
|
+
* selection) and persisted in buyer.state.json like the session peer pin.
|
|
326
|
+
*/
|
|
327
|
+
_defaultRoutedModel = null;
|
|
328
|
+
_conversations;
|
|
329
|
+
/**
|
|
330
|
+
* Wall-clock of the last model-request activity (dispatch or streamed
|
|
331
|
+
* frame). Exposed on /_antseed/buyer-usage so the desktop pill can show a
|
|
332
|
+
* live "traffic" signal without depending on debug logging. A single field
|
|
333
|
+
* write per frame — negligible next to the routing/payment/stream work.
|
|
334
|
+
*/
|
|
335
|
+
_lastModelActivityAt = 0;
|
|
313
336
|
_verifier;
|
|
314
337
|
_verifyCache = new Map();
|
|
315
338
|
_stateWatchDebounce = null;
|
|
@@ -319,11 +342,24 @@ export class BuyerProxy {
|
|
|
319
342
|
_cacheMutationEpoch = 0;
|
|
320
343
|
_peerRefreshPromise = null;
|
|
321
344
|
_lastStaleCacheLogAtMs = 0;
|
|
345
|
+
_startedAtMs = 0;
|
|
346
|
+
/** After a network switch the DHT routing table can stay populated with stale
|
|
347
|
+
nodes, so repeated empty sweeps are the reachability signal — not node count. */
|
|
348
|
+
_consecutiveEmptyDiscoveries = 0;
|
|
322
349
|
_lastModelNotFoundRefreshAtMs = 0;
|
|
323
350
|
_bgRefreshHandle = null;
|
|
324
351
|
_peerFailures = new Map();
|
|
325
352
|
/** Latest relayer receipt per sweep authNonce, for CLI progress polling. */
|
|
326
353
|
_sweepReceipts = new Map();
|
|
354
|
+
/**
|
|
355
|
+
* requestId -> the conversation that issued it, so the node's per-request
|
|
356
|
+
* spend events can be attributed to a chat. Only the proxy knows this
|
|
357
|
+
* mapping; the payment layer sees a seller and a request id.
|
|
358
|
+
*
|
|
359
|
+
* `counted` guards requestCount: one request can produce several spend
|
|
360
|
+
* deltas (buyer- and seller-initiated auth both advance the cumulative).
|
|
361
|
+
*/
|
|
362
|
+
_requestConversations = new Map();
|
|
327
363
|
constructor(config) {
|
|
328
364
|
this._node = config.node;
|
|
329
365
|
this._verifier = config.verifier;
|
|
@@ -332,6 +368,7 @@ export class BuyerProxy {
|
|
|
332
368
|
this._peerCacheTtlMs = Math.max(0, config.peerCacheTtlMs ?? Math.max(6 * 60_000, this._bgRefreshIntervalMs + 60_000));
|
|
333
369
|
this._stateDir = config.dataDir;
|
|
334
370
|
this._stateFile = join(config.dataDir, 'buyer.state.json');
|
|
371
|
+
this._conversations = new ConversationStore(config.dataDir);
|
|
335
372
|
this._pinnedPeer = config.pinnedPeerId?.toLowerCase() ?? null;
|
|
336
373
|
this._server = createServer((req, res) => {
|
|
337
374
|
this._handleRequest(req, res).catch((err) => {
|
|
@@ -353,6 +390,12 @@ export class BuyerProxy {
|
|
|
353
390
|
}
|
|
354
391
|
});
|
|
355
392
|
}
|
|
393
|
+
const spendEventNode = this._node;
|
|
394
|
+
if (typeof spendEventNode.on === 'function') {
|
|
395
|
+
spendEventNode.on('payment:spend', (event) => {
|
|
396
|
+
this._attributeSpend(event);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
356
399
|
const eventNode = this._node;
|
|
357
400
|
if (typeof eventNode.on === 'function') {
|
|
358
401
|
eventNode.on('peers:discovered', (peers) => {
|
|
@@ -363,18 +406,47 @@ export class BuyerProxy {
|
|
|
363
406
|
});
|
|
364
407
|
}
|
|
365
408
|
}
|
|
409
|
+
/** Roll a signed spend delta into the conversation that triggered it. */
|
|
410
|
+
_attributeSpend(event) {
|
|
411
|
+
if (!event.requestId)
|
|
412
|
+
return;
|
|
413
|
+
const entry = this._requestConversations.get(event.requestId);
|
|
414
|
+
if (!entry)
|
|
415
|
+
return;
|
|
416
|
+
this._conversations.addSpend(entry.convId, {
|
|
417
|
+
amountUsdc: event.amountUsdc,
|
|
418
|
+
inputTokens: event.inputTokens,
|
|
419
|
+
cachedInputTokens: event.cachedInputTokens,
|
|
420
|
+
outputTokens: event.outputTokens,
|
|
421
|
+
}, !entry.counted);
|
|
422
|
+
entry.counted = true;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Remember which chat a request belongs to for the life of the request plus
|
|
426
|
+
* a grace window — the seller-initiated auth path can land just after the
|
|
427
|
+
* response is returned. Bounded so a leaked id can't grow the map forever.
|
|
428
|
+
*/
|
|
429
|
+
_trackRequestConversation(requestId, convId) {
|
|
430
|
+
this._requestConversations.set(requestId, { convId, counted: false });
|
|
431
|
+
while (this._requestConversations.size > MAX_TRACKED_REQUEST_CONVERSATIONS) {
|
|
432
|
+
const oldest = this._requestConversations.keys().next().value;
|
|
433
|
+
if (oldest === undefined)
|
|
434
|
+
break;
|
|
435
|
+
this._requestConversations.delete(oldest);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
366
438
|
async start() {
|
|
439
|
+
this._startedAtMs = Date.now();
|
|
367
440
|
// Hydrate the in-memory peer cache from the persisted state file BEFORE
|
|
368
441
|
// the server starts accepting requests. This lets the first request after
|
|
369
442
|
// startup route from the warm cache without blocking on DHT discovery.
|
|
370
443
|
// The background refresh still runs to pick up fresh peers and IP changes.
|
|
371
444
|
await this._hydratePeersFromStateFile();
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
445
|
+
// Adopt persisted session overrides (peer pin, default routed model) so
|
|
446
|
+
// they survive daemon restart. A --peer CLI flag beats the persisted pin
|
|
447
|
+
// at startup; runtime `connection set` writes still take over via the
|
|
448
|
+
// state-file watcher.
|
|
449
|
+
await this._reloadSessionOverrides({ preservePeerPin: this._pinnedPeer !== null });
|
|
378
450
|
await new Promise((resolve, reject) => {
|
|
379
451
|
this._server.once('error', reject);
|
|
380
452
|
this._server.listen(this._port, '127.0.0.1', () => {
|
|
@@ -428,6 +500,7 @@ export class BuyerProxy {
|
|
|
428
500
|
this._bgRefreshHandle = null;
|
|
429
501
|
}
|
|
430
502
|
await this._writeStateFile('stopped');
|
|
503
|
+
await this._conversations.flush();
|
|
431
504
|
return new Promise((resolve) => {
|
|
432
505
|
this._server.close(() => resolve());
|
|
433
506
|
});
|
|
@@ -455,20 +528,28 @@ export class BuyerProxy {
|
|
|
455
528
|
// watcher setup failed; non-fatal
|
|
456
529
|
}
|
|
457
530
|
}
|
|
458
|
-
async _reloadSessionOverrides() {
|
|
531
|
+
async _reloadSessionOverrides(opts = {}) {
|
|
459
532
|
try {
|
|
460
533
|
const raw = await readFile(this._stateFile, 'utf-8');
|
|
461
534
|
const parsed = JSON.parse(raw);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
535
|
+
if (!opts.preservePeerPin) {
|
|
536
|
+
const pinnedPeer = typeof parsed.pinnedPeerId === 'string' && parsed.pinnedPeerId.trim().length > 0
|
|
537
|
+
? parsed.pinnedPeerId.trim().toLowerCase()
|
|
538
|
+
: null;
|
|
539
|
+
this._pinnedPeer = pinnedPeer;
|
|
540
|
+
}
|
|
541
|
+
const routedModel = typeof parsed.defaultRoutedModel === 'string' ? parsed.defaultRoutedModel.trim() : '';
|
|
542
|
+
this._defaultRoutedModel = parsePeerPinnedService(routedModel) ? routedModel : null;
|
|
543
|
+
log(`Session overrides reloaded: peer=${this._pinnedPeer ?? 'none'} route=${this._defaultRoutedModel ?? 'none'}`);
|
|
467
544
|
}
|
|
468
545
|
catch {
|
|
469
546
|
// state file unreadable; keep current values
|
|
470
547
|
}
|
|
471
548
|
}
|
|
549
|
+
/** Stamp the last model-request activity time (dispatch or streamed frame). */
|
|
550
|
+
_markModelActivity() {
|
|
551
|
+
this._lastModelActivityAt = Date.now();
|
|
552
|
+
}
|
|
472
553
|
/** Serialised read-modify-write to buyer.state.json. Returns the queued write promise. */
|
|
473
554
|
_mergeStateFile(patch) {
|
|
474
555
|
this._stateWriteChain = this._stateWriteChain.then(async () => {
|
|
@@ -494,11 +575,11 @@ export class BuyerProxy {
|
|
|
494
575
|
return this._stateWriteChain;
|
|
495
576
|
}
|
|
496
577
|
async _writeStateFile(state) {
|
|
497
|
-
// When stopping, preserve whatever
|
|
578
|
+
// When stopping, preserve whatever session overrides are already
|
|
498
579
|
// in the file — the debounce may have been cancelled before
|
|
499
580
|
// _reloadSessionOverrides could commit the latest CLI-written values.
|
|
500
581
|
const sessionOverrides = state === 'connected'
|
|
501
|
-
? { pinnedPeerId: this._pinnedPeer }
|
|
582
|
+
? { pinnedPeerId: this._pinnedPeer, defaultRoutedModel: this._defaultRoutedModel }
|
|
502
583
|
: {};
|
|
503
584
|
await this._mergeStateFile({
|
|
504
585
|
state,
|
|
@@ -695,9 +776,11 @@ export class BuyerProxy {
|
|
|
695
776
|
this._peerRefreshPromise = (async () => {
|
|
696
777
|
const peers = await this._discoverPeersFromNetwork();
|
|
697
778
|
if (peers.length > 0) {
|
|
779
|
+
this._consecutiveEmptyDiscoveries = 0;
|
|
698
780
|
this._replacePeers(peers);
|
|
699
781
|
return peers;
|
|
700
782
|
}
|
|
783
|
+
this._consecutiveEmptyDiscoveries += 1;
|
|
701
784
|
const fallbackPeers = previousCachedPeers.length > 0 && this._cacheMutationEpoch === mutationEpochAtStart
|
|
702
785
|
? [...previousCachedPeers]
|
|
703
786
|
: [];
|
|
@@ -765,6 +848,20 @@ export class BuyerProxy {
|
|
|
765
848
|
res.end();
|
|
766
849
|
return;
|
|
767
850
|
}
|
|
851
|
+
if (path === '/_antseed/status' && method === 'GET') {
|
|
852
|
+
// Network reachability snapshot for UI diagnostics.
|
|
853
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
854
|
+
res.end(JSON.stringify({
|
|
855
|
+
ok: true,
|
|
856
|
+
dhtNodeCount: this._node.dhtNodeCount,
|
|
857
|
+
consecutiveEmptyDiscoveries: this._consecutiveEmptyDiscoveries,
|
|
858
|
+
peerCount: this._cachedPeers.length,
|
|
859
|
+
peersUpdatedAt: this._cacheLastUpdatedAtMs > 0 ? this._cacheLastUpdatedAtMs : null,
|
|
860
|
+
startedAt: this._startedAtMs,
|
|
861
|
+
uptimeMs: this._startedAtMs > 0 ? Date.now() - this._startedAtMs : 0,
|
|
862
|
+
}));
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
768
865
|
if (path === '/_antseed/peers/refresh' && method === 'POST') {
|
|
769
866
|
try {
|
|
770
867
|
const peers = await this._refreshPeersNow();
|
|
@@ -797,6 +894,107 @@ export class BuyerProxy {
|
|
|
797
894
|
res.end(JSON.stringify({ ok: true, peers: payload }));
|
|
798
895
|
return;
|
|
799
896
|
}
|
|
897
|
+
if (path === '/_antseed/route' && method === 'GET') {
|
|
898
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
899
|
+
res.end(JSON.stringify({ ok: true, model: this._defaultRoutedModel }));
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (path === '/_antseed/route' && method === 'POST') {
|
|
903
|
+
const chunks = [];
|
|
904
|
+
let totalSize = 0;
|
|
905
|
+
for await (const chunk of req) {
|
|
906
|
+
totalSize += chunk.length;
|
|
907
|
+
if (totalSize > 8192) {
|
|
908
|
+
res.writeHead(413, { 'content-type': 'application/json' });
|
|
909
|
+
res.end(JSON.stringify({ ok: false, error: 'Request body too large' }));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
chunks.push(chunk);
|
|
913
|
+
}
|
|
914
|
+
let model;
|
|
915
|
+
try {
|
|
916
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
917
|
+
model = typeof body.model === 'string' ? body.model.trim() : '';
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
920
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
921
|
+
res.end(JSON.stringify({ ok: false, error: 'Invalid JSON body' }));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (model.length > 0 && !parsePeerPinnedService(model)) {
|
|
925
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
926
|
+
res.end(JSON.stringify({ ok: false, error: 'model must be "<peerId>@<service>" (or empty to clear)' }));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
this._defaultRoutedModel = model.length > 0 ? model : null;
|
|
930
|
+
await this._mergeStateFile({ defaultRoutedModel: this._defaultRoutedModel });
|
|
931
|
+
log(`Default routed model set: ${this._defaultRoutedModel ?? 'none'}`);
|
|
932
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
933
|
+
res.end(JSON.stringify({ ok: true, model: this._defaultRoutedModel }));
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (path === '/_antseed/conversations' && method === 'GET') {
|
|
937
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
938
|
+
res.end(JSON.stringify({ ok: true, conversations: this._conversations.list() }));
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (path === '/_antseed/conversations/update' && method === 'POST') {
|
|
942
|
+
const chunks = [];
|
|
943
|
+
let totalSize = 0;
|
|
944
|
+
for await (const chunk of req) {
|
|
945
|
+
totalSize += chunk.length;
|
|
946
|
+
if (totalSize > 8192) {
|
|
947
|
+
res.writeHead(413, { 'content-type': 'application/json' });
|
|
948
|
+
res.end(JSON.stringify({ ok: false, error: 'Request body too large' }));
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
chunks.push(chunk);
|
|
952
|
+
}
|
|
953
|
+
let parsed;
|
|
954
|
+
try {
|
|
955
|
+
parsed = JSON.parse(Buffer.concat(chunks).toString());
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
959
|
+
res.end(JSON.stringify({ ok: false, error: 'Invalid JSON body' }));
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const id = typeof parsed.id === 'string' ? parsed.id : '';
|
|
963
|
+
if (!id) {
|
|
964
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
965
|
+
res.end(JSON.stringify({ ok: false, error: 'id is required' }));
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (parsed.delete === true) {
|
|
969
|
+
const removed = this._conversations.remove(id);
|
|
970
|
+
res.writeHead(removed ? 200 : 404, { 'content-type': 'application/json' });
|
|
971
|
+
res.end(JSON.stringify(removed ? { ok: true } : { ok: false, error: 'Unknown conversation' }));
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
let conversation = this._conversations.get(id);
|
|
975
|
+
if (!conversation) {
|
|
976
|
+
res.writeHead(404, { 'content-type': 'application/json' });
|
|
977
|
+
res.end(JSON.stringify({ ok: false, error: 'Unknown conversation' }));
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if ('pinnedModel' in parsed) {
|
|
981
|
+
const pin = typeof parsed.pinnedModel === 'string' ? parsed.pinnedModel.trim() : '';
|
|
982
|
+
if (pin.length > 0 && !parsePeerPinnedService(pin)) {
|
|
983
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
984
|
+
res.end(JSON.stringify({ ok: false, error: 'pinnedModel must be "<peerId>@<service>" (or empty to clear)' }));
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
conversation = this._conversations.setPinnedModel(id, pin.length > 0 ? pin : null);
|
|
988
|
+
log(`Conversation ${id.slice(0, 40)} pin: ${pin || 'cleared'}`);
|
|
989
|
+
}
|
|
990
|
+
if ('label' in parsed) {
|
|
991
|
+
const label = typeof parsed.label === 'string' ? parsed.label : null;
|
|
992
|
+
conversation = this._conversations.setLabel(id, label);
|
|
993
|
+
}
|
|
994
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
995
|
+
res.end(JSON.stringify({ ok: true, conversation }));
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
800
998
|
if (path === '/_antseed/connect' && method === 'POST') {
|
|
801
999
|
const chunks = [];
|
|
802
1000
|
let totalSize = 0;
|
|
@@ -857,7 +1055,7 @@ export class BuyerProxy {
|
|
|
857
1055
|
if (path.startsWith('/_antseed/buyer-usage') && method === 'GET') {
|
|
858
1056
|
const totals = this._node.getBuyerUsageTotals();
|
|
859
1057
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
860
|
-
res.end(JSON.stringify({ ok: true, totals }));
|
|
1058
|
+
res.end(JSON.stringify({ ok: true, totals, lastActivityAt: this._lastModelActivityAt || null }));
|
|
861
1059
|
return;
|
|
862
1060
|
}
|
|
863
1061
|
const meteringMatch = path.match(/^\/_antseed\/metering\/(.+)$/);
|
|
@@ -868,8 +1066,11 @@ export class BuyerProxy {
|
|
|
868
1066
|
res.end(JSON.stringify(stats));
|
|
869
1067
|
return;
|
|
870
1068
|
}
|
|
871
|
-
//
|
|
872
|
-
//
|
|
1069
|
+
// Offer a signed deposit-sweep request to the daemon's connected relayers
|
|
1070
|
+
// one at a time (see docs/protocol/spec/09-deposit-sweep.md) — sequential
|
|
1071
|
+
// dispatch keeps relayers from racing the same nonce and burning gas on
|
|
1072
|
+
// reverted transactions. Responds only after a relayer accepts, every
|
|
1073
|
+
// candidate declines, or the offer round runs out of peers.
|
|
873
1074
|
if (path === '/_antseed/sweep' && method === 'POST') {
|
|
874
1075
|
const chunks = [];
|
|
875
1076
|
let totalSize = 0;
|
|
@@ -885,10 +1086,10 @@ export class BuyerProxy {
|
|
|
885
1086
|
try {
|
|
886
1087
|
// Re-validate through the wire codec — same rules as inbound P2P frames.
|
|
887
1088
|
const payload = decodeSweepRequest(new Uint8Array(Buffer.concat(chunks)));
|
|
888
|
-
const
|
|
889
|
-
log(`Sweep request ${payload.nonce.slice(0, 10)}...
|
|
1089
|
+
const { offered, accepted } = await this._node.dispatchSweepRequest(payload);
|
|
1090
|
+
log(`Sweep request ${payload.nonce.slice(0, 10)}... offered to ${offered} peer(s), accepted=${accepted}`);
|
|
890
1091
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
891
|
-
res.end(JSON.stringify({ ok: true, sent }));
|
|
1092
|
+
res.end(JSON.stringify({ ok: true, sent: offered, accepted }));
|
|
892
1093
|
}
|
|
893
1094
|
catch (err) {
|
|
894
1095
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -979,6 +1180,17 @@ export class BuyerProxy {
|
|
|
979
1180
|
chunks.push(chunk);
|
|
980
1181
|
}
|
|
981
1182
|
const body = Buffer.concat(chunks);
|
|
1183
|
+
// `/v1/messages/count_tokens` sits under the completion prefix but is not
|
|
1184
|
+
// a turn — Anthropic tools call it before most turns to size the context.
|
|
1185
|
+
// Answering it locally keeps it off the wire: routed to a seller it costs
|
|
1186
|
+
// a full inference and answers in a shape the caller cannot read.
|
|
1187
|
+
if (method === 'POST' && isCountTokensPath(normalizedPath)) {
|
|
1188
|
+
const inputTokens = estimateAnthropicPromptTokens(new Uint8Array(body));
|
|
1189
|
+
log(`count_tokens answered locally: ${inputTokens} tokens`);
|
|
1190
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
1191
|
+
res.end(JSON.stringify({ input_tokens: inputTokens }));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
982
1194
|
// Build serialized request
|
|
983
1195
|
const headers = {};
|
|
984
1196
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
@@ -991,6 +1203,11 @@ export class BuyerProxy {
|
|
|
991
1203
|
}
|
|
992
1204
|
// Remove host header (points to localhost, not the seller)
|
|
993
1205
|
delete headers['host'];
|
|
1206
|
+
// Internal marker from the system proxy: the body's model was assigned
|
|
1207
|
+
// by the proxy's route rewrite, not chosen by the tool. Stripped here so
|
|
1208
|
+
// it never reaches a seller.
|
|
1209
|
+
const systemRoutedModel = headers[SYSTEM_ROUTED_MODEL_HEADER] === '1';
|
|
1210
|
+
delete headers[SYSTEM_ROUTED_MODEL_HEADER];
|
|
994
1211
|
let serializedReq = {
|
|
995
1212
|
requestId: randomUUID(),
|
|
996
1213
|
method,
|
|
@@ -998,9 +1215,106 @@ export class BuyerProxy {
|
|
|
998
1215
|
headers,
|
|
999
1216
|
body: new Uint8Array(body),
|
|
1000
1217
|
};
|
|
1001
|
-
// Snapshot the session
|
|
1218
|
+
// Snapshot the session overrides before any await so a concurrent
|
|
1002
1219
|
// _reloadSessionOverrides() cannot change routing mid-request.
|
|
1003
1220
|
const effectivePinnedPeer = this._pinnedPeer;
|
|
1221
|
+
// Per-chat routing: completion requests carry a stable per-conversation
|
|
1222
|
+
// identity (see conversation-identity.ts). A chat pinned to a model
|
|
1223
|
+
// overrides the session default when resolving the `antseed` alias;
|
|
1224
|
+
// subagent sessions inherit their parent chat's pin. The default route
|
|
1225
|
+
// only steers a chat's first request — the model that serves it becomes
|
|
1226
|
+
// the chat's own pin (ConversationStore.touch), so changing the default
|
|
1227
|
+
// later applies to new chats only.
|
|
1228
|
+
const isConversationRequest = method === 'POST' && isCompletionRequestPath(path);
|
|
1229
|
+
const conversationBody = isConversationRequest
|
|
1230
|
+
? parseRequestBodyObject(serializedReq.body, serializedReq.headers)
|
|
1231
|
+
: null;
|
|
1232
|
+
const conversationIdentity = isConversationRequest
|
|
1233
|
+
? extractConversationIdentity(serializedReq.headers, conversationBody)
|
|
1234
|
+
: null;
|
|
1235
|
+
// Internal marker from the system proxy: source profile used only for
|
|
1236
|
+
// local conversation attribution. Stripped here so it never reaches a seller.
|
|
1237
|
+
delete serializedReq.headers[SYSTEM_PROXY_SOURCE_HEADER];
|
|
1238
|
+
const chatPinnedModel = conversationIdentity
|
|
1239
|
+
? this._conversations.getPinnedModel(conversationIdentity.tool, conversationIdentity.sessionKey)
|
|
1240
|
+
?? (conversationIdentity.parentSessionKey
|
|
1241
|
+
? this._conversations.getPinnedModel(conversationIdentity.tool, conversationIdentity.parentSessionKey)
|
|
1242
|
+
: null)
|
|
1243
|
+
: null;
|
|
1244
|
+
const effectiveRoutedModel = chatPinnedModel ?? this._defaultRoutedModel;
|
|
1245
|
+
// Resolve the `antseed` model alias to the session's default route first,
|
|
1246
|
+
// so the regular `<peerId>@<service>` pin rewrite below picks up the
|
|
1247
|
+
// substituted value. Tool configs written by the desktop carry the alias
|
|
1248
|
+
// so route changes apply to running sessions without config rewrites.
|
|
1249
|
+
const aliasResult = substituteRoutedModelAlias(serializedReq.body, serializedReq.headers, effectiveRoutedModel);
|
|
1250
|
+
if (aliasResult.aliasRequested && !aliasResult.substituted) {
|
|
1251
|
+
log(`Request rejected: model alias "${ROUTED_MODEL_ALIAS}" with no default route set`);
|
|
1252
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
1253
|
+
res.end(JSON.stringify({
|
|
1254
|
+
error: {
|
|
1255
|
+
type: 'no_default_route',
|
|
1256
|
+
code: 'no_default_route',
|
|
1257
|
+
message: `Model "${ROUTED_MODEL_ALIAS}" routes to the model selected in AntStation, but no route is set. `
|
|
1258
|
+
+ 'Pick a model in the desktop app, or request "<peerId>@<model>" explicitly.',
|
|
1259
|
+
param: 'model',
|
|
1260
|
+
},
|
|
1261
|
+
}));
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (aliasResult.substituted) {
|
|
1265
|
+
serializedReq = { ...serializedReq, body: aliasResult.body, headers: aliasResult.headers };
|
|
1266
|
+
log(`Model alias applied: ${ROUTED_MODEL_ALIAS} -> ${effectiveRoutedModel}${chatPinnedModel ? ' (chat pin)' : ''}`);
|
|
1267
|
+
}
|
|
1268
|
+
// System-proxy-intercepted tools can't carry the alias — the proxy
|
|
1269
|
+
// rewrites their upstream model names to its connect-time route and
|
|
1270
|
+
// marks the request. A chat pin must still win there, exactly as it
|
|
1271
|
+
// does for alias-carrying configs; otherwise pinning a chat in the
|
|
1272
|
+
// desktop silently does nothing for intercepted apps.
|
|
1273
|
+
let chatPinOverrideApplied = false;
|
|
1274
|
+
if (systemRoutedModel && !aliasResult.aliasRequested && chatPinnedModel) {
|
|
1275
|
+
const pinOverride = overrideRoutedModelInBody(serializedReq.body, serializedReq.headers, chatPinnedModel);
|
|
1276
|
+
if (pinOverride.overridden) {
|
|
1277
|
+
serializedReq = { ...serializedReq, body: pinOverride.body, headers: pinOverride.headers };
|
|
1278
|
+
chatPinOverrideApplied = true;
|
|
1279
|
+
log(`Chat pin applied to system-routed model: -> ${chatPinnedModel}`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
// Track the conversation (subagent traffic rolls up into the parent
|
|
1283
|
+
// chat). The snippet is only extracted the first time a chat is seen.
|
|
1284
|
+
// A thread the tool opened for its own housekeeping is routed and paid
|
|
1285
|
+
// for normally, but is not a chat — Codex titles every new chat from one.
|
|
1286
|
+
if (conversationIdentity?.isUserThread) {
|
|
1287
|
+
const rawModel = typeof conversationBody?.['model'] === 'string' ? conversationBody['model'] : '';
|
|
1288
|
+
const resolvedModel = aliasResult.substituted
|
|
1289
|
+
? effectiveRoutedModel
|
|
1290
|
+
: chatPinOverrideApplied
|
|
1291
|
+
? chatPinnedModel
|
|
1292
|
+
: (parsePeerPinnedService(rawModel) ? rawModel : null);
|
|
1293
|
+
const trackedKey = conversationIdentity.parentSessionKey ?? conversationIdentity.sessionKey;
|
|
1294
|
+
const known = this._conversations.get(`${conversationIdentity.tool}:${trackedKey}`);
|
|
1295
|
+
// A title turn runs on the tool's own small model and races ahead of the
|
|
1296
|
+
// first real turn, so it must not become the chat's model: that pin
|
|
1297
|
+
// outranks the default route for every later turn, which would strand
|
|
1298
|
+
// the whole session on a model the user never picked.
|
|
1299
|
+
const titleTurn = isTitleGenerationRequest(conversationBody);
|
|
1300
|
+
const snippet = known?.snippet ? null : extractFirstUserSnippet(conversationBody);
|
|
1301
|
+
// Some tools (T3/Claude) fire title-only requests when a new chat opens.
|
|
1302
|
+
// Route them normally, but do not create a blank AntSeed conversation row
|
|
1303
|
+
// until the first genuine user turn arrives.
|
|
1304
|
+
if (known || !titleTurn) {
|
|
1305
|
+
const tracked = this._conversations.touch({
|
|
1306
|
+
tool: conversationIdentity.tool,
|
|
1307
|
+
sessionKey: trackedKey,
|
|
1308
|
+
// Extract until a label sticks: a tool's title-generation request can
|
|
1309
|
+
// race ahead of the first real turn and create the row snippet-less.
|
|
1310
|
+
snippet,
|
|
1311
|
+
lastModel: titleTurn ? null : resolvedModel,
|
|
1312
|
+
});
|
|
1313
|
+
// Bind the request to the chat so its cost can be attributed when the
|
|
1314
|
+
// payment layer signs for it (see _attributeSpend).
|
|
1315
|
+
this._trackRequestConversation(serializedReq.requestId, tracked.id);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1004
1318
|
const { body: servicePinBody, headers: servicePinHeaders, pinnedPeerId: bodyPinnedPeer, } = rewritePeerPinnedServiceInBody(serializedReq.body, serializedReq.headers);
|
|
1005
1319
|
if (servicePinBody !== serializedReq.body) {
|
|
1006
1320
|
serializedReq = { ...serializedReq, body: servicePinBody, headers: servicePinHeaders };
|
|
@@ -1335,6 +1649,7 @@ export class BuyerProxy {
|
|
|
1335
1649
|
log(`Outbound request shape: ${summarizeRequestShape(requestForPeer)}`);
|
|
1336
1650
|
}
|
|
1337
1651
|
log(`Routing to peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
1652
|
+
this._markModelActivity();
|
|
1338
1653
|
// Forward through P2P
|
|
1339
1654
|
const wantsStreaming = clientWantsStreaming;
|
|
1340
1655
|
const startTime = Date.now();
|
|
@@ -1364,6 +1679,7 @@ export class BuyerProxy {
|
|
|
1364
1679
|
onResponseChunk: (chunk) => {
|
|
1365
1680
|
if (!streamed)
|
|
1366
1681
|
return;
|
|
1682
|
+
this._markModelActivity();
|
|
1367
1683
|
const adaptedChunks = streamResponseAdapter
|
|
1368
1684
|
? streamResponseAdapter.adaptChunk(chunk)
|
|
1369
1685
|
: [chunk];
|
|
@@ -1447,6 +1763,7 @@ export class BuyerProxy {
|
|
|
1447
1763
|
response = inject402PeerId(response, selectedPeer.peerId);
|
|
1448
1764
|
}
|
|
1449
1765
|
const latencyMs = Date.now() - startTime;
|
|
1766
|
+
this._markModelActivity();
|
|
1450
1767
|
log(`Response: ${response.statusCode} (${latencyMs}ms, ${response.body.length} bytes)`);
|
|
1451
1768
|
if (response.statusCode >= 400) {
|
|
1452
1769
|
const prefix = adaptResponse ? 'Upstream adapted error detail' : 'Upstream error detail';
|