@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.
Files changed (94) hide show
  1. package/README.md +51 -17
  2. package/dist/cli/commands/buyer/activity.d.ts +3 -0
  3. package/dist/cli/commands/buyer/activity.d.ts.map +1 -0
  4. package/dist/cli/commands/buyer/activity.js +555 -0
  5. package/dist/cli/commands/buyer/activity.js.map +1 -0
  6. package/dist/cli/commands/buyer/daemon.d.ts +13 -0
  7. package/dist/cli/commands/buyer/daemon.d.ts.map +1 -0
  8. package/dist/cli/commands/buyer/daemon.js +39 -0
  9. package/dist/cli/commands/buyer/daemon.js.map +1 -0
  10. package/dist/cli/commands/buyer/deposit.d.ts +8 -0
  11. package/dist/cli/commands/buyer/deposit.d.ts.map +1 -1
  12. package/dist/cli/commands/buyer/deposit.js +338 -25
  13. package/dist/cli/commands/buyer/deposit.js.map +1 -1
  14. package/dist/cli/commands/buyer/index.d.ts.map +1 -1
  15. package/dist/cli/commands/buyer/index.js +2 -0
  16. package/dist/cli/commands/buyer/index.js.map +1 -1
  17. package/dist/cli/commands/buyer/start.d.ts +1 -0
  18. package/dist/cli/commands/buyer/start.d.ts.map +1 -1
  19. package/dist/cli/commands/buyer/start.js +53 -10
  20. package/dist/cli/commands/buyer/start.js.map +1 -1
  21. package/dist/cli/commands/buyer/start.test.js +30 -1
  22. package/dist/cli/commands/buyer/start.test.js.map +1 -1
  23. package/dist/cli/commands/buyer/sweep.d.ts.map +1 -1
  24. package/dist/cli/commands/buyer/sweep.js +17 -17
  25. package/dist/cli/commands/buyer/sweep.js.map +1 -1
  26. package/dist/cli/commands/network/browse.d.ts.map +1 -1
  27. package/dist/cli/commands/network/browse.js +65 -114
  28. package/dist/cli/commands/network/browse.js.map +1 -1
  29. package/dist/cli/commands/payments.d.ts.map +1 -1
  30. package/dist/cli/commands/payments.js +29 -34
  31. package/dist/cli/commands/payments.js.map +1 -1
  32. package/dist/cli/commands/seller/start.d.ts.map +1 -1
  33. package/dist/cli/commands/seller/start.js +4 -1
  34. package/dist/cli/commands/seller/start.js.map +1 -1
  35. package/dist/cli/commands/seller/start.test.js +15 -0
  36. package/dist/cli/commands/seller/start.test.js.map +1 -1
  37. package/dist/cli/index.js +2 -0
  38. package/dist/cli/index.js.map +1 -1
  39. package/dist/config/defaults.d.ts.map +1 -1
  40. package/dist/config/defaults.js +7 -0
  41. package/dist/config/defaults.js.map +1 -1
  42. package/dist/config/loader.d.ts.map +1 -1
  43. package/dist/config/loader.js +41 -0
  44. package/dist/config/loader.js.map +1 -1
  45. package/dist/config/loader.test.js +52 -0
  46. package/dist/config/loader.test.js.map +1 -1
  47. package/dist/config/types.d.ts +10 -0
  48. package/dist/config/types.d.ts.map +1 -1
  49. package/dist/config/validation.d.ts.map +1 -1
  50. package/dist/config/validation.js +17 -0
  51. package/dist/config/validation.js.map +1 -1
  52. package/dist/generated/baked-defaults.d.ts +15 -0
  53. package/dist/generated/baked-defaults.d.ts.map +1 -0
  54. package/dist/generated/baked-defaults.js +15 -0
  55. package/dist/generated/baked-defaults.js.map +1 -0
  56. package/dist/plugins/manager.d.ts +1 -0
  57. package/dist/plugins/manager.d.ts.map +1 -1
  58. package/dist/plugins/manager.js +15 -4
  59. package/dist/plugins/manager.js.map +1 -1
  60. package/dist/plugins/manager.test.d.ts +2 -0
  61. package/dist/plugins/manager.test.d.ts.map +1 -0
  62. package/dist/plugins/manager.test.js +15 -0
  63. package/dist/plugins/manager.test.js.map +1 -0
  64. package/dist/proxy/buyer-proxy.d.ts +31 -2
  65. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  66. package/dist/proxy/buyer-proxy.js +481 -59
  67. package/dist/proxy/buyer-proxy.js.map +1 -1
  68. package/dist/proxy/buyer-proxy.test.js +894 -56
  69. package/dist/proxy/buyer-proxy.test.js.map +1 -1
  70. package/dist/proxy/conversation-store.d.ts +6 -6
  71. package/dist/proxy/conversation-store.d.ts.map +1 -1
  72. package/dist/proxy/conversation-store.js +16 -0
  73. package/dist/proxy/conversation-store.js.map +1 -1
  74. package/dist/proxy/deposit-watcher.d.ts +125 -0
  75. package/dist/proxy/deposit-watcher.d.ts.map +1 -0
  76. package/dist/proxy/deposit-watcher.js +306 -0
  77. package/dist/proxy/deposit-watcher.js.map +1 -0
  78. package/dist/proxy/deposit-watcher.test.d.ts +2 -0
  79. package/dist/proxy/deposit-watcher.test.d.ts.map +1 -0
  80. package/dist/proxy/deposit-watcher.test.js +173 -0
  81. package/dist/proxy/deposit-watcher.test.js.map +1 -0
  82. package/dist/proxy/network-models.d.ts +76 -0
  83. package/dist/proxy/network-models.d.ts.map +1 -0
  84. package/dist/proxy/network-models.js +229 -0
  85. package/dist/proxy/network-models.js.map +1 -0
  86. package/dist/proxy/network-models.test.d.ts +2 -0
  87. package/dist/proxy/network-models.test.d.ts.map +1 -0
  88. package/dist/proxy/network-models.test.js +734 -0
  89. package/dist/proxy/network-models.test.js.map +1 -0
  90. package/dist/proxy/routing.d.ts +3 -1
  91. package/dist/proxy/routing.d.ts.map +1 -1
  92. package/dist/proxy/routing.js +69 -16
  93. package/dist/proxy/routing.js.map +1 -1
  94. package/package.json +6 -5
@@ -0,0 +1,734 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { buildNetworkModels, normalizedModelReputationScore, parseModelTypeFilter } from './network-models.js';
4
+ const NOW_MS = 1_700_000_000_000;
5
+ function makePeer(overrides) {
6
+ return {
7
+ lastSeen: NOW_MS,
8
+ providers: [],
9
+ ...overrides,
10
+ peerId: overrides.peerId,
11
+ };
12
+ }
13
+ const textSeller = makePeer({
14
+ peerId: 'a'.repeat(40),
15
+ displayName: 'Text Seller',
16
+ onChainTrustScore: 40,
17
+ onChainReputationScore: 35,
18
+ providerPricing: {
19
+ openai: {
20
+ defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
21
+ services: {
22
+ 'qwen3-coder': { inputUsdPerMillion: 3, outputUsdPerMillion: 9, cachedInputUsdPerMillion: 0.3 },
23
+ },
24
+ },
25
+ },
26
+ providerServiceApiProtocols: {
27
+ openai: { services: { 'qwen3-coder': ['openai-chat-completions'] } },
28
+ },
29
+ });
30
+ const imageSeller = makePeer({
31
+ peerId: 'b'.repeat(40),
32
+ onChainTrustScore: 80,
33
+ onChainReputationScore: 75,
34
+ providerPricing: {
35
+ openai: { defaults: { inputUsdPerMillion: 0, outputUsdPerMillion: 0 } },
36
+ },
37
+ providerServiceApiProtocols: {
38
+ openai: { services: { 'flux-1-schnell': ['openai-images'] } },
39
+ },
40
+ providerServiceUnitBillingModels: {
41
+ openai: {
42
+ services: {
43
+ 'flux-1-schnell': {
44
+ 'openai-images': {
45
+ version: 1,
46
+ components: [
47
+ { unit: 'output_images', priceUsd: 0.01, match: { size: '512x512' } },
48
+ { unit: 'output_images', priceUsd: 0.05, match: { size: '1024x1024' } },
49
+ ],
50
+ },
51
+ },
52
+ },
53
+ },
54
+ },
55
+ });
56
+ // Announces the same text model as textSeller (different id casing) plus an
57
+ // image model identified only via capability outputs — no protocol announced.
58
+ const mixedSeller = makePeer({
59
+ peerId: 'c'.repeat(40),
60
+ onChainTrustScore: 90,
61
+ onChainReputationScore: 85,
62
+ providerPricing: {
63
+ 'local-llm': { defaults: { inputUsdPerMillion: 0.5, outputUsdPerMillion: 0.7 } },
64
+ },
65
+ providerServiceCapabilities: {
66
+ 'local-llm': {
67
+ services: {
68
+ 'QWEN3-Coder': { outputs: ['text'] },
69
+ 'sdxl-turbo': { outputs: ['image'] },
70
+ },
71
+ },
72
+ },
73
+ });
74
+ test('parseModelTypeFilter maps query values', () => {
75
+ assert.equal(parseModelTypeFilter(null), 'all');
76
+ assert.equal(parseModelTypeFilter(''), 'all');
77
+ assert.equal(parseModelTypeFilter('images'), 'image');
78
+ assert.equal(parseModelTypeFilter('Image'), 'image');
79
+ assert.equal(parseModelTypeFilter('text'), 'text');
80
+ assert.equal(parseModelTypeFilter('audio'), 'invalid');
81
+ });
82
+ test('aggregates one entry per model id across peers, case-insensitively', () => {
83
+ const models = buildNetworkModels([textSeller, imageSeller, mixedSeller], NOW_MS);
84
+ assert.deepEqual(models.map((model) => model.id), ['flux-1-schnell', 'qwen3-coder', 'sdxl-turbo']);
85
+ const qwen = models.find((model) => model.id === 'qwen3-coder');
86
+ assert.ok(qwen);
87
+ assert.equal(qwen.type, 'text');
88
+ assert.equal(qwen.object, 'model');
89
+ assert.equal(qwen.owned_by, 'antseed');
90
+ assert.equal(qwen.created, Math.floor(NOW_MS / 1000));
91
+ assert.deepEqual(qwen.aliases, ['qwen3-coder', 'qwen3coder']);
92
+ // The missing cached-price penalty reduces reputation without forcing the
93
+ // stronger peer behind a substantially weaker complete offer.
94
+ assert.deepEqual(qwen.peers.map((peer) => peer.peerId), [mixedSeller.peerId, textSeller.peerId]);
95
+ assert.deepEqual(qwen.peers.map((peer) => peer.reputationScore), [90, 40]);
96
+ assert.deepEqual(qwen.peers.map((peer) => peer.effectiveReputationScore), [42.5, 35]);
97
+ assert.deepEqual(qwen.peers.map((peer) => peer.onChainTrustScore), [90, 40]);
98
+ assert.deepEqual(qwen.peers.map((peer) => peer.onChainReputationScore), [85, 35]);
99
+ assert.deepEqual(qwen.peers.map((peer) => peer.serviceId), ['QWEN3-Coder', 'qwen3-coder']);
100
+ });
101
+ test('sorts by normalized on-chain reputation like desktop VPR', () => {
102
+ const reputationOnlySeller = makePeer({
103
+ peerId: 'f'.repeat(40),
104
+ onChainReputationScore: 95,
105
+ providerServiceApiProtocols: {
106
+ openai: { services: { 'qwen3-coder': ['openai-chat-completions'] } },
107
+ },
108
+ });
109
+ const models = buildNetworkModels([mixedSeller, reputationOnlySeller], NOW_MS);
110
+ const qwen = models.find((model) => model.id === 'QWEN3-Coder');
111
+ assert.ok(qwen);
112
+ assert.deepEqual(qwen.peers.map((peer) => peer.peerId), [reputationOnlySeller.peerId, mixedSeller.peerId]);
113
+ assert.deepEqual(qwen.peers.map((peer) => peer.reputationScore), [95, 90]);
114
+ assert.deepEqual(qwen.peers.map((peer) => peer.onChainTrustScore), [null, 90]);
115
+ });
116
+ test('sorts /models peers with the configured Price + Trust preferences', () => {
117
+ const cobaltRelay = makePeer({
118
+ peerId: '1'.repeat(40),
119
+ onChainReputationScore: 99,
120
+ providerPricing: {
121
+ openai: {
122
+ defaults: { inputUsdPerMillion: 1.88, outputUsdPerMillion: 9.38 },
123
+ },
124
+ },
125
+ providerServiceApiProtocols: {
126
+ openai: { services: { 'kimi-k3': ['openai-chat-completions'] } },
127
+ },
128
+ });
129
+ const emberRoute = makePeer({
130
+ peerId: '2'.repeat(40),
131
+ onChainReputationScore: 96,
132
+ providerPricing: {
133
+ openai: {
134
+ defaults: { inputUsdPerMillion: 0.9, outputUsdPerMillion: 2.7 },
135
+ },
136
+ },
137
+ providerServiceApiProtocols: {
138
+ openai: { services: { 'Kimi K3': ['openai-chat-completions'] } },
139
+ },
140
+ });
141
+ const [model] = buildNetworkModels([cobaltRelay, emberRoute], NOW_MS, {
142
+ routingPreferences: {
143
+ preferFreePeers: false,
144
+ maxInputUsdPerMillion: 25,
145
+ minTrustScore: 60,
146
+ allowedPeerIds: [],
147
+ blockedPeerIds: [],
148
+ },
149
+ });
150
+ assert.deepEqual(model?.peers.map((peer) => peer.peerId), [emberRoute.peerId, cobaltRelay.peerId]);
151
+ });
152
+ test('configured /models sorting puts cooling peers after healthy routes', () => {
153
+ const cooling = makePeer({
154
+ peerId: '3'.repeat(40),
155
+ onChainReputationScore: 99,
156
+ providerPricing: { openai: { defaults: { inputUsdPerMillion: 0, outputUsdPerMillion: 0 } } },
157
+ providerServiceApiProtocols: {
158
+ openai: { services: { 'health-model': ['openai-chat-completions'] } },
159
+ },
160
+ });
161
+ const healthy = makePeer({
162
+ peerId: '4'.repeat(40),
163
+ onChainReputationScore: 70,
164
+ providerPricing: { openai: { defaults: { inputUsdPerMillion: 10, outputUsdPerMillion: 10 } } },
165
+ providerServiceApiProtocols: {
166
+ openai: { services: { 'health-model': ['openai-chat-completions'] } },
167
+ },
168
+ });
169
+ const [model] = buildNetworkModels([cooling, healthy], NOW_MS, {
170
+ routingPreferences: {
171
+ preferFreePeers: false,
172
+ maxInputUsdPerMillion: 25,
173
+ minTrustScore: 60,
174
+ allowedPeerIds: [],
175
+ blockedPeerIds: [],
176
+ },
177
+ peerHealth: new Map([[cooling.peerId, { cooldownUntil: NOW_MS + 30_000, failureStreak: 3 }]]),
178
+ });
179
+ assert.deepEqual(model?.peers.map((peer) => peer.peerId), [healthy.peerId, cooling.peerId]);
180
+ });
181
+ test('returns null reputation and sorts unknown scores last', () => {
182
+ const unknownSeller = makePeer({
183
+ peerId: '0'.repeat(40),
184
+ providerServiceApiProtocols: {
185
+ openai: { services: { 'qwen3-coder': ['openai-chat-completions'] } },
186
+ },
187
+ });
188
+ const models = buildNetworkModels([unknownSeller, textSeller], NOW_MS);
189
+ const qwen = models.find((model) => model.id === 'qwen3-coder');
190
+ assert.ok(qwen);
191
+ assert.deepEqual(qwen.peers.map((peer) => peer.peerId), [textSeller.peerId, unknownSeller.peerId]);
192
+ assert.deepEqual(qwen.peers.map((peer) => peer.reputationScore), [40, null]);
193
+ });
194
+ test('derives on-chain reputation from raw persisted stats', () => {
195
+ const peer = makePeer({
196
+ peerId: '9'.repeat(40),
197
+ providers: ['claude-oauth'],
198
+ providerServiceApiProtocols: {
199
+ 'claude-oauth': { services: { 'claude-opus-4-6': ['anthropic-messages'] } },
200
+ },
201
+ onChainStakeUsdcMicros: 2_000_000,
202
+ onChainChannelCount: 20,
203
+ onChainGhostCount: 0,
204
+ onChainTotalVolumeUsdcMicros: 100_000_000,
205
+ onChainLastSettledAtSec: Math.floor((NOW_MS - 60_000) / 1000),
206
+ onChainStakedAtSec: Math.floor((NOW_MS - 40 * 86_400_000) / 1000),
207
+ });
208
+ const [model] = buildNetworkModels([peer], NOW_MS);
209
+ const [offer] = model?.peers ?? [];
210
+ assert.ok(offer);
211
+ assert.ok((offer.onChainReputationScore ?? 0) > 0);
212
+ assert.equal(offer.reputationScore, offer.onChainReputationScore);
213
+ });
214
+ test('cached-input pricing penalty can change a close reputation ranking', () => {
215
+ const priced = makePeer({
216
+ peerId: '1'.repeat(40),
217
+ onChainTrustScore: 75,
218
+ providerPricing: {
219
+ openai: {
220
+ defaults: { inputUsdPerMillion: 2, outputUsdPerMillion: 4 },
221
+ services: {
222
+ 'cache-model': { inputUsdPerMillion: 2, outputUsdPerMillion: 4, cachedInputUsdPerMillion: 0.2 },
223
+ },
224
+ },
225
+ },
226
+ providerServiceApiProtocols: {
227
+ openai: { services: { 'cache-model': ['openai-chat-completions'] } },
228
+ },
229
+ });
230
+ const unpriced = makePeer({
231
+ peerId: '2'.repeat(40),
232
+ onChainTrustScore: 90,
233
+ providerPricing: {
234
+ openai: {
235
+ defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
236
+ services: { 'cache-model': { inputUsdPerMillion: 1, outputUsdPerMillion: 2 } },
237
+ },
238
+ },
239
+ providerServiceApiProtocols: {
240
+ openai: { services: { 'cache-model': ['openai-chat-completions'] } },
241
+ },
242
+ });
243
+ const model = buildNetworkModels([unpriced, priced], NOW_MS)[0];
244
+ assert.deepEqual(model?.peers.map((peer) => peer.peerId), [priced.peerId, unpriced.peerId]);
245
+ assert.deepEqual(model?.peers.map((peer) => peer.reputationScore), [75, 90]);
246
+ assert.ok((model?.peers[0]?.effectiveReputationScore ?? 0) > (model?.peers[1]?.effectiveReputationScore ?? 0));
247
+ });
248
+ test('cached-input penalty uses normalized reputation instead of raw trust', () => {
249
+ const flash = makePeer({
250
+ peerId: '3'.repeat(40),
251
+ onChainTrustScore: 10_425.074,
252
+ onChainReputationScore: 99.93,
253
+ providerServiceApiProtocols: {
254
+ 'claude-oauth': { services: { 'claude-opus-4-6': ['anthropic-messages'] } },
255
+ },
256
+ });
257
+ const venice = makePeer({
258
+ peerId: '4'.repeat(40),
259
+ onChainTrustScore: 5_428.609,
260
+ onChainReputationScore: 90.31,
261
+ providerPricing: {
262
+ 'claude-oauth': {
263
+ defaults: { inputUsdPerMillion: 3, outputUsdPerMillion: 15 },
264
+ services: {
265
+ 'opus-4.6': { inputUsdPerMillion: 3, outputUsdPerMillion: 15, cachedInputUsdPerMillion: 0.3 },
266
+ },
267
+ },
268
+ },
269
+ providerServiceApiProtocols: {
270
+ 'claude-oauth': { services: { 'opus-4.6': ['anthropic-messages'] } },
271
+ },
272
+ });
273
+ const model = buildNetworkModels([flash, venice], NOW_MS)[0];
274
+ assert.deepEqual(model?.peers.map((peer) => peer.peerId), [venice.peerId, flash.peerId]);
275
+ assert.equal(model?.peers[0]?.effectiveReputationScore, 90.31);
276
+ assert.ok(Math.abs((model?.peers[1]?.effectiveReputationScore ?? 0) - 49.965) < 1e-9);
277
+ });
278
+ test('cached-input pricing penalty does not bury a substantially stronger peer', () => {
279
+ const priced = makePeer({
280
+ peerId: '5'.repeat(40),
281
+ onChainTrustScore: 20,
282
+ onChainReputationScore: 20,
283
+ providerPricing: {
284
+ openai: {
285
+ defaults: { inputUsdPerMillion: 2, outputUsdPerMillion: 4 },
286
+ services: {
287
+ 'cache-model': { inputUsdPerMillion: 2, outputUsdPerMillion: 4, cachedInputUsdPerMillion: 0.2 },
288
+ },
289
+ },
290
+ },
291
+ providerServiceApiProtocols: {
292
+ openai: { services: { 'cache-model': ['openai-chat-completions'] } },
293
+ },
294
+ });
295
+ const unpriced = makePeer({
296
+ peerId: '6'.repeat(40),
297
+ onChainTrustScore: 100,
298
+ onChainReputationScore: 100,
299
+ providerServiceApiProtocols: {
300
+ openai: { services: { 'cache-model': ['openai-chat-completions'] } },
301
+ },
302
+ });
303
+ const model = buildNetworkModels([priced, unpriced], NOW_MS)[0];
304
+ assert.deepEqual(model?.peers.map((peer) => peer.peerId), [unpriced.peerId, priced.peerId]);
305
+ assert.ok((model?.peers[0]?.effectiveReputationScore ?? 0) > (model?.peers[1]?.effectiveReputationScore ?? 0));
306
+ });
307
+ test('keeps reputation ordering when no offer reports cached-input pricing', () => {
308
+ const lower = makePeer({
309
+ peerId: '3'.repeat(40),
310
+ onChainTrustScore: 20,
311
+ providerServiceApiProtocols: {
312
+ openai: { services: { 'no-cache-model': ['openai-chat-completions'] } },
313
+ },
314
+ });
315
+ const higher = makePeer({
316
+ peerId: '4'.repeat(40),
317
+ onChainTrustScore: 100,
318
+ providerServiceApiProtocols: {
319
+ openai: { services: { 'no-cache-model': ['openai-chat-completions'] } },
320
+ },
321
+ });
322
+ const model = buildNetworkModels([lower, higher], NOW_MS)[0];
323
+ assert.deepEqual(model?.peers.map((peer) => peer.peerId), [higher.peerId, lower.peerId]);
324
+ assert.deepEqual(model?.peers.map((peer) => peer.effectiveReputationScore), [normalizedModelReputationScore(higher, NOW_MS), normalizedModelReputationScore(lower, NOW_MS)]);
325
+ });
326
+ test('merges conservative family aliases while preserving advertised service ids', () => {
327
+ const branded = makePeer({
328
+ peerId: 'd'.repeat(40),
329
+ providerServiceApiProtocols: {
330
+ anthropic: { services: { 'claude-opus-5': ['anthropic-messages'] } },
331
+ },
332
+ });
333
+ const unbranded = makePeer({
334
+ peerId: 'e'.repeat(40),
335
+ providerServiceApiProtocols: {
336
+ anthropic: { services: { 'Opus 5': ['anthropic-messages'] } },
337
+ },
338
+ });
339
+ const models = buildNetworkModels([branded, unbranded], NOW_MS);
340
+ assert.equal(models.length, 1);
341
+ assert.equal(models[0]?.id, 'claude-opus-5');
342
+ assert.deepEqual(models[0]?.aliases, ['claude-opus-5', 'opus-5', 'opus5']);
343
+ assert.deepEqual(models[0]?.peers.map((peer) => peer.serviceId), ['claude-opus-5', 'Opus 5']);
344
+ });
345
+ test('merges Fable aliases and the coding-only service variant', () => {
346
+ const branded = makePeer({
347
+ peerId: 'a'.repeat(40),
348
+ providerServiceApiProtocols: {
349
+ openai: { services: { 'claude-fable-5': ['openai-chat-completions'] } },
350
+ },
351
+ });
352
+ const unbranded = makePeer({
353
+ peerId: 'b'.repeat(40),
354
+ providerServiceApiProtocols: {
355
+ openai: { services: { 'fable-5': ['openai-chat-completions'] } },
356
+ },
357
+ });
358
+ const codingOnly = makePeer({
359
+ peerId: 'c'.repeat(40),
360
+ providerServiceApiProtocols: {
361
+ 'claude-oauth': { services: { 'fable-5-coding-only': ['anthropic-messages'] } },
362
+ },
363
+ });
364
+ const models = buildNetworkModels([branded, unbranded, codingOnly], NOW_MS);
365
+ assert.equal(models.length, 1);
366
+ assert.equal(models[0]?.id, 'claude-fable-5');
367
+ assert.deepEqual(models[0]?.aliases, [
368
+ 'claude-fable-5',
369
+ 'fable-5',
370
+ 'fable-5-coding-only',
371
+ 'fable5',
372
+ ]);
373
+ assert.deepEqual(models[0]?.supported_protocols, ['anthropic-messages', 'openai-chat-completions']);
374
+ assert.deepEqual(models[0]?.peers.map((peer) => peer.serviceId), [
375
+ 'claude-fable-5',
376
+ 'fable-5',
377
+ 'fable-5-coding-only',
378
+ ]);
379
+ });
380
+ test('merges Claude coding-only aliases and displays decimal versions', () => {
381
+ const base = makePeer({
382
+ peerId: '7'.repeat(40),
383
+ providerServiceApiProtocols: {
384
+ anthropic: { services: { 'claude-opus-4.8': ['anthropic-messages'] } },
385
+ },
386
+ });
387
+ const codingOnly = makePeer({
388
+ peerId: '8'.repeat(40),
389
+ providerServiceApiProtocols: {
390
+ 'claude-oauth': { services: { 'opus-4.8-coding-only': ['anthropic-messages'] } },
391
+ },
392
+ });
393
+ const models = buildNetworkModels([base, codingOnly], NOW_MS);
394
+ assert.equal(models.length, 1);
395
+ assert.equal(models[0]?.name, 'Claude Opus 4.8');
396
+ assert.deepEqual(models[0]?.peers.map((peer) => peer.serviceId), ['claude-opus-4.8', 'opus-4.8-coding-only']);
397
+ });
398
+ test('merges compact numeric versions and flattened vendor prefixes', () => {
399
+ const dotted = makePeer({
400
+ peerId: '1'.repeat(40),
401
+ providerServiceApiProtocols: {
402
+ openai: { services: { 'gpt-5.6-sol': ['openai-responses'] } },
403
+ },
404
+ });
405
+ const compact = makePeer({
406
+ peerId: '2'.repeat(40),
407
+ providerServiceApiProtocols: {
408
+ openai: { services: { 'gpt-56-sol': ['openai-responses'] } },
409
+ },
410
+ });
411
+ const vendorPrefixed = makePeer({
412
+ peerId: '3'.repeat(40),
413
+ providerServiceApiProtocols: {
414
+ openai: { services: { 'openai-gpt-56-sol': ['openai-responses'] } },
415
+ },
416
+ });
417
+ const models = buildNetworkModels([dotted, compact, vendorPrefixed], NOW_MS);
418
+ assert.equal(models.length, 1);
419
+ assert.equal(models[0]?.id, 'gpt-5.6-sol');
420
+ assert.equal(models[0]?.name, 'GPT 5.6 Sol');
421
+ assert.deepEqual(models[0]?.aliases, ['gpt-5.6-sol', 'gpt-56-sol', 'gpt56sol', 'openai-gpt-56-sol']);
422
+ assert.deepEqual(models[0]?.peers.map((peer) => peer.serviceId), [
423
+ 'gpt-5.6-sol',
424
+ 'gpt-56-sol',
425
+ 'openai-gpt-56-sol',
426
+ ]);
427
+ });
428
+ test('uses preferred MiniMax decimal display names across aliases', () => {
429
+ const dotted = makePeer({
430
+ peerId: '4'.repeat(40),
431
+ providerServiceApiProtocols: {
432
+ openai: { services: { 'minimax-m2.5': ['openai-chat-completions'] } },
433
+ },
434
+ });
435
+ const separated = makePeer({
436
+ peerId: '5'.repeat(40),
437
+ providerServiceApiProtocols: {
438
+ openai: { services: { 'minimax-m2-5': ['openai-chat-completions'] } },
439
+ },
440
+ });
441
+ const compact = makePeer({
442
+ peerId: '6'.repeat(40),
443
+ providerServiceApiProtocols: {
444
+ openai: { services: { 'minimax-m25': ['openai-chat-completions'] } },
445
+ },
446
+ });
447
+ const models = buildNetworkModels([dotted, separated, compact], NOW_MS);
448
+ assert.equal(models.length, 1);
449
+ assert.equal(models[0]?.name, 'MiniMax M2.5');
450
+ });
451
+ test('keeps only the lowest-priced duplicate text offer from one peer', () => {
452
+ const peer = makePeer({
453
+ peerId: '8'.repeat(40),
454
+ providerPricing: {
455
+ openai: {
456
+ defaults: { inputUsdPerMillion: 0, outputUsdPerMillion: 0 },
457
+ services: {
458
+ 'gpt-5.6-sol': { inputUsdPerMillion: 4, outputUsdPerMillion: 8 },
459
+ 'gpt-56-sol': { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
460
+ },
461
+ },
462
+ },
463
+ providerServiceApiProtocols: {
464
+ openai: {
465
+ services: {
466
+ 'gpt-5.6-sol': ['openai-responses'],
467
+ 'gpt-56-sol': ['openai-responses'],
468
+ },
469
+ },
470
+ },
471
+ });
472
+ const model = buildNetworkModels([peer], NOW_MS)[0];
473
+ assert.ok(model);
474
+ assert.equal(model.peers.length, 1);
475
+ assert.equal(model.peers[0]?.serviceId, 'gpt-56-sol');
476
+ assert.equal(model.peers[0]?.inputUsdPerMillion, 1);
477
+ assert.equal(model.peers[0]?.outputUsdPerMillion, 2);
478
+ assert.deepEqual(model.aliases, ['gpt-5.6-sol', 'gpt-56-sol', 'gpt56sol']);
479
+ });
480
+ test('keeps only the cheapest canonical offer across providers for one peer', () => {
481
+ const peer = makePeer({
482
+ peerId: 'a'.repeat(40),
483
+ providers: ['anthropic', 'claude-oauth', 'openai'],
484
+ providerPricing: {
485
+ anthropic: {
486
+ defaults: { inputUsdPerMillion: 18, outputUsdPerMillion: 9 },
487
+ services: { 'claude-opus-4.8': { inputUsdPerMillion: 18, outputUsdPerMillion: 9 } },
488
+ },
489
+ 'claude-oauth': {
490
+ defaults: { inputUsdPerMillion: 2.25, outputUsdPerMillion: 11.25 },
491
+ services: { 'opus-4.8-coding-only': { inputUsdPerMillion: 2.25, outputUsdPerMillion: 11.25 } },
492
+ },
493
+ openai: {
494
+ defaults: { inputUsdPerMillion: 0.35, outputUsdPerMillion: 0.95 },
495
+ services: { 'claude-opus-4-8': { inputUsdPerMillion: 0.35, outputUsdPerMillion: 0.95 } },
496
+ },
497
+ },
498
+ providerServiceApiProtocols: {
499
+ anthropic: { services: { 'claude-opus-4.8': ['anthropic-messages'] } },
500
+ 'claude-oauth': { services: { 'opus-4.8-coding-only': ['anthropic-messages'] } },
501
+ openai: { services: { 'claude-opus-4-8': ['openai-chat-completions'] } },
502
+ },
503
+ });
504
+ const model = buildNetworkModels([peer], NOW_MS)[0];
505
+ assert.ok(model);
506
+ assert.equal(model.peers.length, 1);
507
+ assert.equal(model.peers[0]?.provider, 'openai');
508
+ assert.equal(model.peers[0]?.serviceId, 'claude-opus-4-8');
509
+ assert.equal(model.peers[0]?.inputUsdPerMillion, 0.35);
510
+ assert.deepEqual(model.aliases, [
511
+ 'claude-opus-4-8',
512
+ 'claude-opus-4.8',
513
+ 'opus-4.8-coding-only',
514
+ 'opus48',
515
+ ]);
516
+ });
517
+ test('keeps only the lowest-priced duplicate image offer from one peer', () => {
518
+ const peer = makePeer({
519
+ peerId: '9'.repeat(40),
520
+ providerServiceApiProtocols: {
521
+ openai: {
522
+ services: {
523
+ 'flux-1-schnell': ['openai-images'],
524
+ 'flux1-schnell': ['openai-images'],
525
+ },
526
+ },
527
+ },
528
+ providerServiceUnitBillingModels: {
529
+ openai: {
530
+ services: {
531
+ 'flux-1-schnell': {
532
+ 'openai-images': {
533
+ version: 1,
534
+ components: [{ unit: 'output_images', priceUsd: 0.04 }],
535
+ },
536
+ },
537
+ 'flux1-schnell': {
538
+ 'openai-images': {
539
+ version: 1,
540
+ components: [{ unit: 'output_images', priceUsd: 0.01 }],
541
+ },
542
+ },
543
+ },
544
+ },
545
+ },
546
+ });
547
+ const model = buildNetworkModels([peer], NOW_MS)[0];
548
+ assert.ok(model);
549
+ assert.equal(model.peers.length, 1);
550
+ assert.equal(model.peers[0]?.serviceId, 'flux1-schnell');
551
+ assert.equal(model.peers[0]?.minImageUsdPerImage, 0.01);
552
+ });
553
+ test('prefers a known duplicate price over an unknown price', () => {
554
+ const peer = makePeer({
555
+ peerId: '0'.repeat(40),
556
+ providerServiceApiProtocols: {
557
+ openai: {
558
+ services: {
559
+ 'flux-1-schnell': ['openai-images'],
560
+ 'flux1-schnell': ['openai-images'],
561
+ },
562
+ },
563
+ },
564
+ providerServiceUnitBillingModels: {
565
+ openai: {
566
+ services: {
567
+ 'flux1-schnell': {
568
+ 'openai-images': {
569
+ version: 1,
570
+ components: [{ unit: 'output_images', priceUsd: 0.02 }],
571
+ },
572
+ },
573
+ },
574
+ },
575
+ },
576
+ });
577
+ const model = buildNetworkModels([peer], NOW_MS)[0];
578
+ assert.equal(model?.peers[0]?.serviceId, 'flux1-schnell');
579
+ });
580
+ test('service pricing overrides provider defaults', () => {
581
+ const models = buildNetworkModels([textSeller], NOW_MS);
582
+ const offer = models[0]?.peers[0];
583
+ assert.ok(offer);
584
+ assert.equal(offer.inputUsdPerMillion, 3);
585
+ assert.equal(offer.outputUsdPerMillion, 9);
586
+ assert.equal(offer.cachedInputUsdPerMillion, 0.3);
587
+ assert.equal(offer.displayName, 'Text Seller');
588
+ });
589
+ test('exposes peer capabilities and conservative model-level guarantees', () => {
590
+ const first = makePeer({
591
+ peerId: '4'.repeat(40),
592
+ providerServiceApiProtocols: {
593
+ openai: { services: { 'gpt-capable': ['openai-responses'] } },
594
+ },
595
+ providerServiceCapabilities: {
596
+ openai: {
597
+ services: {
598
+ 'gpt-capable': {
599
+ contextWindow: 200_000,
600
+ maxOutputTokens: 64_000,
601
+ inputs: ['text', 'image'],
602
+ outputs: ['text'],
603
+ reasoning: true,
604
+ toolUse: true,
605
+ structuredOutput: true,
606
+ supportedParameters: ['temperature', 'tools', 'response_format'],
607
+ },
608
+ },
609
+ },
610
+ },
611
+ providerServiceCategories: {
612
+ openai: { services: { 'gpt-capable': ['chat', 'reasoning'] } },
613
+ },
614
+ });
615
+ const second = makePeer({
616
+ peerId: '5'.repeat(40),
617
+ providerServiceApiProtocols: {
618
+ openai: { services: { 'gpt-capable': ['openai-chat-completions'] } },
619
+ },
620
+ providerServiceCapabilities: {
621
+ openai: {
622
+ services: {
623
+ 'gpt-capable': {
624
+ contextWindow: 128_000,
625
+ maxOutputTokens: 32_000,
626
+ inputs: ['text'],
627
+ outputs: ['text'],
628
+ reasoning: false,
629
+ toolUse: true,
630
+ structuredOutput: false,
631
+ supportedParameters: ['tools', 'top_p', 'temperature'],
632
+ },
633
+ },
634
+ },
635
+ },
636
+ });
637
+ const model = buildNetworkModels([first, second], NOW_MS)[0];
638
+ assert.ok(model);
639
+ assert.deepEqual(model.supported_protocols, ['openai-chat-completions', 'openai-responses']);
640
+ assert.equal(model.context_length, 128_000);
641
+ assert.equal(model.max_output_tokens, 32_000);
642
+ assert.deepEqual(model.architecture, {
643
+ input_modalities: ['text'],
644
+ output_modalities: ['text'],
645
+ });
646
+ assert.deepEqual(model.capabilities, {
647
+ reasoning: false,
648
+ tool_use: true,
649
+ structured_output: false,
650
+ });
651
+ assert.deepEqual(model.supported_parameters, ['temperature', 'tools']);
652
+ assert.deepEqual(model.capability_coverage, {
653
+ total_offers: 2,
654
+ context_length: 2,
655
+ max_output_tokens: 2,
656
+ input_modalities: 2,
657
+ output_modalities: 2,
658
+ reasoning: 2,
659
+ tool_use: 2,
660
+ structured_output: 2,
661
+ supported_parameters: 2,
662
+ });
663
+ assert.equal(model.peers[0]?.protocol, 'openai-responses');
664
+ assert.deepEqual(model.peers[0]?.categories, ['chat', 'reasoning']);
665
+ assert.equal(model.peers[0]?.capabilities?.contextWindow, 200_000);
666
+ });
667
+ test('does not claim model-level guarantees when any offer omits a capability', () => {
668
+ const reported = makePeer({
669
+ peerId: '6'.repeat(40),
670
+ providerServiceCapabilities: {
671
+ openai: {
672
+ services: {
673
+ 'partially-described': {
674
+ contextWindow: 128_000,
675
+ inputs: ['text', 'image'],
676
+ toolUse: true,
677
+ },
678
+ },
679
+ },
680
+ },
681
+ });
682
+ const unknown = makePeer({
683
+ peerId: '7'.repeat(40),
684
+ providerServiceApiProtocols: {
685
+ openai: { services: { 'partially-described': ['openai-chat-completions'] } },
686
+ },
687
+ });
688
+ const model = buildNetworkModels([reported, unknown], NOW_MS)[0];
689
+ assert.ok(model);
690
+ assert.equal(model.context_length, undefined);
691
+ assert.equal(model.architecture, undefined);
692
+ assert.equal(model.capabilities, undefined);
693
+ assert.equal(model.supported_parameters, undefined);
694
+ assert.equal(model.capability_coverage.context_length, 1);
695
+ assert.equal(model.capability_coverage.input_modalities, 1);
696
+ assert.equal(model.capability_coverage.tool_use, 1);
697
+ });
698
+ test('image models are detected via protocol or capability outputs', () => {
699
+ const models = buildNetworkModels([textSeller, imageSeller, mixedSeller], NOW_MS);
700
+ const flux = models.find((model) => model.id === 'flux-1-schnell');
701
+ const sdxl = models.find((model) => model.id === 'sdxl-turbo');
702
+ assert.equal(flux?.type, 'image');
703
+ assert.equal(sdxl?.type, 'image');
704
+ assert.equal(flux?.peers[0]?.minImageUsdPerImage, 0.01);
705
+ assert.equal(flux?.peers[0]?.maxImageUsdPerImage, 0.05);
706
+ });
707
+ test('type filter splits text and image models', () => {
708
+ const models = buildNetworkModels([textSeller, imageSeller, mixedSeller], NOW_MS);
709
+ const images = models.filter((model) => model.type === 'image');
710
+ const text = models.filter((model) => model.type === 'text');
711
+ assert.deepEqual(images.map((model) => model.id), ['flux-1-schnell', 'sdxl-turbo']);
712
+ assert.deepEqual(text.map((model) => model.id), ['qwen3-coder']);
713
+ });
714
+ test('returns an empty list when no peers are discovered', () => {
715
+ assert.deepEqual(buildNetworkModels([], NOW_MS), []);
716
+ });
717
+ test('keeps a canonically merged model text-routable when any peer serves text', () => {
718
+ const image = makePeer({
719
+ peerId: '4'.repeat(40),
720
+ providerServiceApiProtocols: {
721
+ openai: { services: { 'shared-model': ['openai-images'] } },
722
+ },
723
+ });
724
+ const text = makePeer({
725
+ peerId: '5'.repeat(40),
726
+ providerServiceApiProtocols: {
727
+ openai: { services: { 'shared model': ['openai-chat-completions'] } },
728
+ },
729
+ });
730
+ const [model] = buildNetworkModels([image, text], NOW_MS);
731
+ assert.equal(model?.type, 'text');
732
+ assert.deepEqual(model?.peers.map((peer) => peer.type).sort(), ['image', 'text']);
733
+ });
734
+ //# sourceMappingURL=network-models.test.js.map