@antseed/cli 0.1.43 → 0.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/balance.js +6 -6
- package/dist/cli/commands/balance.js.map +1 -1
- package/dist/cli/commands/connect.d.ts.map +1 -1
- package/dist/cli/commands/connect.js +30 -20
- package/dist/cli/commands/connect.js.map +1 -1
- package/dist/cli/commands/deposit.d.ts.map +1 -1
- package/dist/cli/commands/deposit.js +8 -18
- package/dist/cli/commands/deposit.js.map +1 -1
- package/dist/cli/commands/emissions.d.ts +3 -0
- package/dist/cli/commands/emissions.d.ts.map +1 -0
- package/dist/cli/commands/emissions.js +84 -0
- package/dist/cli/commands/emissions.js.map +1 -0
- package/dist/cli/commands/payments.d.ts +3 -0
- package/dist/cli/commands/payments.d.ts.map +1 -0
- package/dist/cli/commands/payments.js +26 -0
- package/dist/cli/commands/payments.js.map +1 -0
- package/dist/cli/commands/register.d.ts +3 -0
- package/dist/cli/commands/register.d.ts.map +1 -0
- package/dist/cli/commands/register.js +41 -0
- package/dist/cli/commands/register.js.map +1 -0
- package/dist/cli/commands/reputation.d.ts +3 -0
- package/dist/cli/commands/reputation.d.ts.map +1 -0
- package/dist/cli/commands/reputation.js +94 -0
- package/dist/cli/commands/reputation.js.map +1 -0
- package/dist/cli/commands/seed.d.ts.map +1 -1
- package/dist/cli/commands/seed.js +20 -7
- package/dist/cli/commands/seed.js.map +1 -1
- package/dist/cli/commands/sessions.d.ts +3 -0
- package/dist/cli/commands/sessions.d.ts.map +1 -0
- package/dist/cli/commands/sessions.js +79 -0
- package/dist/cli/commands/sessions.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +3 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +83 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/stake.d.ts +3 -0
- package/dist/cli/commands/stake.d.ts.map +1 -0
- package/dist/cli/commands/stake.js +73 -0
- package/dist/cli/commands/stake.js.map +1 -0
- package/dist/cli/commands/subscribe.d.ts +3 -0
- package/dist/cli/commands/subscribe.d.ts.map +1 -0
- package/dist/cli/commands/subscribe.js +96 -0
- package/dist/cli/commands/subscribe.js.map +1 -0
- package/dist/cli/commands/withdraw.js +2 -2
- package/dist/cli/commands/withdraw.js.map +1 -1
- package/dist/cli/index.js +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/payment-utils.d.ts +45 -0
- package/dist/cli/payment-utils.d.ts.map +1 -0
- package/dist/cli/payment-utils.js +102 -0
- package/dist/cli/payment-utils.js.map +1 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +3 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +3 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts +9 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.d.ts +7 -23
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +141 -762
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/request-utils.d.ts +21 -0
- package/dist/proxy/request-utils.d.ts.map +1 -0
- package/dist/proxy/request-utils.js +237 -0
- package/dist/proxy/request-utils.js.map +1 -0
- package/dist/proxy/routing.d.ts +17 -0
- package/dist/proxy/routing.d.ts.map +1 -0
- package/dist/proxy/routing.js +115 -0
- package/dist/proxy/routing.js.map +1 -0
- package/dist/proxy/telemetry.d.ts +23 -0
- package/dist/proxy/telemetry.d.ts.map +1 -0
- package/dist/proxy/telemetry.js +227 -0
- package/dist/proxy/telemetry.js.map +1 -0
- package/package.json +7 -6
|
@@ -4,610 +4,65 @@ import { watch } from 'node:fs';
|
|
|
4
4
|
import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
|
-
import { createOpenAIChatToAnthropicStreamingAdapter, createOpenAIChatToResponsesStreamingAdapter, createOpenAIResponsesToChatStreamingAdapter, detectRequestServiceApiProtocol,
|
|
8
|
-
|
|
7
|
+
import { createOpenAIChatToAnthropicStreamingAdapter, createOpenAIChatToResponsesStreamingAdapter, createOpenAIResponsesToChatStreamingAdapter, detectRequestServiceApiProtocol, transformAnthropicMessagesRequestToOpenAIChat, transformOpenAIChatRequestToOpenAIResponses, transformOpenAIChatResponseToAnthropicMessage, transformOpenAIChatResponseToOpenAIResponses, transformOpenAIResponsesRequestToOpenAIChat, transformOpenAIResponsesResponseToOpenAIChat, } from './service-api-adapter.js';
|
|
8
|
+
import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, rewriteServiceInBody, isConnectionChurnError, isConnectionHealthy, isLoopbackPeer, } from './request-utils.js';
|
|
9
|
+
import { getExplicitProviderOverride, getExplicitPeerIdOverride, getPreferredPeerIdHint, resolvePeerRoutePlan, selectCandidatePeersForRouting, } from './routing.js';
|
|
10
|
+
import { computeResponseTelemetry, attachAntseedTelemetryHeaders, attachStreamingAntseedHeaders, } from './telemetry.js';
|
|
11
|
+
// Re-export for backward compatibility (used by tests and other consumers)
|
|
12
|
+
export { selectCandidatePeersForRouting } from './routing.js';
|
|
13
|
+
export { rewriteServiceInBody } from './request-utils.js';
|
|
9
14
|
const BUYER_STATE_FILE = join(homedir(), '.antseed', 'buyer.state.json');
|
|
10
|
-
const
|
|
11
|
-
function
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
function getExplicitProviderOverride(request) {
|
|
16
|
-
const provider = request.headers['x-antseed-provider']?.trim().toLowerCase();
|
|
17
|
-
return provider && provider.length > 0 ? provider : null;
|
|
18
|
-
}
|
|
19
|
-
function getExplicitPeerIdOverride(request, sessionPinnedPeerId) {
|
|
20
|
-
// Per-request header takes priority over session pin
|
|
21
|
-
const header = request.headers['x-antseed-pin-peer']?.trim().toLowerCase();
|
|
22
|
-
if (header && header.length > 0)
|
|
23
|
-
return header;
|
|
24
|
-
return sessionPinnedPeerId?.toLowerCase() ?? null;
|
|
25
|
-
}
|
|
26
|
-
function getPreferredPeerIdHint(request) {
|
|
27
|
-
const header = request.headers['x-antseed-prefer-peer']?.trim().toLowerCase();
|
|
28
|
-
if (!header || header.length === 0) {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
return header;
|
|
32
|
-
}
|
|
33
|
-
function getPeerProviderProtocols(peer, provider, requestedService) {
|
|
34
|
-
const normalizedRequestedService = requestedService?.trim();
|
|
35
|
-
const fromMetadata = peer.providerServiceApiProtocols?.[provider]?.services;
|
|
36
|
-
if (fromMetadata) {
|
|
37
|
-
if (normalizedRequestedService) {
|
|
38
|
-
const directMatchKey = Object.keys(fromMetadata).find((key) => key.toLowerCase() === normalizedRequestedService.toLowerCase());
|
|
39
|
-
if (directMatchKey && fromMetadata[directMatchKey]?.length) {
|
|
40
|
-
log(`Service match: peer ${peer.peerId.slice(0, 8)} provider=${provider} service="${normalizedRequestedService}" `
|
|
41
|
-
+ `→ [${fromMetadata[directMatchKey].join(',')}]`);
|
|
42
|
-
return Array.from(new Set(fromMetadata[directMatchKey]));
|
|
43
|
-
}
|
|
44
|
-
if (Object.keys(fromMetadata).length > 0) {
|
|
45
|
-
log(`Service strict-miss: peer ${peer.peerId.slice(0, 8)} provider=${provider} service="${normalizedRequestedService}" `
|
|
46
|
-
+ 'not in metadata; excluding from route candidates.');
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
const merged = Object.values(fromMetadata).flat();
|
|
51
|
-
if (merged.length > 0) {
|
|
52
|
-
if (requestedService) {
|
|
53
|
-
log(`Service hint miss: peer ${peer.peerId.slice(0, 8)} provider=${provider} service="${requestedService}" not in metadata; falling back to provider protocol set [${Array.from(new Set(merged)).join(',')}]`);
|
|
54
|
-
}
|
|
55
|
-
return Array.from(new Set(merged));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const inferred = inferProviderDefaultServiceApiProtocols(provider);
|
|
59
|
-
log(`No metadata: peer ${peer.peerId.slice(0, 8)} provider=${provider} → inferred [${inferred.join(',')}]`);
|
|
60
|
-
return inferred;
|
|
61
|
-
}
|
|
62
|
-
function resolvePeerRoutePlan(peer, requestProtocol, requestedService, explicitProvider) {
|
|
63
|
-
const providers = peer.providers
|
|
64
|
-
.map((provider) => provider.trim().toLowerCase())
|
|
65
|
-
.filter((provider) => provider.length > 0);
|
|
66
|
-
if (providers.length === 0) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
if (explicitProvider && !providers.includes(explicitProvider)) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
const candidates = explicitProvider ? [explicitProvider] : providers;
|
|
73
|
-
if (!requestProtocol) {
|
|
74
|
-
const provider = candidates[0];
|
|
75
|
-
return provider ? { provider, selection: null } : null;
|
|
76
|
-
}
|
|
77
|
-
let transformedFallback = null;
|
|
78
|
-
for (const provider of candidates) {
|
|
79
|
-
const supportedProtocols = getPeerProviderProtocols(peer, provider, requestedService);
|
|
80
|
-
const selection = selectTargetProtocolForRequest(requestProtocol, supportedProtocols);
|
|
81
|
-
if (!selection) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (!selection.requiresTransform) {
|
|
85
|
-
return { provider, selection };
|
|
86
|
-
}
|
|
87
|
-
if (!transformedFallback) {
|
|
88
|
-
transformedFallback = { provider, selection };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return transformedFallback;
|
|
92
|
-
}
|
|
93
|
-
export function selectCandidatePeersForRouting(peers, requestProtocol, requestedService, explicitProvider) {
|
|
94
|
-
const routePlanByPeerId = new Map();
|
|
95
|
-
if (!requestProtocol && !explicitProvider) {
|
|
96
|
-
return {
|
|
97
|
-
candidatePeers: peers,
|
|
98
|
-
routePlanByPeerId,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
const candidatePeers = peers.filter((peer) => {
|
|
102
|
-
const plan = resolvePeerRoutePlan(peer, requestProtocol, requestedService, explicitProvider);
|
|
103
|
-
if (!plan)
|
|
104
|
-
return false;
|
|
105
|
-
routePlanByPeerId.set(peer.peerId, plan);
|
|
106
|
-
return true;
|
|
107
|
-
});
|
|
108
|
-
return {
|
|
109
|
-
candidatePeers,
|
|
110
|
-
routePlanByPeerId,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
function parseTokenCount(value) {
|
|
114
|
-
const parsed = Number(value);
|
|
115
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
116
|
-
return 0;
|
|
117
|
-
}
|
|
118
|
-
return Math.floor(parsed);
|
|
119
|
-
}
|
|
120
|
-
function parseUsageObject(value) {
|
|
121
|
-
if (!value || typeof value !== 'object') {
|
|
122
|
-
return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
123
|
-
}
|
|
124
|
-
const usage = value;
|
|
125
|
-
const total = parseTokenCount(usage.totalTokens ?? usage.total_tokens ?? usage.total_token_count);
|
|
126
|
-
let input = parseTokenCount(usage.inputTokens
|
|
127
|
-
?? usage.input_tokens
|
|
128
|
-
?? usage.promptTokens
|
|
129
|
-
?? usage.prompt_tokens
|
|
130
|
-
?? usage.input_token_count
|
|
131
|
-
?? usage.prompt_token_count
|
|
132
|
-
?? usage.cache_creation_input_tokens
|
|
133
|
-
?? usage.cache_read_input_tokens);
|
|
134
|
-
let output = parseTokenCount(usage.outputTokens
|
|
135
|
-
?? usage.output_tokens
|
|
136
|
-
?? usage.completionTokens
|
|
137
|
-
?? usage.completion_tokens
|
|
138
|
-
?? usage.output_token_count
|
|
139
|
-
?? usage.completion_token_count);
|
|
140
|
-
if (total > 0) {
|
|
141
|
-
if (input === 0 && output === 0) {
|
|
142
|
-
output = total;
|
|
143
|
-
}
|
|
144
|
-
else if (output === 0 && input > 0 && total >= input) {
|
|
145
|
-
output = total - input;
|
|
146
|
-
}
|
|
147
|
-
else if (input === 0 && output > 0 && total >= output) {
|
|
148
|
-
input = total - output;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return {
|
|
152
|
-
inputTokens: input,
|
|
153
|
-
outputTokens: output,
|
|
154
|
-
totalTokens: input + output,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
function estimateTokensFromBytes(inputBytes, outputBytes) {
|
|
158
|
-
const inputTokens = Math.max(1, Math.round(Math.max(0, inputBytes) / 4));
|
|
159
|
-
const outputTokens = Math.max(1, Math.round(Math.max(0, outputBytes) / 4));
|
|
160
|
-
return {
|
|
161
|
-
inputTokens,
|
|
162
|
-
outputTokens,
|
|
163
|
-
totalTokens: inputTokens + outputTokens,
|
|
164
|
-
source: 'estimated',
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
function parseSseUsage(body) {
|
|
168
|
-
const text = new TextDecoder().decode(body);
|
|
169
|
-
const lines = text.split('\n');
|
|
170
|
-
let inputTokens = 0;
|
|
171
|
-
let outputTokens = 0;
|
|
172
|
-
let totalTokens = 0;
|
|
173
|
-
for (const line of lines) {
|
|
174
|
-
const trimmed = line.trim();
|
|
175
|
-
if (!trimmed.startsWith('data:'))
|
|
176
|
-
continue;
|
|
177
|
-
const payload = trimmed.slice(5).trim();
|
|
178
|
-
if (payload.length === 0 || payload === '[DONE]')
|
|
179
|
-
continue;
|
|
180
|
-
let parsed;
|
|
181
|
-
try {
|
|
182
|
-
parsed = JSON.parse(payload);
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
const directUsage = parseUsageObject(parsed.usage);
|
|
188
|
-
if (directUsage.totalTokens > 0) {
|
|
189
|
-
inputTokens = Math.max(inputTokens, directUsage.inputTokens);
|
|
190
|
-
outputTokens = Math.max(outputTokens, directUsage.outputTokens);
|
|
191
|
-
totalTokens = Math.max(totalTokens, directUsage.totalTokens);
|
|
192
|
-
}
|
|
193
|
-
const message = parsed.message;
|
|
194
|
-
const messageUsage = parseUsageObject(message && typeof message === 'object' ? message.usage : undefined);
|
|
195
|
-
if (messageUsage.totalTokens > 0) {
|
|
196
|
-
inputTokens = Math.max(inputTokens, messageUsage.inputTokens);
|
|
197
|
-
outputTokens = Math.max(outputTokens, messageUsage.outputTokens);
|
|
198
|
-
totalTokens = Math.max(totalTokens, messageUsage.totalTokens);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if (totalTokens <= 0) {
|
|
202
|
-
totalTokens = inputTokens + outputTokens;
|
|
203
|
-
}
|
|
204
|
-
return { inputTokens, outputTokens, totalTokens };
|
|
205
|
-
}
|
|
206
|
-
function parseJsonUsage(body) {
|
|
207
|
-
try {
|
|
208
|
-
const parsed = JSON.parse(new TextDecoder().decode(body));
|
|
209
|
-
const direct = parseUsageObject(parsed.usage);
|
|
210
|
-
if (direct.totalTokens > 0) {
|
|
211
|
-
return direct;
|
|
212
|
-
}
|
|
213
|
-
const message = parsed.message;
|
|
214
|
-
if (message && typeof message === 'object') {
|
|
215
|
-
const nested = parseUsageObject(message.usage);
|
|
216
|
-
if (nested.totalTokens > 0) {
|
|
217
|
-
return nested;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
const result = parsed.result;
|
|
221
|
-
if (result && typeof result === 'object') {
|
|
222
|
-
const nested = parseUsageObject(result.usage);
|
|
223
|
-
if (nested.totalTokens > 0) {
|
|
224
|
-
return nested;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
function pickProviderForPeer(peer, request) {
|
|
234
|
-
const explicit = getExplicitProviderOverride(request);
|
|
235
|
-
if (explicit) {
|
|
236
|
-
return explicit;
|
|
237
|
-
}
|
|
238
|
-
if (request.path.startsWith('/v1/messages') && peer.providers.includes('anthropic')) {
|
|
239
|
-
return 'anthropic';
|
|
240
|
-
}
|
|
241
|
-
const first = peer.providers[0]?.trim();
|
|
242
|
-
if (first && first.length > 0) {
|
|
243
|
-
return first.toLowerCase();
|
|
244
|
-
}
|
|
245
|
-
return 'unknown';
|
|
246
|
-
}
|
|
247
|
-
function extractRequestedService(request) {
|
|
248
|
-
const contentType = (request.headers['content-type'] ?? request.headers['Content-Type'] ?? '').toLowerCase();
|
|
249
|
-
if (!contentType.includes('application/json')) {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
try {
|
|
253
|
-
const parsed = JSON.parse(new TextDecoder().decode(request.body));
|
|
254
|
-
const service = parsed.service ?? parsed.model;
|
|
255
|
-
if (typeof service === 'string' && service.trim().length > 0) {
|
|
256
|
-
return service.trim();
|
|
257
|
-
}
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
return null;
|
|
15
|
+
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
16
|
+
function adaptOpenAICompatibleErrorResponse(response, requestProtocol) {
|
|
17
|
+
if (response.statusCode !== 402) {
|
|
18
|
+
return response;
|
|
262
19
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (!body || body.length === 0) {
|
|
266
|
-
return null;
|
|
20
|
+
if (requestProtocol !== 'openai-responses' && requestProtocol !== 'openai-chat-completions') {
|
|
21
|
+
return response;
|
|
267
22
|
}
|
|
23
|
+
let parsed = null;
|
|
268
24
|
try {
|
|
269
|
-
|
|
270
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
return parsed;
|
|
25
|
+
parsed = JSON.parse(Buffer.from(response.body).toString('utf-8'));
|
|
274
26
|
}
|
|
275
27
|
catch {
|
|
276
|
-
return
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
function summarizeMessageShape(messagesRaw) {
|
|
280
|
-
if (!Array.isArray(messagesRaw)) {
|
|
281
|
-
return 'msgShape=none';
|
|
282
|
-
}
|
|
283
|
-
const roleCounts = new Map();
|
|
284
|
-
const contentKindCounts = new Map();
|
|
285
|
-
const blockTypeCounts = new Map();
|
|
286
|
-
let invalidMessages = 0;
|
|
287
|
-
let firstRole = 'none';
|
|
288
|
-
let lastRole = 'none';
|
|
289
|
-
const bump = (map, key) => {
|
|
290
|
-
map.set(key, (map.get(key) ?? 0) + 1);
|
|
291
|
-
};
|
|
292
|
-
for (const entry of messagesRaw) {
|
|
293
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
294
|
-
invalidMessages += 1;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
const message = entry;
|
|
298
|
-
const role = typeof message.role === 'string' && message.role.trim().length > 0
|
|
299
|
-
? message.role.trim().toLowerCase()
|
|
300
|
-
: 'invalid-role';
|
|
301
|
-
bump(roleCounts, role);
|
|
302
|
-
if (firstRole === 'none') {
|
|
303
|
-
firstRole = role;
|
|
304
|
-
}
|
|
305
|
-
lastRole = role;
|
|
306
|
-
const content = message.content;
|
|
307
|
-
if (typeof content === 'string') {
|
|
308
|
-
bump(contentKindCounts, 'string');
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
if (Array.isArray(content)) {
|
|
312
|
-
bump(contentKindCounts, 'array');
|
|
313
|
-
for (const block of content) {
|
|
314
|
-
if (!block || typeof block !== 'object' || Array.isArray(block)) {
|
|
315
|
-
bump(blockTypeCounts, 'invalid');
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
const blockType = typeof block.type === 'string'
|
|
319
|
-
? String(block.type).trim().toLowerCase()
|
|
320
|
-
: 'missing-type';
|
|
321
|
-
bump(blockTypeCounts, blockType || 'missing-type');
|
|
322
|
-
}
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
if (content && typeof content === 'object') {
|
|
326
|
-
bump(contentKindCounts, 'object');
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
bump(contentKindCounts, 'other');
|
|
330
|
-
}
|
|
331
|
-
const joinMap = (map) => ([...map.entries()]
|
|
332
|
-
.sort((left, right) => left[0].localeCompare(right[0]))
|
|
333
|
-
.map(([key, value]) => `${key}:${String(value)}`)
|
|
334
|
-
.join(','));
|
|
335
|
-
const roleSummary = joinMap(roleCounts) || 'none';
|
|
336
|
-
const contentSummary = joinMap(contentKindCounts) || 'none';
|
|
337
|
-
const blockSummary = joinMap(blockTypeCounts) || 'none';
|
|
338
|
-
return [
|
|
339
|
-
`msgShape=roles{${roleSummary}}`,
|
|
340
|
-
`content{${contentSummary}}`,
|
|
341
|
-
`blocks{${blockSummary}}`,
|
|
342
|
-
`firstRole=${firstRole}`,
|
|
343
|
-
`lastRole=${lastRole}`,
|
|
344
|
-
`invalidMsgs=${String(invalidMessages)}`,
|
|
345
|
-
].join(' ');
|
|
346
|
-
}
|
|
347
|
-
function summarizeRequestShape(request) {
|
|
348
|
-
const contentType = (request.headers['content-type'] ?? request.headers['Content-Type'] ?? '').toLowerCase();
|
|
349
|
-
const accept = (request.headers['accept'] ?? request.headers['Accept'] ?? '').toLowerCase();
|
|
350
|
-
const providerHeader = request.headers['x-antseed-provider'] ?? 'none';
|
|
351
|
-
const preferPeerHeader = request.headers['x-antseed-prefer-peer'] ?? 'none';
|
|
352
|
-
const service = extractRequestedService(request) ?? 'none';
|
|
353
|
-
const wantsStreaming = requestWantsStreaming(request.headers, request.body);
|
|
354
|
-
const baseParts = [
|
|
355
|
-
`method=${request.method}`,
|
|
356
|
-
`path=${request.path}`,
|
|
357
|
-
`provider=${providerHeader}`,
|
|
358
|
-
`preferPeer=${preferPeerHeader}`,
|
|
359
|
-
`contentType=${contentType || 'none'}`,
|
|
360
|
-
`accept=${accept || 'none'}`,
|
|
361
|
-
`stream=${String(wantsStreaming)}`,
|
|
362
|
-
`service=${service}`,
|
|
363
|
-
`bodyBytes=${String(request.body.length)}`,
|
|
364
|
-
];
|
|
365
|
-
const jsonBody = decodeJsonBody(request.body);
|
|
366
|
-
if (!jsonBody) {
|
|
367
|
-
return baseParts.join(' ');
|
|
368
|
-
}
|
|
369
|
-
const messagesRaw = jsonBody.messages;
|
|
370
|
-
const toolsRaw = jsonBody.tools;
|
|
371
|
-
const messageCount = Array.isArray(messagesRaw) ? messagesRaw.length : 0;
|
|
372
|
-
const toolCount = Array.isArray(toolsRaw) ? toolsRaw.length : 0;
|
|
373
|
-
const maxTokens = Number(jsonBody.max_tokens ?? jsonBody.maxTokens);
|
|
374
|
-
const keys = Object.keys(jsonBody).sort().join(',');
|
|
375
|
-
baseParts.push(`messages=${String(messageCount)}`);
|
|
376
|
-
baseParts.push(`tools=${String(toolCount)}`);
|
|
377
|
-
if (Number.isFinite(maxTokens) && maxTokens > 0) {
|
|
378
|
-
baseParts.push(`maxTokens=${String(Math.floor(maxTokens))}`);
|
|
379
|
-
}
|
|
380
|
-
if (keys.length > 0) {
|
|
381
|
-
baseParts.push(`keys=[${keys}]`);
|
|
382
|
-
}
|
|
383
|
-
baseParts.push(summarizeMessageShape(messagesRaw));
|
|
384
|
-
return baseParts.join(' ');
|
|
385
|
-
}
|
|
386
|
-
function summarizeErrorResponse(response) {
|
|
387
|
-
const contentType = (response.headers['content-type'] ?? '').toLowerCase();
|
|
388
|
-
if (!response.body || response.body.length === 0) {
|
|
389
|
-
return 'empty response body';
|
|
390
|
-
}
|
|
391
|
-
const raw = new TextDecoder().decode(response.body).trim();
|
|
392
|
-
if (raw.length === 0) {
|
|
393
|
-
return 'empty response body';
|
|
394
|
-
}
|
|
395
|
-
if (contentType.includes('application/json')) {
|
|
396
|
-
try {
|
|
397
|
-
const parsed = JSON.parse(raw);
|
|
398
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
399
|
-
const object = parsed;
|
|
400
|
-
const nestedError = object.error && typeof object.error === 'object' && !Array.isArray(object.error)
|
|
401
|
-
? object.error
|
|
402
|
-
: null;
|
|
403
|
-
const message = ((typeof nestedError?.message === 'string' && nestedError.message)
|
|
404
|
-
|| (typeof object.message === 'string' && object.message)
|
|
405
|
-
|| (typeof object.detail === 'string' && object.detail));
|
|
406
|
-
if (message) {
|
|
407
|
-
return `message="${message}"`;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
catch {
|
|
412
|
-
// fall through to raw snippet
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
const compact = raw.replace(/\s+/g, ' ');
|
|
416
|
-
const maxChars = 280;
|
|
417
|
-
const snippet = compact.length > maxChars ? `${compact.slice(0, maxChars)}...` : compact;
|
|
418
|
-
return `body="${snippet}"`;
|
|
419
|
-
}
|
|
420
|
-
function toFiniteNumberOrNull(value) {
|
|
421
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
422
|
-
}
|
|
423
|
-
function setFiniteNumberHeader(headers, name, value) {
|
|
424
|
-
const finite = toFiniteNumberOrNull(value);
|
|
425
|
-
if (finite !== null) {
|
|
426
|
-
headers[name] = String(finite);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
function setPeerIdentityHeaders(headers, selectedPeer) {
|
|
430
|
-
headers['x-antseed-peer-id'] = selectedPeer.peerId;
|
|
431
|
-
if (selectedPeer.publicAddress) {
|
|
432
|
-
headers['x-antseed-peer-address'] = selectedPeer.publicAddress;
|
|
433
|
-
}
|
|
434
|
-
if (selectedPeer.providers.length > 0) {
|
|
435
|
-
headers['x-antseed-peer-providers'] = selectedPeer.providers.join(',');
|
|
28
|
+
return response;
|
|
436
29
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const providerPricing = peer.providerPricing?.[provider];
|
|
440
|
-
if (providerPricing) {
|
|
441
|
-
const servicePricing = service ? providerPricing.services?.[service] : undefined;
|
|
442
|
-
if (servicePricing) {
|
|
443
|
-
return {
|
|
444
|
-
inputUsdPerMillion: toFiniteNumberOrNull(servicePricing.inputUsdPerMillion),
|
|
445
|
-
outputUsdPerMillion: toFiniteNumberOrNull(servicePricing.outputUsdPerMillion),
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
return {
|
|
449
|
-
inputUsdPerMillion: toFiniteNumberOrNull(providerPricing.defaults.inputUsdPerMillion),
|
|
450
|
-
outputUsdPerMillion: toFiniteNumberOrNull(providerPricing.defaults.outputUsdPerMillion),
|
|
451
|
-
};
|
|
30
|
+
if (!parsed || parsed.error !== 'payment_required') {
|
|
31
|
+
return response;
|
|
452
32
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
33
|
+
const wrappedError = {
|
|
34
|
+
error: {
|
|
35
|
+
...parsed,
|
|
36
|
+
type: 'payment_required',
|
|
37
|
+
message: JSON.stringify(parsed),
|
|
38
|
+
},
|
|
456
39
|
};
|
|
457
|
-
}
|
|
458
|
-
function computeResponseTelemetry(request, responseHeaders, responseBody, selectedPeer) {
|
|
459
|
-
const provider = pickProviderForPeer(selectedPeer, request);
|
|
460
|
-
const service = extractRequestedService(request);
|
|
461
|
-
const pricing = resolvePeerPricing(selectedPeer, provider, service);
|
|
462
|
-
const contentType = (responseHeaders['content-type'] ?? '').toLowerCase();
|
|
463
|
-
const usageFromBody = contentType.includes('text/event-stream')
|
|
464
|
-
? parseSseUsage(responseBody)
|
|
465
|
-
: parseJsonUsage(responseBody);
|
|
466
|
-
let usage;
|
|
467
|
-
if (usageFromBody.totalTokens > 0) {
|
|
468
|
-
usage = {
|
|
469
|
-
inputTokens: usageFromBody.inputTokens,
|
|
470
|
-
outputTokens: usageFromBody.outputTokens,
|
|
471
|
-
totalTokens: usageFromBody.totalTokens,
|
|
472
|
-
source: 'usage',
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
usage = estimateTokensFromBytes(request.body.length, responseBody.length);
|
|
477
|
-
}
|
|
478
|
-
let estimatedCostUsd = null;
|
|
479
|
-
if (pricing.inputUsdPerMillion !== null &&
|
|
480
|
-
pricing.outputUsdPerMillion !== null &&
|
|
481
|
-
Number.isFinite(pricing.inputUsdPerMillion) &&
|
|
482
|
-
Number.isFinite(pricing.outputUsdPerMillion)) {
|
|
483
|
-
estimatedCostUsd =
|
|
484
|
-
(usage.inputTokens * pricing.inputUsdPerMillion + usage.outputTokens * pricing.outputUsdPerMillion) / 1_000_000;
|
|
485
|
-
}
|
|
486
40
|
return {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
inputUsdPerMillion: pricing.inputUsdPerMillion,
|
|
492
|
-
outputUsdPerMillion: pricing.outputUsdPerMillion,
|
|
41
|
+
...response,
|
|
42
|
+
headers: {
|
|
43
|
+
...response.headers,
|
|
44
|
+
'content-type': 'application/json',
|
|
493
45
|
},
|
|
494
|
-
|
|
46
|
+
body: Buffer.from(JSON.stringify(wrappedError)),
|
|
495
47
|
};
|
|
496
48
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
headers['x-antseed-output-tokens'] = String(telemetry.usage.outputTokens);
|
|
515
|
-
headers['x-antseed-total-tokens'] = String(telemetry.usage.totalTokens);
|
|
516
|
-
if (telemetry.estimatedCostUsd !== null && Number.isFinite(telemetry.estimatedCostUsd)) {
|
|
517
|
-
headers['x-antseed-estimated-cost-usd'] = telemetry.estimatedCostUsd.toFixed(6);
|
|
518
|
-
}
|
|
519
|
-
return headers;
|
|
520
|
-
}
|
|
521
|
-
function attachStreamingAntseedHeaders(upstreamHeaders, selectedPeer, requestId) {
|
|
522
|
-
const headers = { ...upstreamHeaders };
|
|
523
|
-
headers['x-antseed-request-id'] = requestId;
|
|
524
|
-
setPeerIdentityHeaders(headers, selectedPeer);
|
|
525
|
-
return headers;
|
|
526
|
-
}
|
|
527
|
-
function requestWantsStreaming(headers, body) {
|
|
528
|
-
const accept = (headers['accept'] ?? headers['Accept'] ?? '').toLowerCase();
|
|
529
|
-
if (accept.includes('text/event-stream')) {
|
|
530
|
-
return true;
|
|
531
|
-
}
|
|
532
|
-
const contentType = (headers['content-type'] ?? headers['Content-Type'] ?? '').toLowerCase();
|
|
533
|
-
if (!contentType.includes('application/json') || body.length === 0) {
|
|
534
|
-
return false;
|
|
535
|
-
}
|
|
536
|
-
try {
|
|
537
|
-
const parsed = JSON.parse(new TextDecoder().decode(body));
|
|
538
|
-
return parsed.stream === true;
|
|
539
|
-
}
|
|
540
|
-
catch {
|
|
541
|
-
return false;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
function isConnectionChurnError(message) {
|
|
545
|
-
return /connection .*?\b(closed|failed)\s+during request\b/i.test(message);
|
|
546
|
-
}
|
|
547
|
-
function isConnectionHealthy(state) {
|
|
548
|
-
if (!state) {
|
|
549
|
-
return false;
|
|
550
|
-
}
|
|
551
|
-
const normalized = String(state).toLowerCase();
|
|
552
|
-
return normalized === 'open' || normalized === 'authenticated' || normalized === 'connecting';
|
|
553
|
-
}
|
|
554
|
-
function extractHostFromAddress(address) {
|
|
555
|
-
const trimmed = address.trim();
|
|
556
|
-
if (trimmed.length === 0)
|
|
557
|
-
return '';
|
|
558
|
-
if (trimmed.startsWith('[')) {
|
|
559
|
-
const end = trimmed.indexOf(']');
|
|
560
|
-
return end > 1 ? trimmed.slice(1, end).toLowerCase() : '';
|
|
561
|
-
}
|
|
562
|
-
const idx = trimmed.lastIndexOf(':');
|
|
563
|
-
if (idx > 0) {
|
|
564
|
-
return trimmed.slice(0, idx).toLowerCase();
|
|
565
|
-
}
|
|
566
|
-
return trimmed.toLowerCase();
|
|
567
|
-
}
|
|
568
|
-
function isLoopbackHost(host) {
|
|
569
|
-
return host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
570
|
-
}
|
|
571
|
-
function isLoopbackPeer(peer) {
|
|
572
|
-
if (!peer.publicAddress) {
|
|
573
|
-
return false;
|
|
574
|
-
}
|
|
575
|
-
const host = extractHostFromAddress(peer.publicAddress);
|
|
576
|
-
return isLoopbackHost(host);
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Rewrite the `service` (and `model` for upstream LLM API compat) fields in a JSON request body.
|
|
580
|
-
* Also updates `content-length` if present in headers.
|
|
581
|
-
* Returns the original body/headers unchanged if the body is not JSON,
|
|
582
|
-
* is empty, or cannot be parsed.
|
|
583
|
-
*/
|
|
584
|
-
export function rewriteServiceInBody(body, headers, service) {
|
|
585
|
-
const contentType = (headers['content-type'] ?? headers['Content-Type'] ?? '').toLowerCase();
|
|
586
|
-
if (!contentType.includes('application/json') || body.length === 0) {
|
|
587
|
-
return { body, headers };
|
|
588
|
-
}
|
|
589
|
-
try {
|
|
590
|
-
const parsed = JSON.parse(new TextDecoder().decode(body));
|
|
591
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
592
|
-
return { body, headers };
|
|
593
|
-
}
|
|
594
|
-
const obj = parsed;
|
|
595
|
-
obj['service'] = service;
|
|
596
|
-
obj['model'] = service;
|
|
597
|
-
const rewritten = new TextEncoder().encode(JSON.stringify(obj));
|
|
598
|
-
const updatedHeaders = { ...headers };
|
|
599
|
-
if ('content-length' in updatedHeaders) {
|
|
600
|
-
updatedHeaders['content-length'] = String(rewritten.length);
|
|
601
|
-
}
|
|
602
|
-
else if ('Content-Length' in updatedHeaders) {
|
|
603
|
-
updatedHeaders['Content-Length'] = String(rewritten.length);
|
|
604
|
-
}
|
|
605
|
-
return { body: rewritten, headers: updatedHeaders };
|
|
606
|
-
}
|
|
607
|
-
catch {
|
|
608
|
-
return { body, headers };
|
|
609
|
-
}
|
|
610
|
-
}
|
|
49
|
+
const PROTOCOL_TRANSFORMS = {
|
|
50
|
+
'anthropic-messages→openai-chat-completions': {
|
|
51
|
+
transformRequest: transformAnthropicMessagesRequestToOpenAIChat,
|
|
52
|
+
adaptResponse: (res, meta) => transformOpenAIChatResponseToAnthropicMessage(res, meta),
|
|
53
|
+
createStreamAdapter: createOpenAIChatToAnthropicStreamingAdapter,
|
|
54
|
+
},
|
|
55
|
+
'openai-responses→openai-chat-completions': {
|
|
56
|
+
transformRequest: transformOpenAIResponsesRequestToOpenAIChat,
|
|
57
|
+
adaptResponse: (res, meta) => transformOpenAIChatResponseToOpenAIResponses(res, meta),
|
|
58
|
+
createStreamAdapter: createOpenAIChatToResponsesStreamingAdapter,
|
|
59
|
+
},
|
|
60
|
+
'openai-chat-completions→openai-responses': {
|
|
61
|
+
transformRequest: transformOpenAIChatRequestToOpenAIResponses,
|
|
62
|
+
adaptResponse: (res, meta) => transformOpenAIResponsesResponseToOpenAIChat(res, meta),
|
|
63
|
+
createStreamAdapter: createOpenAIResponsesToChatStreamingAdapter,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
611
66
|
/**
|
|
612
67
|
* Local HTTP proxy that forwards requests to P2P sellers.
|
|
613
68
|
*
|
|
@@ -625,6 +80,7 @@ export class BuyerProxy {
|
|
|
625
80
|
_pinnedService;
|
|
626
81
|
_stateFileWatcher = null;
|
|
627
82
|
_stateWatchDebounce = null;
|
|
83
|
+
_stateWriteChain = Promise.resolve();
|
|
628
84
|
_cachedPeers = [];
|
|
629
85
|
_cacheLastUpdatedAtMs = 0;
|
|
630
86
|
_cacheMutationEpoch = 0;
|
|
@@ -659,6 +115,9 @@ export class BuyerProxy {
|
|
|
659
115
|
});
|
|
660
116
|
});
|
|
661
117
|
this._startBackgroundRefresh();
|
|
118
|
+
// Trigger initial peer discovery immediately so the desktop can show
|
|
119
|
+
// services without waiting for the first request or 5-minute interval.
|
|
120
|
+
void this._refreshPeersNow().catch(() => { });
|
|
662
121
|
await this._writeStateFile('connected');
|
|
663
122
|
this._watchStateFile();
|
|
664
123
|
}
|
|
@@ -716,38 +175,44 @@ export class BuyerProxy {
|
|
|
716
175
|
// state file unreadable; keep current values
|
|
717
176
|
}
|
|
718
177
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
await mkdir(dir, { recursive: true });
|
|
723
|
-
let existing = {};
|
|
178
|
+
/** Serialised read-modify-write to buyer.state.json. Returns the queued write promise. */
|
|
179
|
+
_mergeStateFile(patch) {
|
|
180
|
+
this._stateWriteChain = this._stateWriteChain.then(async () => {
|
|
724
181
|
try {
|
|
725
|
-
const
|
|
726
|
-
|
|
182
|
+
const dir = join(homedir(), '.antseed');
|
|
183
|
+
await mkdir(dir, { recursive: true });
|
|
184
|
+
let existing = {};
|
|
185
|
+
try {
|
|
186
|
+
const raw = await readFile(BUYER_STATE_FILE, 'utf-8');
|
|
187
|
+
existing = JSON.parse(raw);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// file doesn't exist yet
|
|
191
|
+
}
|
|
192
|
+
const data = { ...existing, ...patch };
|
|
193
|
+
const tmp = join(homedir(), '.antseed', `.buyer.state.${randomUUID()}.json.tmp`);
|
|
194
|
+
await writeFile(tmp, JSON.stringify(data, null, 2));
|
|
195
|
+
await rename(tmp, BUYER_STATE_FILE);
|
|
727
196
|
}
|
|
728
197
|
catch {
|
|
729
|
-
//
|
|
198
|
+
// non-fatal
|
|
730
199
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
catch {
|
|
749
|
-
// non-fatal
|
|
750
|
-
}
|
|
200
|
+
}).catch(() => { });
|
|
201
|
+
return this._stateWriteChain;
|
|
202
|
+
}
|
|
203
|
+
async _writeStateFile(state) {
|
|
204
|
+
// When stopping, preserve whatever pinnedService/pinnedPeerId is already
|
|
205
|
+
// in the file — the debounce may have been cancelled before
|
|
206
|
+
// _reloadSessionOverrides could commit the latest CLI-written values.
|
|
207
|
+
const sessionOverrides = state === 'connected'
|
|
208
|
+
? { pinnedService: this._pinnedService, pinnedPeerId: this._pinnedPeer }
|
|
209
|
+
: {};
|
|
210
|
+
await this._mergeStateFile({
|
|
211
|
+
state,
|
|
212
|
+
pid: process.pid,
|
|
213
|
+
port: this._port,
|
|
214
|
+
...sessionOverrides,
|
|
215
|
+
});
|
|
751
216
|
}
|
|
752
217
|
_startBackgroundRefresh() {
|
|
753
218
|
this._bgRefreshHandle = setInterval(() => {
|
|
@@ -760,6 +225,34 @@ export class BuyerProxy {
|
|
|
760
225
|
this._cachedPeers = incoming;
|
|
761
226
|
this._cacheLastUpdatedAtMs = Date.now();
|
|
762
227
|
this._cacheMutationEpoch += 1;
|
|
228
|
+
this._persistPeersToState();
|
|
229
|
+
}
|
|
230
|
+
_persistPeersToState() {
|
|
231
|
+
// Write discovered peers to buyer.state.json so the dashboard can read them
|
|
232
|
+
// without running its own DHT node.
|
|
233
|
+
const peers = this._cachedPeers.map((p) => {
|
|
234
|
+
// Extract service names from providerPricing entries.
|
|
235
|
+
const services = [];
|
|
236
|
+
if (p.providerPricing) {
|
|
237
|
+
for (const entry of Object.values(p.providerPricing)) {
|
|
238
|
+
if (entry.services) {
|
|
239
|
+
services.push(...Object.keys(entry.services));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
peerId: p.peerId,
|
|
245
|
+
displayName: p.displayName ?? null,
|
|
246
|
+
publicAddress: p.publicAddress ?? null,
|
|
247
|
+
providers: p.providers,
|
|
248
|
+
services,
|
|
249
|
+
defaultInputUsdPerMillion: p.defaultInputUsdPerMillion ?? 0,
|
|
250
|
+
defaultOutputUsdPerMillion: p.defaultOutputUsdPerMillion ?? 0,
|
|
251
|
+
maxConcurrency: p.maxConcurrency ?? 0,
|
|
252
|
+
lastSeen: p.lastSeen,
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
this._mergeStateFile({ discoveredPeers: peers, peersUpdatedAt: Date.now() });
|
|
763
256
|
}
|
|
764
257
|
_evictPeer(peerId) {
|
|
765
258
|
const before = this._cachedPeers.length;
|
|
@@ -813,55 +306,7 @@ export class BuyerProxy {
|
|
|
813
306
|
explicitProvider ?? 'auto-provider',
|
|
814
307
|
].join('|');
|
|
815
308
|
}
|
|
816
|
-
async _readLocalSeederFallback() {
|
|
817
|
-
try {
|
|
818
|
-
const raw = await readFile(DAEMON_STATE_FILE, 'utf-8');
|
|
819
|
-
const parsed = JSON.parse(raw);
|
|
820
|
-
if (parsed.state !== 'seeding')
|
|
821
|
-
return null;
|
|
822
|
-
if (typeof parsed.peerId !== 'string' || !/^[0-9a-f]{64}$/i.test(parsed.peerId))
|
|
823
|
-
return null;
|
|
824
|
-
const signalingPort = Number(parsed.signalingPort);
|
|
825
|
-
if (!Number.isFinite(signalingPort) || signalingPort <= 0 || signalingPort > 65535)
|
|
826
|
-
return null;
|
|
827
|
-
const pid = Number(parsed.pid);
|
|
828
|
-
if (Number.isFinite(pid) && pid > 0) {
|
|
829
|
-
try {
|
|
830
|
-
process.kill(Math.floor(pid), 0);
|
|
831
|
-
}
|
|
832
|
-
catch {
|
|
833
|
-
return null;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
const providers = typeof parsed.provider === 'string' && parsed.provider.trim().length > 0
|
|
837
|
-
? [parsed.provider.trim()]
|
|
838
|
-
: [];
|
|
839
|
-
const defaultInputUsdPerMillion = Number(parsed.defaultInputUsdPerMillion);
|
|
840
|
-
const defaultOutputUsdPerMillion = Number(parsed.defaultOutputUsdPerMillion);
|
|
841
|
-
const providerPricing = parsed.providerPricing && typeof parsed.providerPricing === 'object'
|
|
842
|
-
? parsed.providerPricing
|
|
843
|
-
: undefined;
|
|
844
|
-
const peerId = parsed.peerId.toLowerCase();
|
|
845
|
-
return {
|
|
846
|
-
peerId: peerId,
|
|
847
|
-
lastSeen: Date.now(),
|
|
848
|
-
publicAddress: `127.0.0.1:${Math.floor(signalingPort)}`,
|
|
849
|
-
providers,
|
|
850
|
-
defaultInputUsdPerMillion: Number.isFinite(defaultInputUsdPerMillion) ? defaultInputUsdPerMillion : 0,
|
|
851
|
-
defaultOutputUsdPerMillion: Number.isFinite(defaultOutputUsdPerMillion) ? defaultOutputUsdPerMillion : 0,
|
|
852
|
-
...(providerPricing ? { providerPricing } : {}),
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
catch {
|
|
856
|
-
return null;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
309
|
async _discoverPeersFromNetwork() {
|
|
860
|
-
const localSeeder = await this._readLocalSeederFallback();
|
|
861
|
-
if (localSeeder) {
|
|
862
|
-
log(`Using local seeder ${localSeeder.peerId.slice(0, 12)}... @ ${localSeeder.publicAddress} (skipping DHT lookup)`);
|
|
863
|
-
return [localSeeder];
|
|
864
|
-
}
|
|
865
310
|
log('Discovering peers via DHT...');
|
|
866
311
|
const peers = await this._node.discoverPeers();
|
|
867
312
|
if (peers.length > 0) {
|
|
@@ -1108,28 +553,13 @@ export class BuyerProxy {
|
|
|
1108
553
|
}
|
|
1109
554
|
if (routingPeers.length === 0) {
|
|
1110
555
|
const diagnostics = this._formatPeerSelectionDiagnostics(discoveredPeers);
|
|
1111
|
-
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
1112
|
-
if (requestProtocol) {
|
|
1113
|
-
const protocolLabel = requestProtocol;
|
|
1114
|
-
const providerLabel = explicitProvider ? ` for provider "${explicitProvider}"` : '';
|
|
1115
|
-
res.end(`No peers support ${protocolLabel}${providerLabel}. ${diagnostics}`);
|
|
1116
|
-
}
|
|
1117
|
-
else {
|
|
1118
|
-
res.end(`No peers advertise provider "${explicitProvider}". ${diagnostics}`);
|
|
1119
|
-
}
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
if (routingPeers.length === 0) {
|
|
1123
|
-
const diagnostics = this._formatPeerSelectionDiagnostics(discoveredPeers);
|
|
1124
|
-
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
1125
556
|
const providerLabel = explicitProvider ? ` for provider "${explicitProvider}"` : '';
|
|
557
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
1126
558
|
res.end(`No peers support ${requestProtocol ?? 'this request'}${providerLabel}. ${diagnostics}`);
|
|
1127
559
|
return;
|
|
1128
560
|
}
|
|
1129
561
|
log(`Routing candidates: ${routingPeers.length} peer(s)`);
|
|
1130
|
-
// Select peer: explicit pin bypasses the router (and retry)
|
|
1131
562
|
const router = this._node.router;
|
|
1132
|
-
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
1133
563
|
if (explicitPeerId) {
|
|
1134
564
|
// Pinned peers must use fresh discovery data so IP changes are picked up.
|
|
1135
565
|
// Safe with the hasForcedRefresh guard: if an earlier refresh already ran
|
|
@@ -1243,7 +673,7 @@ export class BuyerProxy {
|
|
|
1243
673
|
}
|
|
1244
674
|
if (!selectedPeer) {
|
|
1245
675
|
selectedPeer = router
|
|
1246
|
-
? router.selectPeer(serializedReq, availableCandidates)
|
|
676
|
+
? (router.selectPeer(serializedReq, availableCandidates) ?? availableCandidates[0] ?? null)
|
|
1247
677
|
: availableCandidates[0] ?? null;
|
|
1248
678
|
}
|
|
1249
679
|
if (!selectedPeer)
|
|
@@ -1298,88 +728,35 @@ export class BuyerProxy {
|
|
|
1298
728
|
let adaptResponse = null;
|
|
1299
729
|
let streamResponseAdapter = null;
|
|
1300
730
|
if (selectedRoutePlan.selection?.requiresTransform) {
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
res.end('Failed to transform Anthropic request for selected provider protocol');
|
|
1308
|
-
return { done: true };
|
|
1309
|
-
}
|
|
1310
|
-
requestForPeer = {
|
|
1311
|
-
...transformed.request,
|
|
1312
|
-
headers: {
|
|
1313
|
-
...transformed.request.headers,
|
|
1314
|
-
'x-antseed-provider': selectedRoutePlan.provider,
|
|
1315
|
-
},
|
|
1316
|
-
};
|
|
1317
|
-
adaptResponse = (response) => transformOpenAIChatResponseToAnthropicMessage(response, {
|
|
1318
|
-
streamRequested: transformed.streamRequested,
|
|
1319
|
-
fallbackModel: transformed.requestedModel,
|
|
1320
|
-
});
|
|
1321
|
-
if (transformed.streamRequested) {
|
|
1322
|
-
streamResponseAdapter = createOpenAIChatToAnthropicStreamingAdapter({
|
|
1323
|
-
fallbackModel: transformed.requestedModel,
|
|
1324
|
-
});
|
|
1325
|
-
}
|
|
731
|
+
const transformKey = `${requestProtocol}→${selectedRoutePlan.selection.targetProtocol}`;
|
|
732
|
+
const strategy = PROTOCOL_TRANSFORMS[transformKey];
|
|
733
|
+
if (!strategy) {
|
|
734
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
735
|
+
res.end('Unsupported protocol transformation path');
|
|
736
|
+
return { done: true };
|
|
1326
737
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
res.end('Failed to transform Responses API request for selected provider protocol');
|
|
1334
|
-
return { done: true };
|
|
1335
|
-
}
|
|
1336
|
-
requestForPeer = {
|
|
1337
|
-
...transformed.request,
|
|
1338
|
-
headers: {
|
|
1339
|
-
...transformed.request.headers,
|
|
1340
|
-
'x-antseed-provider': selectedRoutePlan.provider,
|
|
1341
|
-
},
|
|
1342
|
-
};
|
|
1343
|
-
adaptResponse = (response) => transformOpenAIChatResponseToOpenAIResponses(response, {
|
|
1344
|
-
fallbackModel: transformed.requestedModel,
|
|
1345
|
-
streamRequested: transformed.streamRequested,
|
|
1346
|
-
});
|
|
1347
|
-
if (transformed.streamRequested) {
|
|
1348
|
-
streamResponseAdapter = createOpenAIChatToResponsesStreamingAdapter({
|
|
1349
|
-
fallbackModel: transformed.requestedModel,
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
738
|
+
log(`Applying protocol adapter ${transformKey} via provider "${selectedRoutePlan.provider}"`);
|
|
739
|
+
const transformed = strategy.transformRequest(requestForPeer);
|
|
740
|
+
if (!transformed) {
|
|
741
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
742
|
+
res.end(`Failed to transform request for ${transformKey}`);
|
|
743
|
+
return { done: true };
|
|
1352
744
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
'x-antseed-provider': selectedRoutePlan.provider,
|
|
1367
|
-
},
|
|
1368
|
-
};
|
|
1369
|
-
adaptResponse = (response) => transformOpenAIResponsesResponseToOpenAIChat(response, {
|
|
745
|
+
requestForPeer = {
|
|
746
|
+
...transformed.request,
|
|
747
|
+
headers: {
|
|
748
|
+
...transformed.request.headers,
|
|
749
|
+
'x-antseed-provider': selectedRoutePlan.provider,
|
|
750
|
+
},
|
|
751
|
+
};
|
|
752
|
+
adaptResponse = (response) => strategy.adaptResponse(response, {
|
|
753
|
+
streamRequested: transformed.streamRequested,
|
|
754
|
+
fallbackModel: transformed.requestedModel,
|
|
755
|
+
});
|
|
756
|
+
if (transformed.streamRequested) {
|
|
757
|
+
streamResponseAdapter = strategy.createStreamAdapter({
|
|
1370
758
|
fallbackModel: transformed.requestedModel,
|
|
1371
|
-
streamRequested: transformed.streamRequested,
|
|
1372
759
|
});
|
|
1373
|
-
if (transformed.streamRequested) {
|
|
1374
|
-
streamResponseAdapter = createOpenAIResponsesToChatStreamingAdapter({
|
|
1375
|
-
fallbackModel: transformed.requestedModel,
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
else {
|
|
1380
|
-
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
1381
|
-
res.end('Unsupported protocol transformation path');
|
|
1382
|
-
return { done: true };
|
|
1383
760
|
}
|
|
1384
761
|
}
|
|
1385
762
|
if (DEBUG()) {
|
|
@@ -1429,6 +806,7 @@ export class BuyerProxy {
|
|
|
1429
806
|
if (!streamed && adaptResponse) {
|
|
1430
807
|
responseForClient = adaptResponse(response);
|
|
1431
808
|
}
|
|
809
|
+
responseForClient = adaptOpenAICompatibleErrorResponse(responseForClient, requestProtocol);
|
|
1432
810
|
const latencyMs = Date.now() - startTime;
|
|
1433
811
|
log(`Response: ${responseForClient.statusCode} (${latencyMs}ms, ${responseForClient.body.length} bytes)`);
|
|
1434
812
|
if (responseForClient.statusCode >= 400) {
|
|
@@ -1480,6 +858,7 @@ export class BuyerProxy {
|
|
|
1480
858
|
if (adaptResponse) {
|
|
1481
859
|
response = adaptResponse(response);
|
|
1482
860
|
}
|
|
861
|
+
response = adaptOpenAICompatibleErrorResponse(response, requestProtocol);
|
|
1483
862
|
const latencyMs = Date.now() - startTime;
|
|
1484
863
|
log(`Response: ${response.statusCode} (${latencyMs}ms, ${response.body.length} bytes)`);
|
|
1485
864
|
if (response.statusCode >= 400) {
|