@antseed/cli 0.1.148 → 0.1.150
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 +51 -17
- package/dist/cli/commands/buyer/activity.d.ts +3 -0
- package/dist/cli/commands/buyer/activity.d.ts.map +1 -0
- package/dist/cli/commands/buyer/activity.js +555 -0
- package/dist/cli/commands/buyer/activity.js.map +1 -0
- package/dist/cli/commands/buyer/daemon.d.ts +13 -0
- package/dist/cli/commands/buyer/daemon.d.ts.map +1 -0
- package/dist/cli/commands/buyer/daemon.js +39 -0
- package/dist/cli/commands/buyer/daemon.js.map +1 -0
- package/dist/cli/commands/buyer/deposit.d.ts +8 -0
- package/dist/cli/commands/buyer/deposit.d.ts.map +1 -1
- package/dist/cli/commands/buyer/deposit.js +338 -25
- package/dist/cli/commands/buyer/deposit.js.map +1 -1
- package/dist/cli/commands/buyer/index.d.ts.map +1 -1
- package/dist/cli/commands/buyer/index.js +2 -0
- package/dist/cli/commands/buyer/index.js.map +1 -1
- package/dist/cli/commands/buyer/start.d.ts +1 -0
- package/dist/cli/commands/buyer/start.d.ts.map +1 -1
- package/dist/cli/commands/buyer/start.js +53 -10
- package/dist/cli/commands/buyer/start.js.map +1 -1
- package/dist/cli/commands/buyer/start.test.js +30 -1
- package/dist/cli/commands/buyer/start.test.js.map +1 -1
- package/dist/cli/commands/buyer/sweep.d.ts.map +1 -1
- package/dist/cli/commands/buyer/sweep.js +17 -17
- package/dist/cli/commands/buyer/sweep.js.map +1 -1
- package/dist/cli/commands/network/browse.d.ts.map +1 -1
- package/dist/cli/commands/network/browse.js +65 -114
- package/dist/cli/commands/network/browse.js.map +1 -1
- package/dist/cli/commands/payments.d.ts.map +1 -1
- package/dist/cli/commands/payments.js +29 -34
- package/dist/cli/commands/payments.js.map +1 -1
- package/dist/cli/commands/seller/start.d.ts.map +1 -1
- package/dist/cli/commands/seller/start.js +4 -1
- package/dist/cli/commands/seller/start.js.map +1 -1
- package/dist/cli/commands/seller/start.test.js +15 -0
- package/dist/cli/commands/seller/start.test.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +7 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +41 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/loader.test.js +52 -0
- package/dist/config/loader.test.js.map +1 -1
- package/dist/config/types.d.ts +10 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +17 -0
- package/dist/config/validation.js.map +1 -1
- package/dist/generated/baked-defaults.d.ts +15 -0
- package/dist/generated/baked-defaults.d.ts.map +1 -0
- package/dist/generated/baked-defaults.js +15 -0
- package/dist/generated/baked-defaults.js.map +1 -0
- package/dist/plugins/manager.d.ts +1 -0
- package/dist/plugins/manager.d.ts.map +1 -1
- package/dist/plugins/manager.js +15 -4
- package/dist/plugins/manager.js.map +1 -1
- package/dist/plugins/manager.test.d.ts +2 -0
- package/dist/plugins/manager.test.d.ts.map +1 -0
- package/dist/plugins/manager.test.js +15 -0
- package/dist/plugins/manager.test.js.map +1 -0
- package/dist/proxy/buyer-proxy.d.ts +31 -2
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +481 -59
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +894 -56
- package/dist/proxy/buyer-proxy.test.js.map +1 -1
- package/dist/proxy/conversation-store.d.ts +6 -6
- package/dist/proxy/conversation-store.d.ts.map +1 -1
- package/dist/proxy/conversation-store.js +16 -0
- package/dist/proxy/conversation-store.js.map +1 -1
- package/dist/proxy/deposit-watcher.d.ts +125 -0
- package/dist/proxy/deposit-watcher.d.ts.map +1 -0
- package/dist/proxy/deposit-watcher.js +306 -0
- package/dist/proxy/deposit-watcher.js.map +1 -0
- package/dist/proxy/deposit-watcher.test.d.ts +2 -0
- package/dist/proxy/deposit-watcher.test.d.ts.map +1 -0
- package/dist/proxy/deposit-watcher.test.js +173 -0
- package/dist/proxy/deposit-watcher.test.js.map +1 -0
- package/dist/proxy/network-models.d.ts +76 -0
- package/dist/proxy/network-models.d.ts.map +1 -0
- package/dist/proxy/network-models.js +229 -0
- package/dist/proxy/network-models.js.map +1 -0
- package/dist/proxy/network-models.test.d.ts +2 -0
- package/dist/proxy/network-models.test.d.ts.map +1 -0
- package/dist/proxy/network-models.test.js +734 -0
- package/dist/proxy/network-models.test.js.map +1 -0
- package/dist/proxy/routing.d.ts +3 -1
- package/dist/proxy/routing.d.ts.map +1 -1
- package/dist/proxy/routing.js +69 -16
- package/dist/proxy/routing.js.map +1 -1
- package/package.json +6 -5
|
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { Readable } from 'node:stream';
|
|
7
7
|
import test from 'node:test';
|
|
8
|
-
import { ANTSEED_BUYER_FAULT_ERROR_CODE, ANTSEED_FAULT_ATTRIBUTION_HEADER, CONNECTION_CAPABILITY_COOPERATIVE_CLOSE_V1, CONNECTION_CAPABILITY_RELAYS_SWEEPS_V1, buyerFault, } from '@antseed/node';
|
|
8
|
+
import { ANTSEED_BUYER_FAULT_ERROR_CODE, ANTSEED_FAULT_ATTRIBUTION_HEADER, CONNECTION_CAPABILITY_COOPERATIVE_CLOSE_V1, CONNECTION_CAPABILITY_RELAYS_SWEEPS_V1, buyerFault, computeOnChainReputationScore, } from '@antseed/node';
|
|
9
9
|
import { DEFAULT_BUYER_PEER_REFRESH_INTERVAL_MS } from '../config/defaults.js';
|
|
10
10
|
import { BuyerProxy, isModelNotFoundResponse, makeVerifierReach, mergeJsonStateFile, parsePeerPinnedService, parsePersistedPeers, rewritePeerPinnedServiceInBody, selectCandidatePeersForRouting, substituteRoutedModelAlias, sweepStaleStateTmpFiles, } from './buyer-proxy.js';
|
|
11
11
|
import { overrideRoutedModelInBody, SYSTEM_ROUTED_MODEL_HEADER } from './request-utils.js';
|
|
@@ -58,7 +58,7 @@ function makeProxyResponse() {
|
|
|
58
58
|
},
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
-
function makeBuyerProxyWithPeers(initialPeers, refreshedPeers = initialPeers, router = null, now) {
|
|
61
|
+
function makeBuyerProxyWithPeers(initialPeers, refreshedPeers = initialPeers, router = null, now, routingPreferences) {
|
|
62
62
|
const proxy = new BuyerProxy({
|
|
63
63
|
port: 0,
|
|
64
64
|
dataDir: '/tmp/antseed-test',
|
|
@@ -66,11 +66,19 @@ function makeBuyerProxyWithPeers(initialPeers, refreshedPeers = initialPeers, ro
|
|
|
66
66
|
router,
|
|
67
67
|
},
|
|
68
68
|
...(now ? { now } : {}),
|
|
69
|
+
...(routingPreferences ? { routingPreferences } : {}),
|
|
69
70
|
});
|
|
70
71
|
proxy._getPeers = async (options) => options?.forceRefresh ? refreshedPeers : initialPeers;
|
|
71
72
|
proxy._cacheLastUpdatedAtMs = Date.now();
|
|
72
73
|
return proxy;
|
|
73
74
|
}
|
|
75
|
+
const priceAndTrustPreferences = {
|
|
76
|
+
preferFreePeers: false,
|
|
77
|
+
maxInputUsdPerMillion: 25,
|
|
78
|
+
minTrustScore: 60,
|
|
79
|
+
allowedPeerIds: [],
|
|
80
|
+
blockedPeerIds: [],
|
|
81
|
+
};
|
|
74
82
|
/**
|
|
75
83
|
* A hand-cranked clock for peer-health tests.
|
|
76
84
|
*
|
|
@@ -122,6 +130,38 @@ test('BuyerProxy accepts a custom background refresh interval', () => {
|
|
|
122
130
|
});
|
|
123
131
|
assert.equal(proxy._bgRefreshIntervalMs, 15_000);
|
|
124
132
|
});
|
|
133
|
+
test('BuyerProxy reloads model routing preferences from config', async (t) => {
|
|
134
|
+
const dataDir = await mkdtemp(join(tmpdir(), 'antseed-routing-config-'));
|
|
135
|
+
const configPath = join(dataDir, 'config.json');
|
|
136
|
+
t.after(() => rm(dataDir, { recursive: true, force: true }));
|
|
137
|
+
const allowedPeerId = 'a'.repeat(40);
|
|
138
|
+
await writeFile(configPath, JSON.stringify({
|
|
139
|
+
buyer: {
|
|
140
|
+
routingPreferences: {
|
|
141
|
+
preferFreePeers: true,
|
|
142
|
+
maxInputUsdPerMillion: 8,
|
|
143
|
+
minTrustScore: 72,
|
|
144
|
+
allowedPeerIds: [allowedPeerId],
|
|
145
|
+
blockedPeerIds: [],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}));
|
|
149
|
+
const proxy = new BuyerProxy({
|
|
150
|
+
port: 0,
|
|
151
|
+
dataDir,
|
|
152
|
+
configPath,
|
|
153
|
+
routingPreferences: priceAndTrustPreferences,
|
|
154
|
+
node: { router: null },
|
|
155
|
+
});
|
|
156
|
+
await proxy._reloadRoutingPreferences();
|
|
157
|
+
assert.deepEqual(proxy._routingPreferences, {
|
|
158
|
+
preferFreePeers: true,
|
|
159
|
+
maxInputUsdPerMillion: 8,
|
|
160
|
+
minTrustScore: 72,
|
|
161
|
+
allowedPeerIds: [allowedPeerId],
|
|
162
|
+
blockedPeerIds: [],
|
|
163
|
+
});
|
|
164
|
+
});
|
|
125
165
|
test('BuyerProxy starts incremental discovery on startup', async (t) => {
|
|
126
166
|
const dir = await mkdtemp(join(tmpdir(), 'antseed-buyer-proxy-'));
|
|
127
167
|
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
@@ -330,6 +370,52 @@ test('selectCandidatePeersForRouting in lenient mode prefers exact service match
|
|
|
330
370
|
assert.ok(plan, 'expected a route plan for the lenient-kept peer');
|
|
331
371
|
assert.equal(plan.provider, 'local-llm');
|
|
332
372
|
assert.equal(plan.selection?.requiresTransform, false);
|
|
373
|
+
assert.equal(plan.serviceId, 'llama');
|
|
374
|
+
});
|
|
375
|
+
test('selectCandidatePeersForRouting derives protocol from the selected cheapest alias', () => {
|
|
376
|
+
const peer = makePeer('a', ['openai', 'claude-oauth']);
|
|
377
|
+
peer.providerPricing = {
|
|
378
|
+
openai: { defaults: { inputUsdPerMillion: 5, outputUsdPerMillion: 25 }, services: { 'claude-opus-5': { inputUsdPerMillion: 5, outputUsdPerMillion: 25 } } },
|
|
379
|
+
'claude-oauth': { defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 5 }, services: { 'opus-5': { inputUsdPerMillion: 1, outputUsdPerMillion: 5 } } },
|
|
380
|
+
};
|
|
381
|
+
peer.providerServiceApiProtocols = {
|
|
382
|
+
openai: { services: { 'claude-opus-5': ['openai-chat-completions'] } },
|
|
383
|
+
'claude-oauth': { services: { 'opus-5': ['anthropic-messages'] } },
|
|
384
|
+
};
|
|
385
|
+
const result = selectCandidatePeersForRouting([peer], 'openai-chat-completions', 'Claude Opus 5', null);
|
|
386
|
+
const plan = result.routePlanByPeerId.get(peer.peerId);
|
|
387
|
+
assert.ok(plan);
|
|
388
|
+
assert.equal(plan.provider, 'claude-oauth');
|
|
389
|
+
assert.equal(plan.serviceId, 'opus-5');
|
|
390
|
+
assert.equal(plan.selection?.targetProtocol, 'anthropic-messages');
|
|
391
|
+
assert.equal(plan.selection?.requiresTransform, true);
|
|
392
|
+
});
|
|
393
|
+
test('selectCandidatePeersForRouting prefers a full service over a cheaper coding-only alias', () => {
|
|
394
|
+
const peer = makePeer('a', ['anthropic', 'claude-oauth']);
|
|
395
|
+
peer.providerPricing = {
|
|
396
|
+
anthropic: { defaults: { inputUsdPerMillion: 5, outputUsdPerMillion: 25 }, services: { 'claude-opus-5': { inputUsdPerMillion: 5, outputUsdPerMillion: 25 } } },
|
|
397
|
+
'claude-oauth': { defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 5 }, services: { 'opus-5-coding-only': { inputUsdPerMillion: 1, outputUsdPerMillion: 5 } } },
|
|
398
|
+
};
|
|
399
|
+
peer.providerServiceApiProtocols = {
|
|
400
|
+
anthropic: { services: { 'claude-opus-5': ['anthropic-messages'] } },
|
|
401
|
+
'claude-oauth': { services: { 'opus-5-coding-only': ['anthropic-messages'] } },
|
|
402
|
+
};
|
|
403
|
+
const result = selectCandidatePeersForRouting([peer], 'anthropic-messages', 'opus-5', null);
|
|
404
|
+
const plan = result.routePlanByPeerId.get(peer.peerId);
|
|
405
|
+
assert.ok(plan);
|
|
406
|
+
assert.equal(plan.provider, 'anthropic');
|
|
407
|
+
assert.equal(plan.serviceId, 'claude-opus-5');
|
|
408
|
+
});
|
|
409
|
+
test('selectCandidatePeersForRouting excludes coding-only-only peers from unrestricted requests', () => {
|
|
410
|
+
const peer = makePeer('a', ['claude-oauth']);
|
|
411
|
+
peer.providerServiceApiProtocols = {
|
|
412
|
+
'claude-oauth': { services: { 'opus-5-coding-only': ['anthropic-messages'] } },
|
|
413
|
+
};
|
|
414
|
+
const unrestricted = selectCandidatePeersForRouting([peer], 'anthropic-messages', 'opus-5', null);
|
|
415
|
+
assert.equal(unrestricted.candidatePeers.length, 0);
|
|
416
|
+
const explicit = selectCandidatePeersForRouting([peer], 'anthropic-messages', 'opus-5-coding-only', null);
|
|
417
|
+
assert.equal(explicit.candidatePeers.length, 1);
|
|
418
|
+
assert.equal(explicit.routePlanByPeerId.get(peer.peerId)?.serviceId, 'opus-5-coding-only');
|
|
333
419
|
});
|
|
334
420
|
test('selectCandidatePeersForRouting can still include peers without service protocol metadata', () => {
|
|
335
421
|
const peerWithoutMetadata = makePeer('a', ['openai']);
|
|
@@ -337,6 +423,546 @@ test('selectCandidatePeersForRouting can still include peers without service pro
|
|
|
337
423
|
assert.equal(result.candidatePeers.length, 1);
|
|
338
424
|
assert.equal(result.candidatePeers[0]?.peerId, peerWithoutMetadata.peerId);
|
|
339
425
|
});
|
|
426
|
+
test('model-only request routes to the highest-ranked canonical service match', async () => {
|
|
427
|
+
const lower = makePeer('a', ['anthropic']);
|
|
428
|
+
lower.reputationScore = 40;
|
|
429
|
+
lower.providerServiceApiProtocols = {
|
|
430
|
+
anthropic: { services: { 'Claude Opus 5': ['anthropic-messages'] } },
|
|
431
|
+
};
|
|
432
|
+
const higher = makePeer('b', ['anthropic']);
|
|
433
|
+
higher.reputationScore = 90;
|
|
434
|
+
higher.providerServiceApiProtocols = {
|
|
435
|
+
anthropic: { services: { 'opus-5': ['anthropic-messages'] } },
|
|
436
|
+
};
|
|
437
|
+
const proxy = makeBuyerProxyWithPeers([lower, higher], [lower, higher], permissiveRouter());
|
|
438
|
+
let selectedPeerId = '';
|
|
439
|
+
let selectedBody = null;
|
|
440
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
441
|
+
selectedPeerId = peer.peerId;
|
|
442
|
+
selectedBody = parseJsonBody(request.body);
|
|
443
|
+
return {
|
|
444
|
+
requestId: request.requestId,
|
|
445
|
+
statusCode: 200,
|
|
446
|
+
headers: { 'content-type': 'application/json' },
|
|
447
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
451
|
+
path: '/v1/messages',
|
|
452
|
+
body: { model: 'claude-opus-5', max_tokens: 32, messages: [] },
|
|
453
|
+
}));
|
|
454
|
+
assert.equal(res.statusCode, 200);
|
|
455
|
+
assert.equal(selectedPeerId, higher.peerId);
|
|
456
|
+
assert.equal(selectedBody?.['model'], 'opus-5');
|
|
457
|
+
});
|
|
458
|
+
test('conversation routing keeps the actual peer as a soft preference and fails over when needed', async () => {
|
|
459
|
+
const preferred = makePeer('c', ['openai']);
|
|
460
|
+
preferred.reputationScore = 70;
|
|
461
|
+
preferred.providerServiceApiProtocols = {
|
|
462
|
+
openai: { services: { 'kimi-k3': ['openai-chat-completions'] } },
|
|
463
|
+
};
|
|
464
|
+
const rankedFirst = makePeer('d', ['openai']);
|
|
465
|
+
rankedFirst.reputationScore = 95;
|
|
466
|
+
rankedFirst.providerServiceApiProtocols = {
|
|
467
|
+
openai: { services: { 'Kimi K3': ['openai-chat-completions'] } },
|
|
468
|
+
};
|
|
469
|
+
const proxy = makeBuyerProxyWithPeers([rankedFirst, preferred], [rankedFirst, preferred], permissiveRouter());
|
|
470
|
+
const attempts = [];
|
|
471
|
+
let preferredReachable = true;
|
|
472
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
473
|
+
attempts.push(peer.peerId);
|
|
474
|
+
assert.equal(request.headers['x-vpr-session-id'], undefined);
|
|
475
|
+
assert.equal(request.headers['x-antstation-session-id'], undefined);
|
|
476
|
+
assert.equal(request.headers['x-antseed-prefer-peer'], undefined);
|
|
477
|
+
return {
|
|
478
|
+
requestId: request.requestId,
|
|
479
|
+
statusCode: peer.peerId === preferred.peerId && !preferredReachable ? 503 : 200,
|
|
480
|
+
headers: { 'content-type': 'application/json' },
|
|
481
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
const conversationHeaders = {
|
|
485
|
+
'x-vpr-session-id': 'conversation-soft-affinity',
|
|
486
|
+
'x-antseed-prefer-peer': preferred.peerId,
|
|
487
|
+
};
|
|
488
|
+
await invokeProxy(proxy, makeProxyRequest({
|
|
489
|
+
headers: conversationHeaders,
|
|
490
|
+
body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'hello' }] },
|
|
491
|
+
}));
|
|
492
|
+
assert.deepEqual(attempts, [preferred.peerId]);
|
|
493
|
+
preferredReachable = false;
|
|
494
|
+
attempts.length = 0;
|
|
495
|
+
await invokeProxy(proxy, makeProxyRequest({
|
|
496
|
+
headers: { 'x-vpr-session-id': 'conversation-soft-affinity' },
|
|
497
|
+
body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'again' }] },
|
|
498
|
+
}));
|
|
499
|
+
assert.deepEqual(attempts, [preferred.peerId, rankedFirst.peerId]);
|
|
500
|
+
const stored = proxy._conversations.get('vpr:conversation-soft-affinity');
|
|
501
|
+
assert.equal(stored?.pinnedModel, `${rankedFirst.peerId}@Kimi K3`);
|
|
502
|
+
assert.equal(stored?.lastModel, `${rankedFirst.peerId}@Kimi K3`);
|
|
503
|
+
attempts.length = 0;
|
|
504
|
+
await invokeProxy(proxy, makeProxyRequest({
|
|
505
|
+
headers: { 'x-vpr-session-id': 'conversation-soft-affinity' },
|
|
506
|
+
body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'third' }] },
|
|
507
|
+
}));
|
|
508
|
+
assert.equal(attempts[0], rankedFirst.peerId);
|
|
509
|
+
const route = await invokeProxy(proxy, makeProxyRequest({
|
|
510
|
+
method: 'GET',
|
|
511
|
+
path: `/_antseed/conversations/${encodeURIComponent('vpr:conversation-soft-affinity')}`,
|
|
512
|
+
}));
|
|
513
|
+
assert.equal(route.statusCode, 200);
|
|
514
|
+
assert.equal(JSON.parse(route.body).conversation.lastModel, `${rankedFirst.peerId}@Kimi K3`);
|
|
515
|
+
});
|
|
516
|
+
test('model-only request applies a cached-input pricing reputation penalty', async () => {
|
|
517
|
+
const priced = makePeer('a', ['openai']);
|
|
518
|
+
priced.reputationScore = 75;
|
|
519
|
+
priced.providerPricing = {
|
|
520
|
+
openai: {
|
|
521
|
+
defaults: { inputUsdPerMillion: 2, outputUsdPerMillion: 4 },
|
|
522
|
+
services: {
|
|
523
|
+
'cache-model': { inputUsdPerMillion: 2, outputUsdPerMillion: 4, cachedInputUsdPerMillion: 0.2 },
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
priced.providerServiceApiProtocols = {
|
|
528
|
+
openai: { services: { 'cache-model': ['openai-chat-completions'] } },
|
|
529
|
+
};
|
|
530
|
+
const unpriced = makePeer('b', ['openai']);
|
|
531
|
+
unpriced.reputationScore = 90;
|
|
532
|
+
unpriced.providerPricing = {
|
|
533
|
+
openai: {
|
|
534
|
+
defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
|
|
535
|
+
services: { 'cache-model': { inputUsdPerMillion: 1, outputUsdPerMillion: 2 } },
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
unpriced.providerServiceApiProtocols = {
|
|
539
|
+
openai: { services: { 'cache-model': ['openai-chat-completions'] } },
|
|
540
|
+
};
|
|
541
|
+
const proxy = makeBuyerProxyWithPeers([unpriced, priced], [unpriced, priced], permissiveRouter());
|
|
542
|
+
let selectedPeerId = '';
|
|
543
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
544
|
+
selectedPeerId = peer.peerId;
|
|
545
|
+
return {
|
|
546
|
+
requestId: request.requestId,
|
|
547
|
+
statusCode: 200,
|
|
548
|
+
headers: { 'content-type': 'application/json' },
|
|
549
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
550
|
+
};
|
|
551
|
+
};
|
|
552
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'cache-model', messages: [] } }));
|
|
553
|
+
assert.equal(res.statusCode, 200);
|
|
554
|
+
assert.equal(selectedPeerId, priced.peerId);
|
|
555
|
+
});
|
|
556
|
+
test('model-only cached-input pricing penalty does not bury a substantially stronger peer', async () => {
|
|
557
|
+
const priced = makePeer('a', ['openai']);
|
|
558
|
+
priced.reputationScore = 20;
|
|
559
|
+
priced.providerPricing = {
|
|
560
|
+
openai: {
|
|
561
|
+
defaults: { inputUsdPerMillion: 2, outputUsdPerMillion: 4 },
|
|
562
|
+
services: {
|
|
563
|
+
'cache-model': { inputUsdPerMillion: 2, outputUsdPerMillion: 4, cachedInputUsdPerMillion: 0.2 },
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
priced.providerServiceApiProtocols = {
|
|
568
|
+
openai: { services: { 'cache-model': ['openai-chat-completions'] } },
|
|
569
|
+
};
|
|
570
|
+
const unpriced = makePeer('b', ['openai']);
|
|
571
|
+
unpriced.reputationScore = 100;
|
|
572
|
+
unpriced.providerServiceApiProtocols = {
|
|
573
|
+
openai: { services: { 'cache-model': ['openai-chat-completions'] } },
|
|
574
|
+
};
|
|
575
|
+
const proxy = makeBuyerProxyWithPeers([priced, unpriced], [priced, unpriced], permissiveRouter());
|
|
576
|
+
let selectedPeerId = '';
|
|
577
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
578
|
+
selectedPeerId = peer.peerId;
|
|
579
|
+
return {
|
|
580
|
+
requestId: request.requestId,
|
|
581
|
+
statusCode: 200,
|
|
582
|
+
headers: { 'content-type': 'application/json' },
|
|
583
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
584
|
+
};
|
|
585
|
+
};
|
|
586
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'cache-model', messages: [] } }));
|
|
587
|
+
assert.equal(res.statusCode, 200);
|
|
588
|
+
assert.equal(selectedPeerId, unpriced.peerId);
|
|
589
|
+
});
|
|
590
|
+
test('model-only request keeps reputation ordering when cached-input pricing is absent for all peers', async () => {
|
|
591
|
+
const lower = makePeer('a', ['openai']);
|
|
592
|
+
lower.reputationScore = 20;
|
|
593
|
+
lower.providerServiceApiProtocols = {
|
|
594
|
+
openai: { services: { 'no-cache-model': ['openai-chat-completions'] } },
|
|
595
|
+
};
|
|
596
|
+
const higher = makePeer('b', ['openai']);
|
|
597
|
+
higher.reputationScore = 100;
|
|
598
|
+
higher.providerServiceApiProtocols = {
|
|
599
|
+
openai: { services: { 'no-cache-model': ['openai-chat-completions'] } },
|
|
600
|
+
};
|
|
601
|
+
const proxy = makeBuyerProxyWithPeers([lower, higher], [lower, higher], permissiveRouter());
|
|
602
|
+
let selectedPeerId = '';
|
|
603
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
604
|
+
selectedPeerId = peer.peerId;
|
|
605
|
+
return {
|
|
606
|
+
requestId: request.requestId,
|
|
607
|
+
statusCode: 200,
|
|
608
|
+
headers: { 'content-type': 'application/json' },
|
|
609
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
610
|
+
};
|
|
611
|
+
};
|
|
612
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'no-cache-model', messages: [] } }));
|
|
613
|
+
assert.equal(res.statusCode, 200);
|
|
614
|
+
assert.equal(selectedPeerId, higher.peerId);
|
|
615
|
+
});
|
|
616
|
+
test('model-only request routes through a legacy peer-wide service announcement', async () => {
|
|
617
|
+
const peer = makePeer('a', ['openai']);
|
|
618
|
+
peer.services = ['gpt-5.6-terra'];
|
|
619
|
+
const proxy = makeBuyerProxyWithPeers([peer], [peer], permissiveRouter());
|
|
620
|
+
let selectedPeerId = '';
|
|
621
|
+
let selectedBody = null;
|
|
622
|
+
proxy._node.sendRequest = async (selectedPeer, request) => {
|
|
623
|
+
selectedPeerId = selectedPeer.peerId;
|
|
624
|
+
selectedBody = parseJsonBody(request.body);
|
|
625
|
+
return {
|
|
626
|
+
requestId: request.requestId,
|
|
627
|
+
statusCode: 200,
|
|
628
|
+
headers: { 'content-type': 'application/json' },
|
|
629
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
630
|
+
};
|
|
631
|
+
};
|
|
632
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
633
|
+
body: { model: 'gpt-56-terra', messages: [] },
|
|
634
|
+
}));
|
|
635
|
+
assert.equal(res.statusCode, 200);
|
|
636
|
+
assert.equal(selectedPeerId, peer.peerId);
|
|
637
|
+
assert.equal(selectedBody?.['model'], 'gpt-5.6-terra');
|
|
638
|
+
});
|
|
639
|
+
test('model-only request routes when the request path has no detectable protocol', async () => {
|
|
640
|
+
const peer = makePeer('a', ['openai']);
|
|
641
|
+
peer.providerServiceApiProtocols = {
|
|
642
|
+
openai: { services: { 'embedding-model': ['openai-chat-completions'] } },
|
|
643
|
+
};
|
|
644
|
+
const proxy = makeBuyerProxyWithPeers([peer], [peer], permissiveRouter());
|
|
645
|
+
let selectedBody = null;
|
|
646
|
+
proxy._node.sendRequest = async (_peer, request) => {
|
|
647
|
+
selectedBody = parseJsonBody(request.body);
|
|
648
|
+
return {
|
|
649
|
+
requestId: request.requestId,
|
|
650
|
+
statusCode: 200,
|
|
651
|
+
headers: { 'content-type': 'application/json' },
|
|
652
|
+
body: Buffer.from('{}'),
|
|
653
|
+
};
|
|
654
|
+
};
|
|
655
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
656
|
+
path: '/v1/models/custom-operation',
|
|
657
|
+
body: { model: 'embedding-model', input: 'hello' },
|
|
658
|
+
}));
|
|
659
|
+
assert.equal(res.statusCode, 200);
|
|
660
|
+
assert.equal(selectedBody?.['model'], 'embedding-model');
|
|
661
|
+
});
|
|
662
|
+
test('model-only request uses the cheapest duplicate service advertised by one peer', async () => {
|
|
663
|
+
const peer = makePeer('a', ['openai']);
|
|
664
|
+
peer.providerPricing = {
|
|
665
|
+
openai: {
|
|
666
|
+
defaults: { inputUsdPerMillion: 0, outputUsdPerMillion: 0 },
|
|
667
|
+
services: {
|
|
668
|
+
'gpt-5.6-sol': { inputUsdPerMillion: 5, outputUsdPerMillion: 10 },
|
|
669
|
+
'gpt-56-sol': { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
peer.providerServiceApiProtocols = {
|
|
674
|
+
openai: {
|
|
675
|
+
services: {
|
|
676
|
+
'gpt-5.6-sol': ['openai-chat-completions'],
|
|
677
|
+
'gpt-56-sol': ['openai-chat-completions'],
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
const proxy = makeBuyerProxyWithPeers([peer], [peer], permissiveRouter());
|
|
682
|
+
let selectedBody = null;
|
|
683
|
+
proxy._node.sendRequest = async (_peer, request) => {
|
|
684
|
+
selectedBody = parseJsonBody(request.body);
|
|
685
|
+
return {
|
|
686
|
+
requestId: request.requestId,
|
|
687
|
+
statusCode: 200,
|
|
688
|
+
headers: { 'content-type': 'application/json' },
|
|
689
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
693
|
+
body: { model: 'gpt-5.6-sol', messages: [] },
|
|
694
|
+
}));
|
|
695
|
+
assert.equal(res.statusCode, 200);
|
|
696
|
+
assert.equal(selectedBody?.['model'], 'gpt-56-sol');
|
|
697
|
+
});
|
|
698
|
+
test('antseed alias with a model-only default route uses automatic peer selection', async () => {
|
|
699
|
+
const lower = makePeer('a', ['openai']);
|
|
700
|
+
lower.reputationScore = 40;
|
|
701
|
+
lower.providerServiceApiProtocols = {
|
|
702
|
+
openai: { services: { 'gpt-56-sol': ['openai-chat-completions'] } },
|
|
703
|
+
};
|
|
704
|
+
const higher = makePeer('b', ['openai']);
|
|
705
|
+
higher.reputationScore = 90;
|
|
706
|
+
higher.providerServiceApiProtocols = {
|
|
707
|
+
openai: { services: { 'openai-gpt-56-sol': ['openai-chat-completions'] } },
|
|
708
|
+
};
|
|
709
|
+
const proxy = makeBuyerProxyWithPeers([lower, higher], [lower, higher], permissiveRouter());
|
|
710
|
+
proxy._defaultRoutedModel = 'gpt-5.6-sol';
|
|
711
|
+
let selectedPeerId = '';
|
|
712
|
+
let selectedBody = null;
|
|
713
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
714
|
+
selectedPeerId = peer.peerId;
|
|
715
|
+
selectedBody = parseJsonBody(request.body);
|
|
716
|
+
return {
|
|
717
|
+
requestId: request.requestId,
|
|
718
|
+
statusCode: 200,
|
|
719
|
+
headers: { 'content-type': 'application/json' },
|
|
720
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
721
|
+
};
|
|
722
|
+
};
|
|
723
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'antseed', messages: [] } }));
|
|
724
|
+
assert.equal(res.statusCode, 200);
|
|
725
|
+
assert.equal(selectedPeerId, higher.peerId);
|
|
726
|
+
assert.equal(selectedBody?.['model'], 'openai-gpt-56-sol');
|
|
727
|
+
});
|
|
728
|
+
test('model-only routing skips higher-reputation peers rejected by buyer policy', async () => {
|
|
729
|
+
const allowed = makePeer('a', ['openai']);
|
|
730
|
+
allowed.reputationScore = 60;
|
|
731
|
+
allowed.providerServiceApiProtocols = {
|
|
732
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
733
|
+
};
|
|
734
|
+
const rejected = makePeer('b', ['openai']);
|
|
735
|
+
rejected.reputationScore = 95;
|
|
736
|
+
rejected.providerServiceApiProtocols = {
|
|
737
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
738
|
+
};
|
|
739
|
+
const router = {
|
|
740
|
+
allowsPeerForPolicy: (_request, peer) => peer.peerId === allowed.peerId,
|
|
741
|
+
onResult: () => { },
|
|
742
|
+
};
|
|
743
|
+
const proxy = makeBuyerProxyWithPeers([allowed, rejected], [allowed, rejected], router);
|
|
744
|
+
let selectedPeerId = '';
|
|
745
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
746
|
+
selectedPeerId = peer.peerId;
|
|
747
|
+
return {
|
|
748
|
+
requestId: request.requestId,
|
|
749
|
+
statusCode: 200,
|
|
750
|
+
headers: { 'content-type': 'application/json' },
|
|
751
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
752
|
+
};
|
|
753
|
+
};
|
|
754
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
|
|
755
|
+
assert.equal(res.statusCode, 200);
|
|
756
|
+
assert.equal(selectedPeerId, allowed.peerId);
|
|
757
|
+
});
|
|
758
|
+
test('/models order and model-only dispatch use the same Price + Trust ranking', async () => {
|
|
759
|
+
const cobaltRelay = makePeer('a', ['openai']);
|
|
760
|
+
cobaltRelay.reputationScore = 99;
|
|
761
|
+
cobaltRelay.providerPricing = {
|
|
762
|
+
openai: { defaults: { inputUsdPerMillion: 1.88, outputUsdPerMillion: 9.38 } },
|
|
763
|
+
};
|
|
764
|
+
cobaltRelay.providerServiceApiProtocols = {
|
|
765
|
+
openai: { services: { 'kimi-k3': ['openai-chat-completions'] } },
|
|
766
|
+
};
|
|
767
|
+
const emberRoute = makePeer('b', ['openai']);
|
|
768
|
+
emberRoute.reputationScore = 96;
|
|
769
|
+
emberRoute.providerPricing = {
|
|
770
|
+
openai: { defaults: { inputUsdPerMillion: 0.9, outputUsdPerMillion: 2.7 } },
|
|
771
|
+
};
|
|
772
|
+
emberRoute.providerServiceApiProtocols = {
|
|
773
|
+
openai: { services: { 'Kimi K3': ['openai-chat-completions'] } },
|
|
774
|
+
};
|
|
775
|
+
const proxy = makeBuyerProxyWithPeers([cobaltRelay, emberRoute], [cobaltRelay, emberRoute], permissiveRouter(), undefined, priceAndTrustPreferences);
|
|
776
|
+
let selectedPeerId = '';
|
|
777
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
778
|
+
selectedPeerId = peer.peerId;
|
|
779
|
+
return {
|
|
780
|
+
requestId: request.requestId,
|
|
781
|
+
statusCode: 200,
|
|
782
|
+
headers: { 'content-type': 'application/json' },
|
|
783
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
784
|
+
};
|
|
785
|
+
};
|
|
786
|
+
const modelsRes = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/kimi-k3' }));
|
|
787
|
+
const peerOrder = JSON.parse(modelsRes.body).peers
|
|
788
|
+
.map((peer) => peer.peerId);
|
|
789
|
+
const completionRes = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'kimi-k3', messages: [] } }));
|
|
790
|
+
assert.equal(modelsRes.statusCode, 200);
|
|
791
|
+
assert.deepEqual(peerOrder, [emberRoute.peerId, cobaltRelay.peerId]);
|
|
792
|
+
assert.equal(completionRes.statusCode, 200);
|
|
793
|
+
assert.equal(selectedPeerId, emberRoute.peerId);
|
|
794
|
+
});
|
|
795
|
+
test('Price + Trust routing falls back after the preferred cheaper peer fails', async () => {
|
|
796
|
+
const cobaltRelay = makePeer('a', ['openai']);
|
|
797
|
+
cobaltRelay.reputationScore = 99;
|
|
798
|
+
cobaltRelay.providerPricing = {
|
|
799
|
+
openai: { defaults: { inputUsdPerMillion: 1.88, outputUsdPerMillion: 9.38 } },
|
|
800
|
+
};
|
|
801
|
+
cobaltRelay.providerServiceApiProtocols = {
|
|
802
|
+
openai: { services: { 'kimi-k3': ['openai-chat-completions'] } },
|
|
803
|
+
};
|
|
804
|
+
const emberRoute = makePeer('b', ['openai']);
|
|
805
|
+
emberRoute.reputationScore = 96;
|
|
806
|
+
emberRoute.providerPricing = {
|
|
807
|
+
openai: { defaults: { inputUsdPerMillion: 0.9, outputUsdPerMillion: 2.7 } },
|
|
808
|
+
};
|
|
809
|
+
emberRoute.providerServiceApiProtocols = {
|
|
810
|
+
openai: { services: { 'Kimi K3': ['openai-chat-completions'] } },
|
|
811
|
+
};
|
|
812
|
+
const proxy = makeBuyerProxyWithPeers([cobaltRelay, emberRoute], [cobaltRelay, emberRoute], permissiveRouter(), undefined, priceAndTrustPreferences);
|
|
813
|
+
const attempts = [];
|
|
814
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
815
|
+
attempts.push(peer.peerId);
|
|
816
|
+
return {
|
|
817
|
+
requestId: request.requestId,
|
|
818
|
+
statusCode: peer.peerId === emberRoute.peerId ? 503 : 200,
|
|
819
|
+
headers: { 'content-type': 'application/json' },
|
|
820
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
821
|
+
};
|
|
822
|
+
};
|
|
823
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'kimi-k3', messages: [] } }));
|
|
824
|
+
assert.equal(res.statusCode, 200);
|
|
825
|
+
assert.deepEqual(attempts, [emberRoute.peerId, cobaltRelay.peerId]);
|
|
826
|
+
});
|
|
827
|
+
test('model-only routing falls back to the next reputable peer after a retryable failure', async () => {
|
|
828
|
+
const first = makePeer('a', ['openai']);
|
|
829
|
+
first.reputationScore = 95;
|
|
830
|
+
first.providerServiceApiProtocols = {
|
|
831
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
832
|
+
};
|
|
833
|
+
const second = makePeer('b', ['openai']);
|
|
834
|
+
second.reputationScore = 80;
|
|
835
|
+
second.providerServiceApiProtocols = {
|
|
836
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
837
|
+
};
|
|
838
|
+
const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
|
|
839
|
+
const attempts = [];
|
|
840
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
841
|
+
attempts.push(peer.peerId);
|
|
842
|
+
return {
|
|
843
|
+
requestId: request.requestId,
|
|
844
|
+
statusCode: peer.peerId === first.peerId ? 503 : 200,
|
|
845
|
+
headers: { 'content-type': 'application/json' },
|
|
846
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
847
|
+
};
|
|
848
|
+
};
|
|
849
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt5', messages: [] } }));
|
|
850
|
+
assert.equal(res.statusCode, 200);
|
|
851
|
+
assert.deepEqual(attempts, [first.peerId, second.peerId]);
|
|
852
|
+
assert.equal(JSON.parse(res.body).peerId, second.peerId);
|
|
853
|
+
});
|
|
854
|
+
test('model-only routing retries a rate-limited peer before falling back', async () => {
|
|
855
|
+
const first = makePeer('a', ['openai']);
|
|
856
|
+
first.reputationScore = 95;
|
|
857
|
+
first.providerServiceApiProtocols = {
|
|
858
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
859
|
+
};
|
|
860
|
+
const second = makePeer('b', ['openai']);
|
|
861
|
+
second.reputationScore = 80;
|
|
862
|
+
second.providerServiceApiProtocols = {
|
|
863
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
864
|
+
};
|
|
865
|
+
const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
|
|
866
|
+
const attempts = [];
|
|
867
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
868
|
+
attempts.push(peer.peerId);
|
|
869
|
+
const firstAttempts = attempts.filter((peerId) => peerId === first.peerId).length;
|
|
870
|
+
const statusCode = peer.peerId === first.peerId && firstAttempts < 3 ? 429 : 200;
|
|
871
|
+
return {
|
|
872
|
+
requestId: request.requestId,
|
|
873
|
+
statusCode,
|
|
874
|
+
headers: { 'content-type': 'application/json', 'retry-after': '0' },
|
|
875
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
876
|
+
};
|
|
877
|
+
};
|
|
878
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
|
|
879
|
+
assert.equal(res.statusCode, 200);
|
|
880
|
+
assert.deepEqual(attempts, [first.peerId, first.peerId, first.peerId]);
|
|
881
|
+
});
|
|
882
|
+
test('model-only routing falls back after three rate-limit responses from one peer', async () => {
|
|
883
|
+
const first = makePeer('a', ['openai']);
|
|
884
|
+
first.reputationScore = 95;
|
|
885
|
+
first.providerServiceApiProtocols = {
|
|
886
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
887
|
+
};
|
|
888
|
+
const second = makePeer('b', ['openai']);
|
|
889
|
+
second.reputationScore = 80;
|
|
890
|
+
second.providerServiceApiProtocols = {
|
|
891
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
892
|
+
};
|
|
893
|
+
const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
|
|
894
|
+
const attempts = [];
|
|
895
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
896
|
+
attempts.push(peer.peerId);
|
|
897
|
+
return {
|
|
898
|
+
requestId: request.requestId,
|
|
899
|
+
statusCode: peer.peerId === first.peerId ? 429 : 200,
|
|
900
|
+
headers: { 'content-type': 'application/json', 'retry-after': '0' },
|
|
901
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
902
|
+
};
|
|
903
|
+
};
|
|
904
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
|
|
905
|
+
assert.equal(res.statusCode, 200);
|
|
906
|
+
assert.deepEqual(attempts, [first.peerId, first.peerId, first.peerId, second.peerId]);
|
|
907
|
+
});
|
|
908
|
+
test('model-only routing skips a cooling-down peer when another offer is ready', async () => {
|
|
909
|
+
const clock = makeTestClock();
|
|
910
|
+
const cooling = makePeer('a', ['openai']);
|
|
911
|
+
cooling.reputationScore = 95;
|
|
912
|
+
cooling.providerServiceApiProtocols = {
|
|
913
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
914
|
+
};
|
|
915
|
+
const ready = makePeer('b', ['openai']);
|
|
916
|
+
ready.reputationScore = 70;
|
|
917
|
+
ready.providerServiceApiProtocols = {
|
|
918
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
919
|
+
};
|
|
920
|
+
const proxy = makeBuyerProxyWithPeers([cooling, ready], [cooling, ready], permissiveRouter(), clock.now);
|
|
921
|
+
proxy._peerHealth.set(cooling.peerId, {
|
|
922
|
+
failureStreak: 3,
|
|
923
|
+
windowStartedAt: clock.now(),
|
|
924
|
+
episodeStartedAt: clock.now(),
|
|
925
|
+
cooldownUntil: clock.now() + 60_000,
|
|
926
|
+
lastFailureAt: clock.now(),
|
|
927
|
+
lastSuccessAt: 0,
|
|
928
|
+
lastReason: 'seller-5xx',
|
|
929
|
+
});
|
|
930
|
+
let selectedPeerId = '';
|
|
931
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
932
|
+
selectedPeerId = peer.peerId;
|
|
933
|
+
return {
|
|
934
|
+
requestId: request.requestId,
|
|
935
|
+
statusCode: 200,
|
|
936
|
+
headers: { 'content-type': 'application/json' },
|
|
937
|
+
body: Buffer.from('{}'),
|
|
938
|
+
};
|
|
939
|
+
};
|
|
940
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
|
|
941
|
+
assert.equal(res.statusCode, 200);
|
|
942
|
+
assert.equal(selectedPeerId, ready.peerId);
|
|
943
|
+
});
|
|
944
|
+
test('model-only routing does not fail over after a buyer-attributed failure', async () => {
|
|
945
|
+
const first = makePeer('a', ['openai']);
|
|
946
|
+
first.reputationScore = 95;
|
|
947
|
+
first.providerServiceApiProtocols = {
|
|
948
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
949
|
+
};
|
|
950
|
+
const second = makePeer('b', ['openai']);
|
|
951
|
+
second.reputationScore = 80;
|
|
952
|
+
second.providerServiceApiProtocols = {
|
|
953
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
954
|
+
};
|
|
955
|
+
const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
|
|
956
|
+
const attempts = [];
|
|
957
|
+
proxy._node.sendRequest = async (peer) => {
|
|
958
|
+
attempts.push(peer.peerId);
|
|
959
|
+
throw buyerFault('Buyer has insufficient deposits', 'buyer-deposits-insufficient');
|
|
960
|
+
};
|
|
961
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
|
|
962
|
+
assert.equal(res.statusCode, 503);
|
|
963
|
+
assert.deepEqual(attempts, [first.peerId]);
|
|
964
|
+
assert.equal(JSON.parse(res.body).error.code, ANTSEED_BUYER_FAULT_ERROR_CODE);
|
|
965
|
+
});
|
|
340
966
|
test('pinned proxy request reports when the pinned peer is not discoverable', async () => {
|
|
341
967
|
const pinnedPeerId = 'a'.repeat(40);
|
|
342
968
|
const otherPeer = makePeer('b', ['openai']);
|
|
@@ -351,6 +977,52 @@ test('pinned proxy request reports when the pinned peer is not discoverable', as
|
|
|
351
977
|
assert.match(res.body, /is not reachable right now/);
|
|
352
978
|
assert.match(res.body, /It may be offline, not announcing, or temporarily unreachable/);
|
|
353
979
|
});
|
|
980
|
+
test('pinned proxy request rewrites a canonical alias to the advertised service id', async () => {
|
|
981
|
+
const pinnedPeer = makePeer('a', ['openai']);
|
|
982
|
+
pinnedPeer.providerServiceApiProtocols = {
|
|
983
|
+
openai: { services: { 'gpt-5.6-sol': ['openai-chat-completions'] } },
|
|
984
|
+
};
|
|
985
|
+
const proxy = makeBuyerProxyWithPeers([pinnedPeer], [pinnedPeer], permissiveRouter());
|
|
986
|
+
let selectedBody = null;
|
|
987
|
+
proxy._node.sendRequest = async (_peer, request) => {
|
|
988
|
+
selectedBody = parseJsonBody(request.body);
|
|
989
|
+
return {
|
|
990
|
+
requestId: request.requestId,
|
|
991
|
+
statusCode: 200,
|
|
992
|
+
headers: { 'content-type': 'application/json' },
|
|
993
|
+
body: Buffer.from('{}'),
|
|
994
|
+
};
|
|
995
|
+
};
|
|
996
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
997
|
+
headers: { 'x-antseed-pin-peer': pinnedPeer.peerId },
|
|
998
|
+
body: { model: 'gpt-56-sol', messages: [] },
|
|
999
|
+
}));
|
|
1000
|
+
assert.equal(res.statusCode, 200);
|
|
1001
|
+
assert.equal(selectedBody?.['model'], 'gpt-5.6-sol');
|
|
1002
|
+
});
|
|
1003
|
+
test('pinned proxy request dispatches when stale metadata misses the requested model', async () => {
|
|
1004
|
+
const pinnedPeer = makePeer('a', ['openai']);
|
|
1005
|
+
pinnedPeer.providerServiceApiProtocols = {
|
|
1006
|
+
openai: { services: { 'known-model': ['openai-chat-completions'] } },
|
|
1007
|
+
};
|
|
1008
|
+
const proxy = makeBuyerProxyWithPeers([pinnedPeer], [pinnedPeer], permissiveRouter());
|
|
1009
|
+
let selectedBody = null;
|
|
1010
|
+
proxy._node.sendRequest = async (_peer, request) => {
|
|
1011
|
+
selectedBody = parseJsonBody(request.body);
|
|
1012
|
+
return {
|
|
1013
|
+
requestId: request.requestId,
|
|
1014
|
+
statusCode: 200,
|
|
1015
|
+
headers: { 'content-type': 'application/json' },
|
|
1016
|
+
body: Buffer.from('{}'),
|
|
1017
|
+
};
|
|
1018
|
+
};
|
|
1019
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
1020
|
+
headers: { 'x-antseed-pin-peer': pinnedPeer.peerId },
|
|
1021
|
+
body: { model: 'new-helper-model', messages: [] },
|
|
1022
|
+
}));
|
|
1023
|
+
assert.equal(res.statusCode, 200);
|
|
1024
|
+
assert.equal(selectedBody?.['model'], 'new-helper-model');
|
|
1025
|
+
});
|
|
354
1026
|
test('pinned proxy request reports explicit provider mismatch separately', async () => {
|
|
355
1027
|
const pinnedPeer = makePeer('a', ['local-llm']);
|
|
356
1028
|
const proxy = makeBuyerProxyWithPeers([pinnedPeer]);
|
|
@@ -661,29 +1333,126 @@ test('a non-standard seller 5xx proves reachability and restarts the failure str
|
|
|
661
1333
|
assert.equal(healthOf(proxy, peer)?.cooldownUntil, 0);
|
|
662
1334
|
assert.equal(healthOf(proxy, peer)?.failureStreak, 1);
|
|
663
1335
|
});
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1336
|
+
function makeNetworkModelPeers() {
|
|
1337
|
+
const textPeer = makePeer('a', ['openai']);
|
|
1338
|
+
textPeer.providerPricing = {
|
|
1339
|
+
openai: {
|
|
1340
|
+
defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
|
|
1341
|
+
services: { 'qwen3-coder': { inputUsdPerMillion: 3, outputUsdPerMillion: 4 } },
|
|
1342
|
+
},
|
|
1343
|
+
};
|
|
1344
|
+
textPeer.providerServiceApiProtocols = {
|
|
1345
|
+
openai: { services: { 'qwen3-coder': ['openai-responses'] } },
|
|
1346
|
+
};
|
|
1347
|
+
textPeer.providerServiceCapabilities = {
|
|
1348
|
+
openai: {
|
|
1349
|
+
services: {
|
|
1350
|
+
'qwen3-coder': {
|
|
1351
|
+
contextWindow: 128_000,
|
|
1352
|
+
maxOutputTokens: 32_000,
|
|
1353
|
+
inputs: ['text', 'image'],
|
|
1354
|
+
outputs: ['text'],
|
|
1355
|
+
reasoning: true,
|
|
1356
|
+
toolUse: true,
|
|
1357
|
+
structuredOutput: true,
|
|
1358
|
+
supportedParameters: ['temperature', 'tools'],
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1362
|
+
};
|
|
1363
|
+
textPeer.providerServiceCategories = {
|
|
1364
|
+
openai: { services: { 'qwen3-coder': ['chat', 'reasoning'] } },
|
|
1365
|
+
};
|
|
1366
|
+
const imagePeer = makePeer('b', ['openai']);
|
|
1367
|
+
imagePeer.providerServiceApiProtocols = {
|
|
1368
|
+
openai: { services: { 'flux-1-schnell': ['openai-images'] } },
|
|
1369
|
+
};
|
|
1370
|
+
const aliasPeer = makePeer('c', ['anthropic']);
|
|
1371
|
+
aliasPeer.providerServiceApiProtocols = {
|
|
1372
|
+
anthropic: { services: { 'Claude Opus 5': ['anthropic-messages'] } },
|
|
1373
|
+
};
|
|
1374
|
+
const secondAliasPeer = makePeer('d', ['anthropic']);
|
|
1375
|
+
secondAliasPeer.providerServiceApiProtocols = {
|
|
1376
|
+
anthropic: { services: { 'opus-5': ['anthropic-messages'] } },
|
|
1377
|
+
};
|
|
1378
|
+
return [textPeer, imagePeer, aliasPeer, secondAliasPeer];
|
|
1379
|
+
}
|
|
1380
|
+
test('GET /v1/models is answered locally with the network-wide model list', async () => {
|
|
1381
|
+
const peers = makeNetworkModelPeers();
|
|
1382
|
+
const proxy = makeBuyerProxyWithPeers(peers);
|
|
1383
|
+
let forwarded = 0;
|
|
1384
|
+
proxy._node.sendRequest = async () => {
|
|
1385
|
+
forwarded += 1;
|
|
1386
|
+
throw new Error('must not reach a peer');
|
|
1387
|
+
};
|
|
1388
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models' }));
|
|
1389
|
+
assert.equal(res.statusCode, 200);
|
|
1390
|
+
// The port-reuse probe in `buyer start` identifies the proxy by this header.
|
|
1391
|
+
assert.ok(res.headers['x-antseed-request-id']);
|
|
1392
|
+
const body = JSON.parse(res.body);
|
|
1393
|
+
assert.equal(body.object, 'list');
|
|
1394
|
+
assert.deepEqual(body.data.map((model) => model.id), ['Claude Opus 5', 'flux-1-schnell', 'qwen3-coder']);
|
|
1395
|
+
const opus = body.data[0];
|
|
1396
|
+
assert.equal(opus.name, 'Claude Opus 5');
|
|
1397
|
+
assert.deepEqual(opus.peers.map((peer) => peer.serviceId), ['Claude Opus 5', 'opus-5']);
|
|
1398
|
+
const flux = body.data[1];
|
|
1399
|
+
assert.equal(flux.type, 'image');
|
|
1400
|
+
assert.equal(flux.peers[0]?.peerId, peers[1]?.peerId);
|
|
1401
|
+
const qwen = body.data[2];
|
|
1402
|
+
assert.equal(qwen.name, 'Qwen3 Coder');
|
|
1403
|
+
assert.equal(qwen.type, 'text');
|
|
1404
|
+
assert.deepEqual(qwen.supported_protocols, ['openai-responses']);
|
|
1405
|
+
assert.equal(qwen.context_length, 128_000);
|
|
1406
|
+
assert.equal(qwen.max_output_tokens, 32_000);
|
|
1407
|
+
assert.deepEqual(qwen.architecture, {
|
|
1408
|
+
input_modalities: ['image', 'text'],
|
|
1409
|
+
output_modalities: ['text'],
|
|
679
1410
|
});
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
})
|
|
685
|
-
assert.
|
|
686
|
-
assert.equal(
|
|
1411
|
+
assert.deepEqual(qwen.capabilities, {
|
|
1412
|
+
reasoning: true,
|
|
1413
|
+
tool_use: true,
|
|
1414
|
+
structured_output: true,
|
|
1415
|
+
});
|
|
1416
|
+
assert.deepEqual(qwen.supported_parameters, ['temperature', 'tools']);
|
|
1417
|
+
assert.equal(qwen.capability_coverage.context_length, 1);
|
|
1418
|
+
assert.equal(qwen.peers[0]?.protocol, 'openai-responses');
|
|
1419
|
+
assert.deepEqual(qwen.peers[0]?.categories, ['chat', 'reasoning']);
|
|
1420
|
+
assert.equal(qwen.peers[0]?.capabilities?.contextWindow, 128_000);
|
|
1421
|
+
assert.equal(qwen.peers[0]?.inputUsdPerMillion, 3);
|
|
1422
|
+
assert.equal(forwarded, 0);
|
|
1423
|
+
});
|
|
1424
|
+
test('GET /v1/models?type= filters by model type and rejects unknown types', async () => {
|
|
1425
|
+
const proxy = makeBuyerProxyWithPeers(makeNetworkModelPeers());
|
|
1426
|
+
const images = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models?type=images' }));
|
|
1427
|
+
assert.equal(images.statusCode, 200);
|
|
1428
|
+
assert.deepEqual(JSON.parse(images.body).data.map((model) => model.id), ['flux-1-schnell']);
|
|
1429
|
+
const text = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models?type=text' }));
|
|
1430
|
+
assert.deepEqual(JSON.parse(text.body).data.map((model) => model.id), ['Claude Opus 5', 'qwen3-coder']);
|
|
1431
|
+
const bad = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models?type=audio' }));
|
|
1432
|
+
assert.equal(bad.statusCode, 400);
|
|
1433
|
+
assert.equal(JSON.parse(bad.body).error.param, 'type');
|
|
1434
|
+
});
|
|
1435
|
+
test('GET /v1/models/:id looks up a single model across the network', async () => {
|
|
1436
|
+
const proxy = makeBuyerProxyWithPeers(makeNetworkModelPeers());
|
|
1437
|
+
const hit = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/QWEN3-coder' }));
|
|
1438
|
+
assert.equal(hit.statusCode, 200);
|
|
1439
|
+
const hitBody = JSON.parse(hit.body);
|
|
1440
|
+
assert.equal(hitBody.id, 'qwen3-coder');
|
|
1441
|
+
assert.equal(hitBody.context_length, 128_000);
|
|
1442
|
+
assert.equal(hitBody.peers[0]?.capabilities?.toolUse, true);
|
|
1443
|
+
const hitWithIgnoredType = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/QWEN3-coder?type=audio' }));
|
|
1444
|
+
assert.equal(hitWithIgnoredType.statusCode, 200);
|
|
1445
|
+
assert.equal(JSON.parse(hitWithIgnoredType.body).id, 'qwen3-coder');
|
|
1446
|
+
const aliasHit = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/opus-5' }));
|
|
1447
|
+
assert.equal(aliasHit.statusCode, 200);
|
|
1448
|
+
assert.equal(JSON.parse(aliasHit.body).id, 'Claude Opus 5');
|
|
1449
|
+
const miss = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/does-not-exist' }));
|
|
1450
|
+
assert.equal(miss.statusCode, 404);
|
|
1451
|
+
assert.equal(JSON.parse(miss.body).error.code, 'model_not_found');
|
|
1452
|
+
const malformed = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/gpt%zz' }));
|
|
1453
|
+
assert.equal(malformed.statusCode, 404);
|
|
1454
|
+
assert.equal(malformed.headers['content-type'], 'application/json');
|
|
1455
|
+
assert.equal(JSON.parse(malformed.body).error.code, 'model_not_found');
|
|
687
1456
|
});
|
|
688
1457
|
test('a buyer-side outage rolls back the cooldowns it caused', async () => {
|
|
689
1458
|
const clock = makeTestClock();
|
|
@@ -756,34 +1525,6 @@ test('POST /_antseed/peer-health/clear rejects a malformed peer id', async () =>
|
|
|
756
1525
|
}));
|
|
757
1526
|
assert.equal(res.statusCode, 400);
|
|
758
1527
|
});
|
|
759
|
-
test('/v1/models retryable response reports router success', async () => {
|
|
760
|
-
const peer = makePeer('a', ['openai']);
|
|
761
|
-
const routerResults = [];
|
|
762
|
-
const router = {
|
|
763
|
-
allowsPeerForPolicy: () => true,
|
|
764
|
-
onResult: (_peer, result) => {
|
|
765
|
-
routerResults.push(result);
|
|
766
|
-
},
|
|
767
|
-
};
|
|
768
|
-
const proxy = makeBuyerProxyWithPeers([peer], [peer], router);
|
|
769
|
-
proxy._node.sendRequest = async (_peer, request) => ({
|
|
770
|
-
requestId: request.requestId,
|
|
771
|
-
statusCode: 500,
|
|
772
|
-
headers: { 'content-type': 'text/plain' },
|
|
773
|
-
body: Buffer.from('model probe failed'),
|
|
774
|
-
});
|
|
775
|
-
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
776
|
-
method: 'GET',
|
|
777
|
-
path: '/v1/models',
|
|
778
|
-
headers: {
|
|
779
|
-
'x-antseed-pin-peer': peer.peerId,
|
|
780
|
-
},
|
|
781
|
-
}));
|
|
782
|
-
assert.equal(res.statusCode, 500);
|
|
783
|
-
assert.match(res.body, /model probe failed/);
|
|
784
|
-
assert.equal(routerResults.length, 1);
|
|
785
|
-
assert.equal(routerResults[0]?.success, true);
|
|
786
|
-
});
|
|
787
1528
|
test('non-stream transformed responses requests force upstream stream without streaming to client', async () => {
|
|
788
1529
|
const peer = makePeer('a', ['openai-responses']);
|
|
789
1530
|
peer.providerServiceApiProtocols = {
|
|
@@ -1108,6 +1849,27 @@ test('parsePersistedPeers preserves provider metadata so routing filters still w
|
|
|
1108
1849
|
assert.equal(result.candidatePeers[0]?.peerId, validPeerId);
|
|
1109
1850
|
assert.equal(result.routePlanByPeerId.get(validPeerId)?.provider, 'claude-oauth');
|
|
1110
1851
|
});
|
|
1852
|
+
test('parsePersistedPeers re-derives on-chain reputation from persisted stats', () => {
|
|
1853
|
+
const persisted = {
|
|
1854
|
+
discoveredPeers: [
|
|
1855
|
+
{
|
|
1856
|
+
peerId: validPeerId,
|
|
1857
|
+
providers: ['claude-oauth'],
|
|
1858
|
+
lastSeen: NOW - 5_000,
|
|
1859
|
+
onChainStakeUsdcMicros: 2_000_000,
|
|
1860
|
+
onChainChannelCount: 20,
|
|
1861
|
+
onChainGhostCount: 0,
|
|
1862
|
+
onChainTotalVolumeUsdcMicros: 100_000_000,
|
|
1863
|
+
onChainLastSettledAtSec: Math.floor((NOW - 60_000) / 1000),
|
|
1864
|
+
onChainStakedAtSec: Math.floor((NOW - 40 * 86_400_000) / 1000),
|
|
1865
|
+
},
|
|
1866
|
+
],
|
|
1867
|
+
};
|
|
1868
|
+
const [peer] = parsePersistedPeers(persisted, NOW);
|
|
1869
|
+
assert.ok(peer);
|
|
1870
|
+
assert.equal(peer.onChainReputationScore, computeOnChainReputationScore(peer, NOW));
|
|
1871
|
+
assert.ok((peer.onChainReputationScore ?? 0) > 0);
|
|
1872
|
+
});
|
|
1111
1873
|
test('parsePersistedPeers restores sellerContract into peer.metadata', () => {
|
|
1112
1874
|
// Regression: dropping sellerContract through the persistence layer caused
|
|
1113
1875
|
// SellerAddressResolver to fall back to peerIdToAddress, so the buyer signed
|
|
@@ -1333,7 +2095,12 @@ test('route control endpoint sets, persists, and returns the default routed mode
|
|
|
1333
2095
|
assert.deepEqual(JSON.parse(get.body), { ok: true, model: `${validPeerId}@gpt-4o` });
|
|
1334
2096
|
const persisted = JSON.parse(await readFile(join(dir, 'buyer.state.json'), 'utf-8'));
|
|
1335
2097
|
assert.equal(persisted['defaultRoutedModel'], `${validPeerId}@gpt-4o`);
|
|
1336
|
-
const
|
|
2098
|
+
const automatic = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/route', body: { model: 'gpt-4o' } }));
|
|
2099
|
+
assert.equal(automatic.statusCode, 200);
|
|
2100
|
+
assert.deepEqual(JSON.parse(automatic.body), { ok: true, model: 'gpt-4o' });
|
|
2101
|
+
const automaticPersisted = JSON.parse(await readFile(join(dir, 'buyer.state.json'), 'utf-8'));
|
|
2102
|
+
assert.equal(automaticPersisted['defaultRoutedModel'], 'gpt-4o');
|
|
2103
|
+
const invalid = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/route', body: { model: 'not-a-peer@gpt-4o' } }));
|
|
1337
2104
|
assert.equal(invalid.statusCode, 400);
|
|
1338
2105
|
const cleared = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/route', body: { model: '' } }));
|
|
1339
2106
|
assert.deepEqual(JSON.parse(cleared.body), { ok: true, model: null });
|
|
@@ -1399,7 +2166,7 @@ test('per-chat pin overrides the default routed model for the antseed alias', as
|
|
|
1399
2166
|
proxy._defaultRoutedModel = defaultRoute;
|
|
1400
2167
|
const store = proxy._conversations;
|
|
1401
2168
|
store.touch({ tool: 'codex-exec', sessionKey: 'sess-1' });
|
|
1402
|
-
store.setPinnedModel('codex-exec:sess-1', pinnedRoute);
|
|
2169
|
+
store.setPinnedModel('codex-exec:sess-1', pinnedRoute, 'user');
|
|
1403
2170
|
await invokeProxy(proxy, makeProxyRequest({
|
|
1404
2171
|
path: '/v1/responses',
|
|
1405
2172
|
headers: { originator: 'codex_exec', 'session-id': 'sess-1' },
|
|
@@ -1446,7 +2213,7 @@ test('chat pin overrides a system-proxy-routed model on intercepted requests', a
|
|
|
1446
2213
|
}));
|
|
1447
2214
|
assert.equal(store.get('claude-code:cc-1')?.pinnedModel, proxyRoute);
|
|
1448
2215
|
// The user re-pins the chat from the desktop (float / chats view).
|
|
1449
|
-
store.setPinnedModel('claude-code:cc-1', pinnedRoute);
|
|
2216
|
+
store.setPinnedModel('claude-code:cc-1', pinnedRoute, 'user');
|
|
1450
2217
|
// Later requests still arrive with the proxy-assigned model; the pin wins.
|
|
1451
2218
|
await invokeProxy(proxy, makeProxyRequest({
|
|
1452
2219
|
path: '/v1/messages',
|
|
@@ -1542,9 +2309,15 @@ test('conversation control endpoints list, rename, pin, reject bad pins, delete'
|
|
|
1542
2309
|
}));
|
|
1543
2310
|
assert.equal(rename.statusCode, 200);
|
|
1544
2311
|
assert.equal(store.get('opencode:ses_x')?.label, 'Login refactor');
|
|
2312
|
+
const automaticPin = await invokeProxy(proxy, makeProxyRequest({
|
|
2313
|
+
path: '/_antseed/conversations/update',
|
|
2314
|
+
body: { id: 'opencode:ses_x', pinnedModel: 'gpt-5.6-sol', peerSource: 'auto' },
|
|
2315
|
+
}));
|
|
2316
|
+
assert.equal(automaticPin.statusCode, 200);
|
|
2317
|
+
assert.equal(store.getPinnedModel('opencode', 'ses_x'), 'gpt-5.6-sol');
|
|
1545
2318
|
const badPin = await invokeProxy(proxy, makeProxyRequest({
|
|
1546
2319
|
path: '/_antseed/conversations/update',
|
|
1547
|
-
body: { id: 'opencode:ses_x', pinnedModel: 'not-a-
|
|
2320
|
+
body: { id: 'opencode:ses_x', pinnedModel: 'not-a-peer@gpt-5.6-sol' },
|
|
1548
2321
|
}));
|
|
1549
2322
|
assert.equal(badPin.statusCode, 400);
|
|
1550
2323
|
const goodPin = await invokeProxy(proxy, makeProxyRequest({
|
|
@@ -1739,4 +2512,69 @@ test('sweepStaleStateTmpFiles: removes aged temp files, keeps fresh and unrelate
|
|
|
1739
2512
|
test('sweepStaleStateTmpFiles: tolerates a missing directory', async () => {
|
|
1740
2513
|
await sweepStaleStateTmpFiles(join(tmpdir(), 'antseed-does-not-exist', randomUUID()));
|
|
1741
2514
|
});
|
|
2515
|
+
// ---------- Deposit watcher control plane ----------
|
|
2516
|
+
test('deposits/status reports no watcher until one is attached', async () => {
|
|
2517
|
+
const proxy = new BuyerProxy({
|
|
2518
|
+
port: 0,
|
|
2519
|
+
dataDir: '/tmp/antseed-test',
|
|
2520
|
+
node: { router: null },
|
|
2521
|
+
});
|
|
2522
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/_antseed/deposits/status' }));
|
|
2523
|
+
assert.equal(res.statusCode, 200);
|
|
2524
|
+
assert.deepEqual(JSON.parse(res.body), { ok: true, watcher: false, status: null });
|
|
2525
|
+
});
|
|
2526
|
+
test('deposits/watch returns 503 when no watcher is attached', async () => {
|
|
2527
|
+
const proxy = new BuyerProxy({
|
|
2528
|
+
port: 0,
|
|
2529
|
+
dataDir: '/tmp/antseed-test',
|
|
2530
|
+
node: { router: null },
|
|
2531
|
+
});
|
|
2532
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/deposits/watch', body: { mode: 'active' } }));
|
|
2533
|
+
assert.equal(res.statusCode, 503);
|
|
2534
|
+
assert.equal(JSON.parse(res.body).ok, false);
|
|
2535
|
+
});
|
|
2536
|
+
test('deposits/watch promotes and demotes an attached watcher and returns its status', async () => {
|
|
2537
|
+
const proxy = new BuyerProxy({
|
|
2538
|
+
port: 0,
|
|
2539
|
+
dataDir: '/tmp/antseed-test',
|
|
2540
|
+
node: { router: null },
|
|
2541
|
+
});
|
|
2542
|
+
const calls = [];
|
|
2543
|
+
const fakeStatus = {
|
|
2544
|
+
mode: 'idle',
|
|
2545
|
+
address: '0x' + 'ab'.repeat(20),
|
|
2546
|
+
usdcBalance: '0',
|
|
2547
|
+
sweepInFlight: false,
|
|
2548
|
+
lastEvent: null,
|
|
2549
|
+
};
|
|
2550
|
+
proxy.setDepositWatcher({
|
|
2551
|
+
promote: () => { calls.push('promote'); },
|
|
2552
|
+
demote: () => { calls.push('demote'); },
|
|
2553
|
+
status: () => fakeStatus,
|
|
2554
|
+
});
|
|
2555
|
+
const active = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/deposits/watch', body: { mode: 'active' } }));
|
|
2556
|
+
assert.equal(active.statusCode, 200);
|
|
2557
|
+
assert.deepEqual(JSON.parse(active.body), { ok: true, status: fakeStatus });
|
|
2558
|
+
const background = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/deposits/watch', body: { mode: 'background' } }));
|
|
2559
|
+
assert.equal(background.statusCode, 200);
|
|
2560
|
+
assert.deepEqual(calls, ['promote', 'demote']);
|
|
2561
|
+
const invalid = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/deposits/watch', body: { mode: 'nonsense' } }));
|
|
2562
|
+
assert.equal(invalid.statusCode, 400);
|
|
2563
|
+
assert.deepEqual(calls, ['promote', 'demote']);
|
|
2564
|
+
const status = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/_antseed/deposits/status' }));
|
|
2565
|
+
assert.deepEqual(JSON.parse(status.body), { ok: true, watcher: true, status: fakeStatus });
|
|
2566
|
+
});
|
|
2567
|
+
test('getSweepReceipt returns cached relayer receipts case-insensitively', () => {
|
|
2568
|
+
const proxy = new BuyerProxy({
|
|
2569
|
+
port: 0,
|
|
2570
|
+
dataDir: '/tmp/antseed-test',
|
|
2571
|
+
node: { router: null },
|
|
2572
|
+
});
|
|
2573
|
+
const nonce = '0x' + 'AB'.repeat(32);
|
|
2574
|
+
const receipt = { authNonce: nonce.toLowerCase(), status: 'confirmed', txHash: '0x' + '12'.repeat(32) };
|
|
2575
|
+
proxy._sweepReceipts.set(nonce.toLowerCase(), receipt);
|
|
2576
|
+
assert.equal(proxy.getSweepReceipt(nonce), receipt);
|
|
2577
|
+
assert.equal(proxy.getSweepReceipt(nonce.toLowerCase()), receipt);
|
|
2578
|
+
assert.equal(proxy.getSweepReceipt('0x' + '00'.repeat(32)), null);
|
|
2579
|
+
});
|
|
1742
2580
|
//# sourceMappingURL=buyer-proxy.test.js.map
|