@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.
Files changed (76) hide show
  1. package/dist/cli/commands/balance.js +6 -6
  2. package/dist/cli/commands/balance.js.map +1 -1
  3. package/dist/cli/commands/connect.d.ts.map +1 -1
  4. package/dist/cli/commands/connect.js +30 -20
  5. package/dist/cli/commands/connect.js.map +1 -1
  6. package/dist/cli/commands/deposit.d.ts.map +1 -1
  7. package/dist/cli/commands/deposit.js +8 -18
  8. package/dist/cli/commands/deposit.js.map +1 -1
  9. package/dist/cli/commands/emissions.d.ts +3 -0
  10. package/dist/cli/commands/emissions.d.ts.map +1 -0
  11. package/dist/cli/commands/emissions.js +84 -0
  12. package/dist/cli/commands/emissions.js.map +1 -0
  13. package/dist/cli/commands/payments.d.ts +3 -0
  14. package/dist/cli/commands/payments.d.ts.map +1 -0
  15. package/dist/cli/commands/payments.js +26 -0
  16. package/dist/cli/commands/payments.js.map +1 -0
  17. package/dist/cli/commands/register.d.ts +3 -0
  18. package/dist/cli/commands/register.d.ts.map +1 -0
  19. package/dist/cli/commands/register.js +41 -0
  20. package/dist/cli/commands/register.js.map +1 -0
  21. package/dist/cli/commands/reputation.d.ts +3 -0
  22. package/dist/cli/commands/reputation.d.ts.map +1 -0
  23. package/dist/cli/commands/reputation.js +94 -0
  24. package/dist/cli/commands/reputation.js.map +1 -0
  25. package/dist/cli/commands/seed.d.ts.map +1 -1
  26. package/dist/cli/commands/seed.js +20 -7
  27. package/dist/cli/commands/seed.js.map +1 -1
  28. package/dist/cli/commands/sessions.d.ts +3 -0
  29. package/dist/cli/commands/sessions.d.ts.map +1 -0
  30. package/dist/cli/commands/sessions.js +79 -0
  31. package/dist/cli/commands/sessions.js.map +1 -0
  32. package/dist/cli/commands/setup.d.ts +3 -0
  33. package/dist/cli/commands/setup.d.ts.map +1 -0
  34. package/dist/cli/commands/setup.js +83 -0
  35. package/dist/cli/commands/setup.js.map +1 -0
  36. package/dist/cli/commands/stake.d.ts +3 -0
  37. package/dist/cli/commands/stake.d.ts.map +1 -0
  38. package/dist/cli/commands/stake.js +73 -0
  39. package/dist/cli/commands/stake.js.map +1 -0
  40. package/dist/cli/commands/subscribe.d.ts +3 -0
  41. package/dist/cli/commands/subscribe.d.ts.map +1 -0
  42. package/dist/cli/commands/subscribe.js +96 -0
  43. package/dist/cli/commands/subscribe.js.map +1 -0
  44. package/dist/cli/commands/withdraw.js +2 -2
  45. package/dist/cli/commands/withdraw.js.map +1 -1
  46. package/dist/cli/index.js +16 -0
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/cli/payment-utils.d.ts +45 -0
  49. package/dist/cli/payment-utils.d.ts.map +1 -0
  50. package/dist/cli/payment-utils.js +102 -0
  51. package/dist/cli/payment-utils.js.map +1 -0
  52. package/dist/config/defaults.d.ts.map +1 -1
  53. package/dist/config/defaults.js +3 -0
  54. package/dist/config/defaults.js.map +1 -1
  55. package/dist/config/loader.d.ts.map +1 -1
  56. package/dist/config/loader.js +3 -0
  57. package/dist/config/loader.js.map +1 -1
  58. package/dist/config/types.d.ts +9 -0
  59. package/dist/config/types.d.ts.map +1 -1
  60. package/dist/proxy/buyer-proxy.d.ts +7 -23
  61. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  62. package/dist/proxy/buyer-proxy.js +141 -762
  63. package/dist/proxy/buyer-proxy.js.map +1 -1
  64. package/dist/proxy/request-utils.d.ts +21 -0
  65. package/dist/proxy/request-utils.d.ts.map +1 -0
  66. package/dist/proxy/request-utils.js +237 -0
  67. package/dist/proxy/request-utils.js.map +1 -0
  68. package/dist/proxy/routing.d.ts +17 -0
  69. package/dist/proxy/routing.d.ts.map +1 -0
  70. package/dist/proxy/routing.js +115 -0
  71. package/dist/proxy/routing.js.map +1 -0
  72. package/dist/proxy/telemetry.d.ts +23 -0
  73. package/dist/proxy/telemetry.d.ts.map +1 -0
  74. package/dist/proxy/telemetry.js +227 -0
  75. package/dist/proxy/telemetry.js.map +1 -0
  76. 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, inferProviderDefaultServiceApiProtocols, selectTargetProtocolForRequest, transformAnthropicMessagesRequestToOpenAIChat, transformOpenAIChatRequestToOpenAIResponses, transformOpenAIChatResponseToAnthropicMessage, transformOpenAIChatResponseToOpenAIResponses, transformOpenAIResponsesRequestToOpenAIChat, transformOpenAIResponsesResponseToOpenAIChat, } from './service-api-adapter.js';
8
- const DAEMON_STATE_FILE = join(homedir(), '.antseed', 'daemon.state.json');
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 DEBUG = () => ['1', 'true', 'yes', 'on'].includes((process.env['ANTSEED_DEBUG'] ?? '').trim().toLowerCase());
11
- function log(...args) {
12
- if (DEBUG())
13
- console.log('[proxy]', ...args);
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
- function decodeJsonBody(body) {
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
- const parsed = JSON.parse(new TextDecoder().decode(body));
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 null;
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
- function resolvePeerPricing(peer, provider, service) {
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
- return {
454
- inputUsdPerMillion: toFiniteNumberOrNull(peer.defaultInputUsdPerMillion),
455
- outputUsdPerMillion: toFiniteNumberOrNull(peer.defaultOutputUsdPerMillion),
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
- usage,
488
- pricing: {
489
- provider,
490
- service,
491
- inputUsdPerMillion: pricing.inputUsdPerMillion,
492
- outputUsdPerMillion: pricing.outputUsdPerMillion,
41
+ ...response,
42
+ headers: {
43
+ ...response.headers,
44
+ 'content-type': 'application/json',
493
45
  },
494
- estimatedCostUsd,
46
+ body: Buffer.from(JSON.stringify(wrappedError)),
495
47
  };
496
48
  }
497
- function attachAntseedTelemetryHeaders(upstreamHeaders, selectedPeer, telemetry, requestId, latencyMs) {
498
- const headers = { ...upstreamHeaders };
499
- headers['x-antseed-request-id'] = requestId;
500
- headers['x-antseed-latency-ms'] = String(Math.max(0, Math.floor(latencyMs)));
501
- setPeerIdentityHeaders(headers, selectedPeer);
502
- setFiniteNumberHeader(headers, 'x-antseed-peer-reputation', selectedPeer.reputationScore);
503
- setFiniteNumberHeader(headers, 'x-antseed-peer-trust-score', selectedPeer.trustScore);
504
- setFiniteNumberHeader(headers, 'x-antseed-peer-current-load', selectedPeer.currentLoad);
505
- setFiniteNumberHeader(headers, 'x-antseed-peer-max-concurrency', selectedPeer.maxConcurrency);
506
- headers['x-antseed-provider'] = telemetry.pricing.provider;
507
- if (telemetry.pricing.service) {
508
- headers['x-antseed-service'] = telemetry.pricing.service;
509
- }
510
- setFiniteNumberHeader(headers, 'x-antseed-input-usd-per-million', telemetry.pricing.inputUsdPerMillion);
511
- setFiniteNumberHeader(headers, 'x-antseed-output-usd-per-million', telemetry.pricing.outputUsdPerMillion);
512
- headers['x-antseed-token-source'] = telemetry.usage.source;
513
- headers['x-antseed-input-tokens'] = String(telemetry.usage.inputTokens);
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
- async _writeStateFile(state) {
720
- try {
721
- const dir = join(homedir(), '.antseed');
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 raw = await readFile(BUYER_STATE_FILE, 'utf-8');
726
- existing = JSON.parse(raw);
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
- // file doesn't exist yet
198
+ // non-fatal
730
199
  }
731
- // When stopping, preserve whatever pinnedService/pinnedPeerId is already
732
- // in the file — the debounce may have been cancelled before
733
- // _reloadSessionOverrides could commit the latest CLI-written values.
734
- const sessionOverrides = state === 'connected'
735
- ? { pinnedService: this._pinnedService, pinnedPeerId: this._pinnedPeer }
736
- : {};
737
- const data = {
738
- ...existing,
739
- state,
740
- pid: process.pid,
741
- port: this._port,
742
- ...sessionOverrides,
743
- };
744
- const tmp = join(homedir(), '.antseed', `.buyer.state.${randomUUID()}.json.tmp`);
745
- await writeFile(tmp, JSON.stringify(data, null, 2));
746
- await rename(tmp, BUYER_STATE_FILE);
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
- if (requestProtocol === 'anthropic-messages'
1302
- && selectedRoutePlan.selection.targetProtocol === 'openai-chat-completions') {
1303
- log(`Applying protocol adapter anthropic-messages -> openai-chat-completions via provider "${selectedRoutePlan.provider}"`);
1304
- const transformed = transformAnthropicMessagesRequestToOpenAIChat(requestForPeer);
1305
- if (!transformed) {
1306
- res.writeHead(502, { 'content-type': 'text/plain' });
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
- else if (requestProtocol === 'openai-responses'
1328
- && selectedRoutePlan.selection.targetProtocol === 'openai-chat-completions') {
1329
- log(`Applying protocol adapter openai-responses -> openai-chat-completions via provider "${selectedRoutePlan.provider}"`);
1330
- const transformed = transformOpenAIResponsesRequestToOpenAIChat(requestForPeer);
1331
- if (!transformed) {
1332
- res.writeHead(502, { 'content-type': 'text/plain' });
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
- else if (requestProtocol === 'openai-chat-completions'
1354
- && selectedRoutePlan.selection.targetProtocol === 'openai-responses') {
1355
- log(`Applying protocol adapter openai-chat-completions -> openai-responses via provider "${selectedRoutePlan.provider}"`);
1356
- const transformed = transformOpenAIChatRequestToOpenAIResponses(requestForPeer);
1357
- if (!transformed) {
1358
- res.writeHead(502, { 'content-type': 'text/plain' });
1359
- res.end('Failed to transform Chat Completions request for selected provider protocol');
1360
- return { done: true };
1361
- }
1362
- requestForPeer = {
1363
- ...transformed.request,
1364
- headers: {
1365
- ...transformed.request.headers,
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) {