@antseed/cli 0.1.147 → 0.1.149
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 +18 -3
- 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 +10 -9
- 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/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/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +6 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +39 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/loader.test.js +34 -0
- package/dist/config/loader.test.js.map +1 -1
- package/dist/config/types.d.ts +3 -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/proxy/buyer-proxy.d.ts +20 -2
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +408 -58
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +828 -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/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 +5 -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,545 @@ 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-reputation 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-antstation-session-id'], undefined);
|
|
475
|
+
assert.equal(request.headers['x-antseed-prefer-peer'], undefined);
|
|
476
|
+
return {
|
|
477
|
+
requestId: request.requestId,
|
|
478
|
+
statusCode: peer.peerId === preferred.peerId && !preferredReachable ? 503 : 200,
|
|
479
|
+
headers: { 'content-type': 'application/json' },
|
|
480
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
const conversationHeaders = {
|
|
484
|
+
'x-antstation-session-id': 'conversation-soft-affinity',
|
|
485
|
+
'x-antseed-prefer-peer': preferred.peerId,
|
|
486
|
+
};
|
|
487
|
+
await invokeProxy(proxy, makeProxyRequest({
|
|
488
|
+
headers: conversationHeaders,
|
|
489
|
+
body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'hello' }] },
|
|
490
|
+
}));
|
|
491
|
+
assert.deepEqual(attempts, [preferred.peerId]);
|
|
492
|
+
preferredReachable = false;
|
|
493
|
+
attempts.length = 0;
|
|
494
|
+
await invokeProxy(proxy, makeProxyRequest({
|
|
495
|
+
headers: { 'x-antstation-session-id': 'conversation-soft-affinity' },
|
|
496
|
+
body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'again' }] },
|
|
497
|
+
}));
|
|
498
|
+
assert.deepEqual(attempts, [preferred.peerId, rankedFirst.peerId]);
|
|
499
|
+
const stored = proxy._conversations.get('antstation:conversation-soft-affinity');
|
|
500
|
+
assert.equal(stored?.pinnedModel, `${rankedFirst.peerId}@Kimi K3`);
|
|
501
|
+
assert.equal(stored?.lastModel, `${rankedFirst.peerId}@Kimi K3`);
|
|
502
|
+
attempts.length = 0;
|
|
503
|
+
await invokeProxy(proxy, makeProxyRequest({
|
|
504
|
+
headers: { 'x-antstation-session-id': 'conversation-soft-affinity' },
|
|
505
|
+
body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'third' }] },
|
|
506
|
+
}));
|
|
507
|
+
assert.equal(attempts[0], rankedFirst.peerId);
|
|
508
|
+
const route = await invokeProxy(proxy, makeProxyRequest({
|
|
509
|
+
method: 'GET',
|
|
510
|
+
path: `/_antseed/conversations/${encodeURIComponent('antstation:conversation-soft-affinity')}`,
|
|
511
|
+
}));
|
|
512
|
+
assert.equal(route.statusCode, 200);
|
|
513
|
+
assert.equal(JSON.parse(route.body).conversation.lastModel, `${rankedFirst.peerId}@Kimi K3`);
|
|
514
|
+
});
|
|
515
|
+
test('model-only request applies a cached-input pricing reputation penalty', async () => {
|
|
516
|
+
const priced = makePeer('a', ['openai']);
|
|
517
|
+
priced.reputationScore = 75;
|
|
518
|
+
priced.providerPricing = {
|
|
519
|
+
openai: {
|
|
520
|
+
defaults: { inputUsdPerMillion: 2, outputUsdPerMillion: 4 },
|
|
521
|
+
services: {
|
|
522
|
+
'cache-model': { inputUsdPerMillion: 2, outputUsdPerMillion: 4, cachedInputUsdPerMillion: 0.2 },
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
priced.providerServiceApiProtocols = {
|
|
527
|
+
openai: { services: { 'cache-model': ['openai-chat-completions'] } },
|
|
528
|
+
};
|
|
529
|
+
const unpriced = makePeer('b', ['openai']);
|
|
530
|
+
unpriced.reputationScore = 90;
|
|
531
|
+
unpriced.providerPricing = {
|
|
532
|
+
openai: {
|
|
533
|
+
defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
|
|
534
|
+
services: { 'cache-model': { inputUsdPerMillion: 1, outputUsdPerMillion: 2 } },
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
unpriced.providerServiceApiProtocols = {
|
|
538
|
+
openai: { services: { 'cache-model': ['openai-chat-completions'] } },
|
|
539
|
+
};
|
|
540
|
+
const proxy = makeBuyerProxyWithPeers([unpriced, priced], [unpriced, priced], permissiveRouter());
|
|
541
|
+
let selectedPeerId = '';
|
|
542
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
543
|
+
selectedPeerId = peer.peerId;
|
|
544
|
+
return {
|
|
545
|
+
requestId: request.requestId,
|
|
546
|
+
statusCode: 200,
|
|
547
|
+
headers: { 'content-type': 'application/json' },
|
|
548
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
549
|
+
};
|
|
550
|
+
};
|
|
551
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'cache-model', messages: [] } }));
|
|
552
|
+
assert.equal(res.statusCode, 200);
|
|
553
|
+
assert.equal(selectedPeerId, priced.peerId);
|
|
554
|
+
});
|
|
555
|
+
test('model-only cached-input pricing penalty does not bury a substantially stronger peer', async () => {
|
|
556
|
+
const priced = makePeer('a', ['openai']);
|
|
557
|
+
priced.reputationScore = 20;
|
|
558
|
+
priced.providerPricing = {
|
|
559
|
+
openai: {
|
|
560
|
+
defaults: { inputUsdPerMillion: 2, outputUsdPerMillion: 4 },
|
|
561
|
+
services: {
|
|
562
|
+
'cache-model': { inputUsdPerMillion: 2, outputUsdPerMillion: 4, cachedInputUsdPerMillion: 0.2 },
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
priced.providerServiceApiProtocols = {
|
|
567
|
+
openai: { services: { 'cache-model': ['openai-chat-completions'] } },
|
|
568
|
+
};
|
|
569
|
+
const unpriced = makePeer('b', ['openai']);
|
|
570
|
+
unpriced.reputationScore = 100;
|
|
571
|
+
unpriced.providerServiceApiProtocols = {
|
|
572
|
+
openai: { services: { 'cache-model': ['openai-chat-completions'] } },
|
|
573
|
+
};
|
|
574
|
+
const proxy = makeBuyerProxyWithPeers([priced, unpriced], [priced, unpriced], permissiveRouter());
|
|
575
|
+
let selectedPeerId = '';
|
|
576
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
577
|
+
selectedPeerId = peer.peerId;
|
|
578
|
+
return {
|
|
579
|
+
requestId: request.requestId,
|
|
580
|
+
statusCode: 200,
|
|
581
|
+
headers: { 'content-type': 'application/json' },
|
|
582
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
583
|
+
};
|
|
584
|
+
};
|
|
585
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'cache-model', messages: [] } }));
|
|
586
|
+
assert.equal(res.statusCode, 200);
|
|
587
|
+
assert.equal(selectedPeerId, unpriced.peerId);
|
|
588
|
+
});
|
|
589
|
+
test('model-only request keeps reputation ordering when cached-input pricing is absent for all peers', async () => {
|
|
590
|
+
const lower = makePeer('a', ['openai']);
|
|
591
|
+
lower.reputationScore = 20;
|
|
592
|
+
lower.providerServiceApiProtocols = {
|
|
593
|
+
openai: { services: { 'no-cache-model': ['openai-chat-completions'] } },
|
|
594
|
+
};
|
|
595
|
+
const higher = makePeer('b', ['openai']);
|
|
596
|
+
higher.reputationScore = 100;
|
|
597
|
+
higher.providerServiceApiProtocols = {
|
|
598
|
+
openai: { services: { 'no-cache-model': ['openai-chat-completions'] } },
|
|
599
|
+
};
|
|
600
|
+
const proxy = makeBuyerProxyWithPeers([lower, higher], [lower, higher], permissiveRouter());
|
|
601
|
+
let selectedPeerId = '';
|
|
602
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
603
|
+
selectedPeerId = peer.peerId;
|
|
604
|
+
return {
|
|
605
|
+
requestId: request.requestId,
|
|
606
|
+
statusCode: 200,
|
|
607
|
+
headers: { 'content-type': 'application/json' },
|
|
608
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
609
|
+
};
|
|
610
|
+
};
|
|
611
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'no-cache-model', messages: [] } }));
|
|
612
|
+
assert.equal(res.statusCode, 200);
|
|
613
|
+
assert.equal(selectedPeerId, higher.peerId);
|
|
614
|
+
});
|
|
615
|
+
test('model-only request routes through a legacy peer-wide service announcement', async () => {
|
|
616
|
+
const peer = makePeer('a', ['openai']);
|
|
617
|
+
peer.services = ['gpt-5.6-terra'];
|
|
618
|
+
const proxy = makeBuyerProxyWithPeers([peer], [peer], permissiveRouter());
|
|
619
|
+
let selectedPeerId = '';
|
|
620
|
+
let selectedBody = null;
|
|
621
|
+
proxy._node.sendRequest = async (selectedPeer, request) => {
|
|
622
|
+
selectedPeerId = selectedPeer.peerId;
|
|
623
|
+
selectedBody = parseJsonBody(request.body);
|
|
624
|
+
return {
|
|
625
|
+
requestId: request.requestId,
|
|
626
|
+
statusCode: 200,
|
|
627
|
+
headers: { 'content-type': 'application/json' },
|
|
628
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
632
|
+
body: { model: 'gpt-56-terra', messages: [] },
|
|
633
|
+
}));
|
|
634
|
+
assert.equal(res.statusCode, 200);
|
|
635
|
+
assert.equal(selectedPeerId, peer.peerId);
|
|
636
|
+
assert.equal(selectedBody?.['model'], 'gpt-5.6-terra');
|
|
637
|
+
});
|
|
638
|
+
test('model-only request routes when the request path has no detectable protocol', async () => {
|
|
639
|
+
const peer = makePeer('a', ['openai']);
|
|
640
|
+
peer.providerServiceApiProtocols = {
|
|
641
|
+
openai: { services: { 'embedding-model': ['openai-chat-completions'] } },
|
|
642
|
+
};
|
|
643
|
+
const proxy = makeBuyerProxyWithPeers([peer], [peer], permissiveRouter());
|
|
644
|
+
let selectedBody = null;
|
|
645
|
+
proxy._node.sendRequest = async (_peer, request) => {
|
|
646
|
+
selectedBody = parseJsonBody(request.body);
|
|
647
|
+
return {
|
|
648
|
+
requestId: request.requestId,
|
|
649
|
+
statusCode: 200,
|
|
650
|
+
headers: { 'content-type': 'application/json' },
|
|
651
|
+
body: Buffer.from('{}'),
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
655
|
+
path: '/v1/models/custom-operation',
|
|
656
|
+
body: { model: 'embedding-model', input: 'hello' },
|
|
657
|
+
}));
|
|
658
|
+
assert.equal(res.statusCode, 200);
|
|
659
|
+
assert.equal(selectedBody?.['model'], 'embedding-model');
|
|
660
|
+
});
|
|
661
|
+
test('model-only request uses the cheapest duplicate service advertised by one peer', async () => {
|
|
662
|
+
const peer = makePeer('a', ['openai']);
|
|
663
|
+
peer.providerPricing = {
|
|
664
|
+
openai: {
|
|
665
|
+
defaults: { inputUsdPerMillion: 0, outputUsdPerMillion: 0 },
|
|
666
|
+
services: {
|
|
667
|
+
'gpt-5.6-sol': { inputUsdPerMillion: 5, outputUsdPerMillion: 10 },
|
|
668
|
+
'gpt-56-sol': { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
peer.providerServiceApiProtocols = {
|
|
673
|
+
openai: {
|
|
674
|
+
services: {
|
|
675
|
+
'gpt-5.6-sol': ['openai-chat-completions'],
|
|
676
|
+
'gpt-56-sol': ['openai-chat-completions'],
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
const proxy = makeBuyerProxyWithPeers([peer], [peer], permissiveRouter());
|
|
681
|
+
let selectedBody = null;
|
|
682
|
+
proxy._node.sendRequest = async (_peer, request) => {
|
|
683
|
+
selectedBody = parseJsonBody(request.body);
|
|
684
|
+
return {
|
|
685
|
+
requestId: request.requestId,
|
|
686
|
+
statusCode: 200,
|
|
687
|
+
headers: { 'content-type': 'application/json' },
|
|
688
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
689
|
+
};
|
|
690
|
+
};
|
|
691
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
692
|
+
body: { model: 'gpt-5.6-sol', messages: [] },
|
|
693
|
+
}));
|
|
694
|
+
assert.equal(res.statusCode, 200);
|
|
695
|
+
assert.equal(selectedBody?.['model'], 'gpt-56-sol');
|
|
696
|
+
});
|
|
697
|
+
test('antseed alias with a model-only default route uses automatic peer selection', async () => {
|
|
698
|
+
const lower = makePeer('a', ['openai']);
|
|
699
|
+
lower.reputationScore = 40;
|
|
700
|
+
lower.providerServiceApiProtocols = {
|
|
701
|
+
openai: { services: { 'gpt-56-sol': ['openai-chat-completions'] } },
|
|
702
|
+
};
|
|
703
|
+
const higher = makePeer('b', ['openai']);
|
|
704
|
+
higher.reputationScore = 90;
|
|
705
|
+
higher.providerServiceApiProtocols = {
|
|
706
|
+
openai: { services: { 'openai-gpt-56-sol': ['openai-chat-completions'] } },
|
|
707
|
+
};
|
|
708
|
+
const proxy = makeBuyerProxyWithPeers([lower, higher], [lower, higher], permissiveRouter());
|
|
709
|
+
proxy._defaultRoutedModel = 'gpt-5.6-sol';
|
|
710
|
+
let selectedPeerId = '';
|
|
711
|
+
let selectedBody = null;
|
|
712
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
713
|
+
selectedPeerId = peer.peerId;
|
|
714
|
+
selectedBody = parseJsonBody(request.body);
|
|
715
|
+
return {
|
|
716
|
+
requestId: request.requestId,
|
|
717
|
+
statusCode: 200,
|
|
718
|
+
headers: { 'content-type': 'application/json' },
|
|
719
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
720
|
+
};
|
|
721
|
+
};
|
|
722
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'antseed', messages: [] } }));
|
|
723
|
+
assert.equal(res.statusCode, 200);
|
|
724
|
+
assert.equal(selectedPeerId, higher.peerId);
|
|
725
|
+
assert.equal(selectedBody?.['model'], 'openai-gpt-56-sol');
|
|
726
|
+
});
|
|
727
|
+
test('model-only routing skips higher-reputation peers rejected by buyer policy', async () => {
|
|
728
|
+
const allowed = makePeer('a', ['openai']);
|
|
729
|
+
allowed.reputationScore = 60;
|
|
730
|
+
allowed.providerServiceApiProtocols = {
|
|
731
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
732
|
+
};
|
|
733
|
+
const rejected = makePeer('b', ['openai']);
|
|
734
|
+
rejected.reputationScore = 95;
|
|
735
|
+
rejected.providerServiceApiProtocols = {
|
|
736
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
737
|
+
};
|
|
738
|
+
const router = {
|
|
739
|
+
allowsPeerForPolicy: (_request, peer) => peer.peerId === allowed.peerId,
|
|
740
|
+
onResult: () => { },
|
|
741
|
+
};
|
|
742
|
+
const proxy = makeBuyerProxyWithPeers([allowed, rejected], [allowed, rejected], router);
|
|
743
|
+
let selectedPeerId = '';
|
|
744
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
745
|
+
selectedPeerId = peer.peerId;
|
|
746
|
+
return {
|
|
747
|
+
requestId: request.requestId,
|
|
748
|
+
statusCode: 200,
|
|
749
|
+
headers: { 'content-type': 'application/json' },
|
|
750
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
751
|
+
};
|
|
752
|
+
};
|
|
753
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
|
|
754
|
+
assert.equal(res.statusCode, 200);
|
|
755
|
+
assert.equal(selectedPeerId, allowed.peerId);
|
|
756
|
+
});
|
|
757
|
+
test('/models order and model-only dispatch use the same Price + Trust ranking', async () => {
|
|
758
|
+
const cobaltRelay = makePeer('a', ['openai']);
|
|
759
|
+
cobaltRelay.reputationScore = 99;
|
|
760
|
+
cobaltRelay.providerPricing = {
|
|
761
|
+
openai: { defaults: { inputUsdPerMillion: 1.88, outputUsdPerMillion: 9.38 } },
|
|
762
|
+
};
|
|
763
|
+
cobaltRelay.providerServiceApiProtocols = {
|
|
764
|
+
openai: { services: { 'kimi-k3': ['openai-chat-completions'] } },
|
|
765
|
+
};
|
|
766
|
+
const emberRoute = makePeer('b', ['openai']);
|
|
767
|
+
emberRoute.reputationScore = 96;
|
|
768
|
+
emberRoute.providerPricing = {
|
|
769
|
+
openai: { defaults: { inputUsdPerMillion: 0.9, outputUsdPerMillion: 2.7 } },
|
|
770
|
+
};
|
|
771
|
+
emberRoute.providerServiceApiProtocols = {
|
|
772
|
+
openai: { services: { 'Kimi K3': ['openai-chat-completions'] } },
|
|
773
|
+
};
|
|
774
|
+
const proxy = makeBuyerProxyWithPeers([cobaltRelay, emberRoute], [cobaltRelay, emberRoute], permissiveRouter(), undefined, priceAndTrustPreferences);
|
|
775
|
+
let selectedPeerId = '';
|
|
776
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
777
|
+
selectedPeerId = peer.peerId;
|
|
778
|
+
return {
|
|
779
|
+
requestId: request.requestId,
|
|
780
|
+
statusCode: 200,
|
|
781
|
+
headers: { 'content-type': 'application/json' },
|
|
782
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
783
|
+
};
|
|
784
|
+
};
|
|
785
|
+
const modelsRes = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/kimi-k3' }));
|
|
786
|
+
const peerOrder = JSON.parse(modelsRes.body).peers
|
|
787
|
+
.map((peer) => peer.peerId);
|
|
788
|
+
const completionRes = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'kimi-k3', messages: [] } }));
|
|
789
|
+
assert.equal(modelsRes.statusCode, 200);
|
|
790
|
+
assert.deepEqual(peerOrder, [emberRoute.peerId, cobaltRelay.peerId]);
|
|
791
|
+
assert.equal(completionRes.statusCode, 200);
|
|
792
|
+
assert.equal(selectedPeerId, emberRoute.peerId);
|
|
793
|
+
});
|
|
794
|
+
test('Price + Trust routing falls back after the preferred cheaper peer fails', async () => {
|
|
795
|
+
const cobaltRelay = makePeer('a', ['openai']);
|
|
796
|
+
cobaltRelay.reputationScore = 99;
|
|
797
|
+
cobaltRelay.providerPricing = {
|
|
798
|
+
openai: { defaults: { inputUsdPerMillion: 1.88, outputUsdPerMillion: 9.38 } },
|
|
799
|
+
};
|
|
800
|
+
cobaltRelay.providerServiceApiProtocols = {
|
|
801
|
+
openai: { services: { 'kimi-k3': ['openai-chat-completions'] } },
|
|
802
|
+
};
|
|
803
|
+
const emberRoute = makePeer('b', ['openai']);
|
|
804
|
+
emberRoute.reputationScore = 96;
|
|
805
|
+
emberRoute.providerPricing = {
|
|
806
|
+
openai: { defaults: { inputUsdPerMillion: 0.9, outputUsdPerMillion: 2.7 } },
|
|
807
|
+
};
|
|
808
|
+
emberRoute.providerServiceApiProtocols = {
|
|
809
|
+
openai: { services: { 'Kimi K3': ['openai-chat-completions'] } },
|
|
810
|
+
};
|
|
811
|
+
const proxy = makeBuyerProxyWithPeers([cobaltRelay, emberRoute], [cobaltRelay, emberRoute], permissiveRouter(), undefined, priceAndTrustPreferences);
|
|
812
|
+
const attempts = [];
|
|
813
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
814
|
+
attempts.push(peer.peerId);
|
|
815
|
+
return {
|
|
816
|
+
requestId: request.requestId,
|
|
817
|
+
statusCode: peer.peerId === emberRoute.peerId ? 503 : 200,
|
|
818
|
+
headers: { 'content-type': 'application/json' },
|
|
819
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
820
|
+
};
|
|
821
|
+
};
|
|
822
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'kimi-k3', messages: [] } }));
|
|
823
|
+
assert.equal(res.statusCode, 200);
|
|
824
|
+
assert.deepEqual(attempts, [emberRoute.peerId, cobaltRelay.peerId]);
|
|
825
|
+
});
|
|
826
|
+
test('model-only routing falls back to the next reputable peer after a retryable failure', async () => {
|
|
827
|
+
const first = makePeer('a', ['openai']);
|
|
828
|
+
first.reputationScore = 95;
|
|
829
|
+
first.providerServiceApiProtocols = {
|
|
830
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
831
|
+
};
|
|
832
|
+
const second = makePeer('b', ['openai']);
|
|
833
|
+
second.reputationScore = 80;
|
|
834
|
+
second.providerServiceApiProtocols = {
|
|
835
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
836
|
+
};
|
|
837
|
+
const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
|
|
838
|
+
const attempts = [];
|
|
839
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
840
|
+
attempts.push(peer.peerId);
|
|
841
|
+
return {
|
|
842
|
+
requestId: request.requestId,
|
|
843
|
+
statusCode: peer.peerId === first.peerId ? 503 : 200,
|
|
844
|
+
headers: { 'content-type': 'application/json' },
|
|
845
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
846
|
+
};
|
|
847
|
+
};
|
|
848
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt5', messages: [] } }));
|
|
849
|
+
assert.equal(res.statusCode, 200);
|
|
850
|
+
assert.deepEqual(attempts, [first.peerId, second.peerId]);
|
|
851
|
+
assert.equal(JSON.parse(res.body).peerId, second.peerId);
|
|
852
|
+
});
|
|
853
|
+
test('model-only routing retries a rate-limited peer before falling back', async () => {
|
|
854
|
+
const first = makePeer('a', ['openai']);
|
|
855
|
+
first.reputationScore = 95;
|
|
856
|
+
first.providerServiceApiProtocols = {
|
|
857
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
858
|
+
};
|
|
859
|
+
const second = makePeer('b', ['openai']);
|
|
860
|
+
second.reputationScore = 80;
|
|
861
|
+
second.providerServiceApiProtocols = {
|
|
862
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
863
|
+
};
|
|
864
|
+
const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
|
|
865
|
+
const attempts = [];
|
|
866
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
867
|
+
attempts.push(peer.peerId);
|
|
868
|
+
const firstAttempts = attempts.filter((peerId) => peerId === first.peerId).length;
|
|
869
|
+
const statusCode = peer.peerId === first.peerId && firstAttempts < 3 ? 429 : 200;
|
|
870
|
+
return {
|
|
871
|
+
requestId: request.requestId,
|
|
872
|
+
statusCode,
|
|
873
|
+
headers: { 'content-type': 'application/json', 'retry-after': '0' },
|
|
874
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
875
|
+
};
|
|
876
|
+
};
|
|
877
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
|
|
878
|
+
assert.equal(res.statusCode, 200);
|
|
879
|
+
assert.deepEqual(attempts, [first.peerId, first.peerId, first.peerId]);
|
|
880
|
+
});
|
|
881
|
+
test('model-only routing falls back after three rate-limit responses from one peer', async () => {
|
|
882
|
+
const first = makePeer('a', ['openai']);
|
|
883
|
+
first.reputationScore = 95;
|
|
884
|
+
first.providerServiceApiProtocols = {
|
|
885
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
886
|
+
};
|
|
887
|
+
const second = makePeer('b', ['openai']);
|
|
888
|
+
second.reputationScore = 80;
|
|
889
|
+
second.providerServiceApiProtocols = {
|
|
890
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
891
|
+
};
|
|
892
|
+
const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
|
|
893
|
+
const attempts = [];
|
|
894
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
895
|
+
attempts.push(peer.peerId);
|
|
896
|
+
return {
|
|
897
|
+
requestId: request.requestId,
|
|
898
|
+
statusCode: peer.peerId === first.peerId ? 429 : 200,
|
|
899
|
+
headers: { 'content-type': 'application/json', 'retry-after': '0' },
|
|
900
|
+
body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
|
|
901
|
+
};
|
|
902
|
+
};
|
|
903
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
|
|
904
|
+
assert.equal(res.statusCode, 200);
|
|
905
|
+
assert.deepEqual(attempts, [first.peerId, first.peerId, first.peerId, second.peerId]);
|
|
906
|
+
});
|
|
907
|
+
test('model-only routing skips a cooling-down peer when another offer is ready', async () => {
|
|
908
|
+
const clock = makeTestClock();
|
|
909
|
+
const cooling = makePeer('a', ['openai']);
|
|
910
|
+
cooling.reputationScore = 95;
|
|
911
|
+
cooling.providerServiceApiProtocols = {
|
|
912
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
913
|
+
};
|
|
914
|
+
const ready = makePeer('b', ['openai']);
|
|
915
|
+
ready.reputationScore = 70;
|
|
916
|
+
ready.providerServiceApiProtocols = {
|
|
917
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
918
|
+
};
|
|
919
|
+
const proxy = makeBuyerProxyWithPeers([cooling, ready], [cooling, ready], permissiveRouter(), clock.now);
|
|
920
|
+
proxy._peerHealth.set(cooling.peerId, {
|
|
921
|
+
failureStreak: 3,
|
|
922
|
+
windowStartedAt: clock.now(),
|
|
923
|
+
episodeStartedAt: clock.now(),
|
|
924
|
+
cooldownUntil: clock.now() + 60_000,
|
|
925
|
+
lastFailureAt: clock.now(),
|
|
926
|
+
lastSuccessAt: 0,
|
|
927
|
+
lastReason: 'seller-5xx',
|
|
928
|
+
});
|
|
929
|
+
let selectedPeerId = '';
|
|
930
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
931
|
+
selectedPeerId = peer.peerId;
|
|
932
|
+
return {
|
|
933
|
+
requestId: request.requestId,
|
|
934
|
+
statusCode: 200,
|
|
935
|
+
headers: { 'content-type': 'application/json' },
|
|
936
|
+
body: Buffer.from('{}'),
|
|
937
|
+
};
|
|
938
|
+
};
|
|
939
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
|
|
940
|
+
assert.equal(res.statusCode, 200);
|
|
941
|
+
assert.equal(selectedPeerId, ready.peerId);
|
|
942
|
+
});
|
|
943
|
+
test('model-only routing does not fail over after a buyer-attributed failure', async () => {
|
|
944
|
+
const first = makePeer('a', ['openai']);
|
|
945
|
+
first.reputationScore = 95;
|
|
946
|
+
first.providerServiceApiProtocols = {
|
|
947
|
+
openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
|
|
948
|
+
};
|
|
949
|
+
const second = makePeer('b', ['openai']);
|
|
950
|
+
second.reputationScore = 80;
|
|
951
|
+
second.providerServiceApiProtocols = {
|
|
952
|
+
openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
|
|
953
|
+
};
|
|
954
|
+
const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
|
|
955
|
+
const attempts = [];
|
|
956
|
+
proxy._node.sendRequest = async (peer) => {
|
|
957
|
+
attempts.push(peer.peerId);
|
|
958
|
+
throw buyerFault('Buyer has insufficient deposits', 'buyer-deposits-insufficient');
|
|
959
|
+
};
|
|
960
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
|
|
961
|
+
assert.equal(res.statusCode, 503);
|
|
962
|
+
assert.deepEqual(attempts, [first.peerId]);
|
|
963
|
+
assert.equal(JSON.parse(res.body).error.code, ANTSEED_BUYER_FAULT_ERROR_CODE);
|
|
964
|
+
});
|
|
340
965
|
test('pinned proxy request reports when the pinned peer is not discoverable', async () => {
|
|
341
966
|
const pinnedPeerId = 'a'.repeat(40);
|
|
342
967
|
const otherPeer = makePeer('b', ['openai']);
|
|
@@ -351,6 +976,52 @@ test('pinned proxy request reports when the pinned peer is not discoverable', as
|
|
|
351
976
|
assert.match(res.body, /is not reachable right now/);
|
|
352
977
|
assert.match(res.body, /It may be offline, not announcing, or temporarily unreachable/);
|
|
353
978
|
});
|
|
979
|
+
test('pinned proxy request rewrites a canonical alias to the advertised service id', async () => {
|
|
980
|
+
const pinnedPeer = makePeer('a', ['openai']);
|
|
981
|
+
pinnedPeer.providerServiceApiProtocols = {
|
|
982
|
+
openai: { services: { 'gpt-5.6-sol': ['openai-chat-completions'] } },
|
|
983
|
+
};
|
|
984
|
+
const proxy = makeBuyerProxyWithPeers([pinnedPeer], [pinnedPeer], permissiveRouter());
|
|
985
|
+
let selectedBody = null;
|
|
986
|
+
proxy._node.sendRequest = async (_peer, request) => {
|
|
987
|
+
selectedBody = parseJsonBody(request.body);
|
|
988
|
+
return {
|
|
989
|
+
requestId: request.requestId,
|
|
990
|
+
statusCode: 200,
|
|
991
|
+
headers: { 'content-type': 'application/json' },
|
|
992
|
+
body: Buffer.from('{}'),
|
|
993
|
+
};
|
|
994
|
+
};
|
|
995
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
996
|
+
headers: { 'x-antseed-pin-peer': pinnedPeer.peerId },
|
|
997
|
+
body: { model: 'gpt-56-sol', messages: [] },
|
|
998
|
+
}));
|
|
999
|
+
assert.equal(res.statusCode, 200);
|
|
1000
|
+
assert.equal(selectedBody?.['model'], 'gpt-5.6-sol');
|
|
1001
|
+
});
|
|
1002
|
+
test('pinned proxy request dispatches when stale metadata misses the requested model', async () => {
|
|
1003
|
+
const pinnedPeer = makePeer('a', ['openai']);
|
|
1004
|
+
pinnedPeer.providerServiceApiProtocols = {
|
|
1005
|
+
openai: { services: { 'known-model': ['openai-chat-completions'] } },
|
|
1006
|
+
};
|
|
1007
|
+
const proxy = makeBuyerProxyWithPeers([pinnedPeer], [pinnedPeer], permissiveRouter());
|
|
1008
|
+
let selectedBody = null;
|
|
1009
|
+
proxy._node.sendRequest = async (_peer, request) => {
|
|
1010
|
+
selectedBody = parseJsonBody(request.body);
|
|
1011
|
+
return {
|
|
1012
|
+
requestId: request.requestId,
|
|
1013
|
+
statusCode: 200,
|
|
1014
|
+
headers: { 'content-type': 'application/json' },
|
|
1015
|
+
body: Buffer.from('{}'),
|
|
1016
|
+
};
|
|
1017
|
+
};
|
|
1018
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
1019
|
+
headers: { 'x-antseed-pin-peer': pinnedPeer.peerId },
|
|
1020
|
+
body: { model: 'new-helper-model', messages: [] },
|
|
1021
|
+
}));
|
|
1022
|
+
assert.equal(res.statusCode, 200);
|
|
1023
|
+
assert.equal(selectedBody?.['model'], 'new-helper-model');
|
|
1024
|
+
});
|
|
354
1025
|
test('pinned proxy request reports explicit provider mismatch separately', async () => {
|
|
355
1026
|
const pinnedPeer = makePeer('a', ['local-llm']);
|
|
356
1027
|
const proxy = makeBuyerProxyWithPeers([pinnedPeer]);
|
|
@@ -661,29 +1332,126 @@ test('a non-standard seller 5xx proves reachability and restarts the failure str
|
|
|
661
1332
|
assert.equal(healthOf(proxy, peer)?.cooldownUntil, 0);
|
|
662
1333
|
assert.equal(healthOf(proxy, peer)?.failureStreak, 1);
|
|
663
1334
|
});
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1335
|
+
function makeNetworkModelPeers() {
|
|
1336
|
+
const textPeer = makePeer('a', ['openai']);
|
|
1337
|
+
textPeer.providerPricing = {
|
|
1338
|
+
openai: {
|
|
1339
|
+
defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
|
|
1340
|
+
services: { 'qwen3-coder': { inputUsdPerMillion: 3, outputUsdPerMillion: 4 } },
|
|
1341
|
+
},
|
|
1342
|
+
};
|
|
1343
|
+
textPeer.providerServiceApiProtocols = {
|
|
1344
|
+
openai: { services: { 'qwen3-coder': ['openai-responses'] } },
|
|
1345
|
+
};
|
|
1346
|
+
textPeer.providerServiceCapabilities = {
|
|
1347
|
+
openai: {
|
|
1348
|
+
services: {
|
|
1349
|
+
'qwen3-coder': {
|
|
1350
|
+
contextWindow: 128_000,
|
|
1351
|
+
maxOutputTokens: 32_000,
|
|
1352
|
+
inputs: ['text', 'image'],
|
|
1353
|
+
outputs: ['text'],
|
|
1354
|
+
reasoning: true,
|
|
1355
|
+
toolUse: true,
|
|
1356
|
+
structuredOutput: true,
|
|
1357
|
+
supportedParameters: ['temperature', 'tools'],
|
|
1358
|
+
},
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
};
|
|
1362
|
+
textPeer.providerServiceCategories = {
|
|
1363
|
+
openai: { services: { 'qwen3-coder': ['chat', 'reasoning'] } },
|
|
1364
|
+
};
|
|
1365
|
+
const imagePeer = makePeer('b', ['openai']);
|
|
1366
|
+
imagePeer.providerServiceApiProtocols = {
|
|
1367
|
+
openai: { services: { 'flux-1-schnell': ['openai-images'] } },
|
|
1368
|
+
};
|
|
1369
|
+
const aliasPeer = makePeer('c', ['anthropic']);
|
|
1370
|
+
aliasPeer.providerServiceApiProtocols = {
|
|
1371
|
+
anthropic: { services: { 'Claude Opus 5': ['anthropic-messages'] } },
|
|
1372
|
+
};
|
|
1373
|
+
const secondAliasPeer = makePeer('d', ['anthropic']);
|
|
1374
|
+
secondAliasPeer.providerServiceApiProtocols = {
|
|
1375
|
+
anthropic: { services: { 'opus-5': ['anthropic-messages'] } },
|
|
1376
|
+
};
|
|
1377
|
+
return [textPeer, imagePeer, aliasPeer, secondAliasPeer];
|
|
1378
|
+
}
|
|
1379
|
+
test('GET /v1/models is answered locally with the network-wide model list', async () => {
|
|
1380
|
+
const peers = makeNetworkModelPeers();
|
|
1381
|
+
const proxy = makeBuyerProxyWithPeers(peers);
|
|
1382
|
+
let forwarded = 0;
|
|
1383
|
+
proxy._node.sendRequest = async () => {
|
|
1384
|
+
forwarded += 1;
|
|
1385
|
+
throw new Error('must not reach a peer');
|
|
1386
|
+
};
|
|
1387
|
+
const res = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models' }));
|
|
1388
|
+
assert.equal(res.statusCode, 200);
|
|
1389
|
+
// The port-reuse probe in `buyer start` identifies the proxy by this header.
|
|
1390
|
+
assert.ok(res.headers['x-antseed-request-id']);
|
|
1391
|
+
const body = JSON.parse(res.body);
|
|
1392
|
+
assert.equal(body.object, 'list');
|
|
1393
|
+
assert.deepEqual(body.data.map((model) => model.id), ['Claude Opus 5', 'flux-1-schnell', 'qwen3-coder']);
|
|
1394
|
+
const opus = body.data[0];
|
|
1395
|
+
assert.equal(opus.name, 'Claude Opus 5');
|
|
1396
|
+
assert.deepEqual(opus.peers.map((peer) => peer.serviceId), ['Claude Opus 5', 'opus-5']);
|
|
1397
|
+
const flux = body.data[1];
|
|
1398
|
+
assert.equal(flux.type, 'image');
|
|
1399
|
+
assert.equal(flux.peers[0]?.peerId, peers[1]?.peerId);
|
|
1400
|
+
const qwen = body.data[2];
|
|
1401
|
+
assert.equal(qwen.name, 'Qwen3 Coder');
|
|
1402
|
+
assert.equal(qwen.type, 'text');
|
|
1403
|
+
assert.deepEqual(qwen.supported_protocols, ['openai-responses']);
|
|
1404
|
+
assert.equal(qwen.context_length, 128_000);
|
|
1405
|
+
assert.equal(qwen.max_output_tokens, 32_000);
|
|
1406
|
+
assert.deepEqual(qwen.architecture, {
|
|
1407
|
+
input_modalities: ['image', 'text'],
|
|
1408
|
+
output_modalities: ['text'],
|
|
679
1409
|
});
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
})
|
|
685
|
-
assert.
|
|
686
|
-
assert.equal(
|
|
1410
|
+
assert.deepEqual(qwen.capabilities, {
|
|
1411
|
+
reasoning: true,
|
|
1412
|
+
tool_use: true,
|
|
1413
|
+
structured_output: true,
|
|
1414
|
+
});
|
|
1415
|
+
assert.deepEqual(qwen.supported_parameters, ['temperature', 'tools']);
|
|
1416
|
+
assert.equal(qwen.capability_coverage.context_length, 1);
|
|
1417
|
+
assert.equal(qwen.peers[0]?.protocol, 'openai-responses');
|
|
1418
|
+
assert.deepEqual(qwen.peers[0]?.categories, ['chat', 'reasoning']);
|
|
1419
|
+
assert.equal(qwen.peers[0]?.capabilities?.contextWindow, 128_000);
|
|
1420
|
+
assert.equal(qwen.peers[0]?.inputUsdPerMillion, 3);
|
|
1421
|
+
assert.equal(forwarded, 0);
|
|
1422
|
+
});
|
|
1423
|
+
test('GET /v1/models?type= filters by model type and rejects unknown types', async () => {
|
|
1424
|
+
const proxy = makeBuyerProxyWithPeers(makeNetworkModelPeers());
|
|
1425
|
+
const images = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models?type=images' }));
|
|
1426
|
+
assert.equal(images.statusCode, 200);
|
|
1427
|
+
assert.deepEqual(JSON.parse(images.body).data.map((model) => model.id), ['flux-1-schnell']);
|
|
1428
|
+
const text = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models?type=text' }));
|
|
1429
|
+
assert.deepEqual(JSON.parse(text.body).data.map((model) => model.id), ['Claude Opus 5', 'qwen3-coder']);
|
|
1430
|
+
const bad = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models?type=audio' }));
|
|
1431
|
+
assert.equal(bad.statusCode, 400);
|
|
1432
|
+
assert.equal(JSON.parse(bad.body).error.param, 'type');
|
|
1433
|
+
});
|
|
1434
|
+
test('GET /v1/models/:id looks up a single model across the network', async () => {
|
|
1435
|
+
const proxy = makeBuyerProxyWithPeers(makeNetworkModelPeers());
|
|
1436
|
+
const hit = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/QWEN3-coder' }));
|
|
1437
|
+
assert.equal(hit.statusCode, 200);
|
|
1438
|
+
const hitBody = JSON.parse(hit.body);
|
|
1439
|
+
assert.equal(hitBody.id, 'qwen3-coder');
|
|
1440
|
+
assert.equal(hitBody.context_length, 128_000);
|
|
1441
|
+
assert.equal(hitBody.peers[0]?.capabilities?.toolUse, true);
|
|
1442
|
+
const hitWithIgnoredType = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/QWEN3-coder?type=audio' }));
|
|
1443
|
+
assert.equal(hitWithIgnoredType.statusCode, 200);
|
|
1444
|
+
assert.equal(JSON.parse(hitWithIgnoredType.body).id, 'qwen3-coder');
|
|
1445
|
+
const aliasHit = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/opus-5' }));
|
|
1446
|
+
assert.equal(aliasHit.statusCode, 200);
|
|
1447
|
+
assert.equal(JSON.parse(aliasHit.body).id, 'Claude Opus 5');
|
|
1448
|
+
const miss = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/does-not-exist' }));
|
|
1449
|
+
assert.equal(miss.statusCode, 404);
|
|
1450
|
+
assert.equal(JSON.parse(miss.body).error.code, 'model_not_found');
|
|
1451
|
+
const malformed = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/gpt%zz' }));
|
|
1452
|
+
assert.equal(malformed.statusCode, 404);
|
|
1453
|
+
assert.equal(malformed.headers['content-type'], 'application/json');
|
|
1454
|
+
assert.equal(JSON.parse(malformed.body).error.code, 'model_not_found');
|
|
687
1455
|
});
|
|
688
1456
|
test('a buyer-side outage rolls back the cooldowns it caused', async () => {
|
|
689
1457
|
const clock = makeTestClock();
|
|
@@ -756,34 +1524,6 @@ test('POST /_antseed/peer-health/clear rejects a malformed peer id', async () =>
|
|
|
756
1524
|
}));
|
|
757
1525
|
assert.equal(res.statusCode, 400);
|
|
758
1526
|
});
|
|
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
1527
|
test('non-stream transformed responses requests force upstream stream without streaming to client', async () => {
|
|
788
1528
|
const peer = makePeer('a', ['openai-responses']);
|
|
789
1529
|
peer.providerServiceApiProtocols = {
|
|
@@ -1108,6 +1848,27 @@ test('parsePersistedPeers preserves provider metadata so routing filters still w
|
|
|
1108
1848
|
assert.equal(result.candidatePeers[0]?.peerId, validPeerId);
|
|
1109
1849
|
assert.equal(result.routePlanByPeerId.get(validPeerId)?.provider, 'claude-oauth');
|
|
1110
1850
|
});
|
|
1851
|
+
test('parsePersistedPeers re-derives on-chain reputation from persisted stats', () => {
|
|
1852
|
+
const persisted = {
|
|
1853
|
+
discoveredPeers: [
|
|
1854
|
+
{
|
|
1855
|
+
peerId: validPeerId,
|
|
1856
|
+
providers: ['claude-oauth'],
|
|
1857
|
+
lastSeen: NOW - 5_000,
|
|
1858
|
+
onChainStakeUsdcMicros: 2_000_000,
|
|
1859
|
+
onChainChannelCount: 20,
|
|
1860
|
+
onChainGhostCount: 0,
|
|
1861
|
+
onChainTotalVolumeUsdcMicros: 100_000_000,
|
|
1862
|
+
onChainLastSettledAtSec: Math.floor((NOW - 60_000) / 1000),
|
|
1863
|
+
onChainStakedAtSec: Math.floor((NOW - 40 * 86_400_000) / 1000),
|
|
1864
|
+
},
|
|
1865
|
+
],
|
|
1866
|
+
};
|
|
1867
|
+
const [peer] = parsePersistedPeers(persisted, NOW);
|
|
1868
|
+
assert.ok(peer);
|
|
1869
|
+
assert.equal(peer.onChainReputationScore, computeOnChainReputationScore(peer, NOW));
|
|
1870
|
+
assert.ok((peer.onChainReputationScore ?? 0) > 0);
|
|
1871
|
+
});
|
|
1111
1872
|
test('parsePersistedPeers restores sellerContract into peer.metadata', () => {
|
|
1112
1873
|
// Regression: dropping sellerContract through the persistence layer caused
|
|
1113
1874
|
// SellerAddressResolver to fall back to peerIdToAddress, so the buyer signed
|
|
@@ -1333,7 +2094,12 @@ test('route control endpoint sets, persists, and returns the default routed mode
|
|
|
1333
2094
|
assert.deepEqual(JSON.parse(get.body), { ok: true, model: `${validPeerId}@gpt-4o` });
|
|
1334
2095
|
const persisted = JSON.parse(await readFile(join(dir, 'buyer.state.json'), 'utf-8'));
|
|
1335
2096
|
assert.equal(persisted['defaultRoutedModel'], `${validPeerId}@gpt-4o`);
|
|
1336
|
-
const
|
|
2097
|
+
const automatic = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/route', body: { model: 'gpt-4o' } }));
|
|
2098
|
+
assert.equal(automatic.statusCode, 200);
|
|
2099
|
+
assert.deepEqual(JSON.parse(automatic.body), { ok: true, model: 'gpt-4o' });
|
|
2100
|
+
const automaticPersisted = JSON.parse(await readFile(join(dir, 'buyer.state.json'), 'utf-8'));
|
|
2101
|
+
assert.equal(automaticPersisted['defaultRoutedModel'], 'gpt-4o');
|
|
2102
|
+
const invalid = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/route', body: { model: 'not-a-peer@gpt-4o' } }));
|
|
1337
2103
|
assert.equal(invalid.statusCode, 400);
|
|
1338
2104
|
const cleared = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/route', body: { model: '' } }));
|
|
1339
2105
|
assert.deepEqual(JSON.parse(cleared.body), { ok: true, model: null });
|
|
@@ -1399,7 +2165,7 @@ test('per-chat pin overrides the default routed model for the antseed alias', as
|
|
|
1399
2165
|
proxy._defaultRoutedModel = defaultRoute;
|
|
1400
2166
|
const store = proxy._conversations;
|
|
1401
2167
|
store.touch({ tool: 'codex-exec', sessionKey: 'sess-1' });
|
|
1402
|
-
store.setPinnedModel('codex-exec:sess-1', pinnedRoute);
|
|
2168
|
+
store.setPinnedModel('codex-exec:sess-1', pinnedRoute, 'user');
|
|
1403
2169
|
await invokeProxy(proxy, makeProxyRequest({
|
|
1404
2170
|
path: '/v1/responses',
|
|
1405
2171
|
headers: { originator: 'codex_exec', 'session-id': 'sess-1' },
|
|
@@ -1446,7 +2212,7 @@ test('chat pin overrides a system-proxy-routed model on intercepted requests', a
|
|
|
1446
2212
|
}));
|
|
1447
2213
|
assert.equal(store.get('claude-code:cc-1')?.pinnedModel, proxyRoute);
|
|
1448
2214
|
// The user re-pins the chat from the desktop (float / chats view).
|
|
1449
|
-
store.setPinnedModel('claude-code:cc-1', pinnedRoute);
|
|
2215
|
+
store.setPinnedModel('claude-code:cc-1', pinnedRoute, 'user');
|
|
1450
2216
|
// Later requests still arrive with the proxy-assigned model; the pin wins.
|
|
1451
2217
|
await invokeProxy(proxy, makeProxyRequest({
|
|
1452
2218
|
path: '/v1/messages',
|
|
@@ -1542,9 +2308,15 @@ test('conversation control endpoints list, rename, pin, reject bad pins, delete'
|
|
|
1542
2308
|
}));
|
|
1543
2309
|
assert.equal(rename.statusCode, 200);
|
|
1544
2310
|
assert.equal(store.get('opencode:ses_x')?.label, 'Login refactor');
|
|
2311
|
+
const automaticPin = await invokeProxy(proxy, makeProxyRequest({
|
|
2312
|
+
path: '/_antseed/conversations/update',
|
|
2313
|
+
body: { id: 'opencode:ses_x', pinnedModel: 'gpt-5.6-sol', peerSource: 'auto' },
|
|
2314
|
+
}));
|
|
2315
|
+
assert.equal(automaticPin.statusCode, 200);
|
|
2316
|
+
assert.equal(store.getPinnedModel('opencode', 'ses_x'), 'gpt-5.6-sol');
|
|
1545
2317
|
const badPin = await invokeProxy(proxy, makeProxyRequest({
|
|
1546
2318
|
path: '/_antseed/conversations/update',
|
|
1547
|
-
body: { id: 'opencode:ses_x', pinnedModel: 'not-a-
|
|
2319
|
+
body: { id: 'opencode:ses_x', pinnedModel: 'not-a-peer@gpt-5.6-sol' },
|
|
1548
2320
|
}));
|
|
1549
2321
|
assert.equal(badPin.statusCode, 400);
|
|
1550
2322
|
const goodPin = await invokeProxy(proxy, makeProxyRequest({
|