@antseed/cli 0.1.152 → 0.1.154

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 (45) hide show
  1. package/dist/cli/commands/buyer/daemon.d.ts +3 -0
  2. package/dist/cli/commands/buyer/daemon.d.ts.map +1 -1
  3. package/dist/cli/commands/buyer/daemon.js +1 -0
  4. package/dist/cli/commands/buyer/daemon.js.map +1 -1
  5. package/dist/cli/commands/buyer/start.d.ts.map +1 -1
  6. package/dist/cli/commands/buyer/start.js +20 -4
  7. package/dist/cli/commands/buyer/start.js.map +1 -1
  8. package/dist/cli/commands/seller/start.d.ts.map +1 -1
  9. package/dist/cli/commands/seller/start.js +51 -1
  10. package/dist/cli/commands/seller/start.js.map +1 -1
  11. package/dist/cli/commands/seller/status.d.ts.map +1 -1
  12. package/dist/cli/commands/seller/status.js +4 -0
  13. package/dist/cli/commands/seller/status.js.map +1 -1
  14. package/dist/config/loader.d.ts.map +1 -1
  15. package/dist/config/loader.js +25 -0
  16. package/dist/config/loader.js.map +1 -1
  17. package/dist/config/loader.test.js +28 -0
  18. package/dist/config/loader.test.js.map +1 -1
  19. package/dist/config/types.d.ts +17 -0
  20. package/dist/config/types.d.ts.map +1 -1
  21. package/dist/config/validation.d.ts.map +1 -1
  22. package/dist/config/validation.js +14 -0
  23. package/dist/config/validation.js.map +1 -1
  24. package/dist/proxy/buyer-proxy.d.ts +15 -2
  25. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  26. package/dist/proxy/buyer-proxy.js +92 -21
  27. package/dist/proxy/buyer-proxy.js.map +1 -1
  28. package/dist/proxy/buyer-proxy.test.js +155 -3
  29. package/dist/proxy/buyer-proxy.test.js.map +1 -1
  30. package/dist/proxy/conversation-identity.d.ts.map +1 -1
  31. package/dist/proxy/conversation-identity.js +42 -2
  32. package/dist/proxy/conversation-identity.js.map +1 -1
  33. package/dist/proxy/conversation-identity.test.js +27 -0
  34. package/dist/proxy/conversation-identity.test.js.map +1 -1
  35. package/dist/proxy/routing-parameters.test.js +12 -1
  36. package/dist/proxy/routing-parameters.test.js.map +1 -1
  37. package/dist/proxy/routing.d.ts +8 -0
  38. package/dist/proxy/routing.d.ts.map +1 -1
  39. package/dist/proxy/routing.js +42 -2
  40. package/dist/proxy/routing.js.map +1 -1
  41. package/dist/status/node-status.d.ts +2 -0
  42. package/dist/status/node-status.d.ts.map +1 -1
  43. package/dist/status/node-status.js +5 -0
  44. package/dist/status/node-status.js.map +1 -1
  45. package/package.json +4 -4
@@ -8,7 +8,7 @@ import { canonicalModelKey } from '@antseed/node/model-identity';
8
8
  import { createStreamingAdapter, detectRequestServiceApiProtocol, transformRequest, transformResponse, } from './service-api-adapter.js';
9
9
  import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, parsePeerPinnedService, rewritePeerPinnedServiceInBody, substituteRoutedModelAlias, overrideRoutedModelInBody, ROUTED_MODEL_ALIAS, SYSTEM_PROXY_SOURCE_HEADER, SYSTEM_ROUTED_MODEL_HEADER, normalizePeerId, } from './request-utils.js';
10
10
  import { buildNetworkModels, effectiveModelReputationScore, normalizedModelReputationScore, parseModelTypeFilter, } from './network-models.js';
11
- import { findUnannouncedRequestParameters, findAdvertisedServiceOffer, getExplicitProviderOverride, getExplicitPeerIdOverride, resolvePeerRoutePlan, selectCandidatePeersForRouting, } from './routing.js';
11
+ import { findMissingRequiredParameters, findUnannouncedRequestParameters, findAdvertisedServiceOffer, getExplicitProviderOverride, getExplicitPeerIdOverride, parseRequiredParametersHeader, REQUIRED_PARAMETERS_HEADER, resolvePeerRoutePlan, selectCandidatePeersForRouting, } from './routing.js';
12
12
  import { computeResponseTelemetry, attachAntseedTelemetryHeaders, attachStreamingAntseedHeaders, } from './telemetry.js';
13
13
  import { DEFAULT_BUYER_PEER_REFRESH_INTERVAL_MS } from '../config/defaults.js';
14
14
  import { extractConversationIdentity, extractFirstUserSnippet, isCompletionRequestPath, isTitleGenerationRequest, parseRequestBodyObject, } from './conversation-identity.js';
@@ -21,6 +21,11 @@ import { loadConfig } from '../config/loader.js';
21
21
  // Re-export for backward compatibility (used by tests and other consumers)
22
22
  export { selectCandidatePeersForRouting } from './routing.js';
23
23
  export { parsePeerPinnedService, rewritePeerPinnedServiceInBody, substituteRoutedModelAlias, ROUTED_MODEL_ALIAS } from './request-utils.js';
24
+ const WATCHER_ABSENCE_ERRORS = {
25
+ 'payments-disabled': 'Deposit watcher unavailable — payments are disabled on this buyer.',
26
+ 'no-deposit-relay': 'Deposit watcher unavailable — this chain has no deposit relay.',
27
+ 'external-daemon': 'Deposit watcher unavailable — another daemon owns the proxy port and runs the watcher.',
28
+ };
24
29
  const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
25
30
  const MODEL_RATE_LIMIT_MAX_ATTEMPTS_PER_PEER = 3;
26
31
  const MODEL_RATE_LIMIT_RETRY_DELAYS_MS = [250, 750];
@@ -227,7 +232,7 @@ function adaptBuyerFaultErrorResponse(response, requestProtocol) {
227
232
  body: Buffer.from(JSON.stringify(body)),
228
233
  };
229
234
  }
230
- function sanitizePeerBuyerFaultMarker(response) {
235
+ export function sanitizePeerBuyerFaultMarker(response) {
231
236
  if (response.statusCode < 400)
232
237
  return response;
233
238
  let parsed;
@@ -237,18 +242,28 @@ function sanitizePeerBuyerFaultMarker(response) {
237
242
  catch {
238
243
  return response;
239
244
  }
245
+ // Scrub the whole tree, not just the top level: a seller nesting the marker
246
+ // deeper (e.g. error.details.code) must not be able to have its failure
247
+ // classified as buyer-fault downstream. Iterate to avoid recursion limits
248
+ // without leaving deeply nested markers trusted by downstream clients.
240
249
  let changed = false;
241
- const scrub = (record) => {
250
+ const pending = [parsed];
251
+ while (pending.length > 0) {
252
+ const value = pending.pop();
253
+ if (value === null || typeof value !== 'object')
254
+ continue;
255
+ if (Array.isArray(value)) {
256
+ pending.push(...value);
257
+ continue;
258
+ }
259
+ const record = value;
242
260
  for (const key of ['code', 'type', 'errorCode']) {
243
261
  if (record[key] === ANTSEED_BUYER_FAULT_ERROR_CODE) {
244
262
  record[key] = 'upstream_error';
245
263
  changed = true;
246
264
  }
247
265
  }
248
- };
249
- scrub(parsed);
250
- if (parsed.error && typeof parsed.error === 'object' && !Array.isArray(parsed.error)) {
251
- scrub(parsed.error);
266
+ pending.push(...Object.values(record));
252
267
  }
253
268
  return changed
254
269
  ? { ...response, body: Buffer.from(JSON.stringify(parsed)) }
@@ -615,6 +630,8 @@ export class BuyerProxy {
615
630
  * plane instead of running a second signer against the same wallet.
616
631
  */
617
632
  _depositWatcher = null;
633
+ /** Why no watcher is attached, so UIs can show the actual cause. */
634
+ _depositWatcherAbsence = null;
618
635
  /**
619
636
  * requestId -> the conversation that issued it, so the node's per-request
620
637
  * spend events can be attributed to a chat. Only the proxy knows this
@@ -708,9 +725,13 @@ export class BuyerProxy {
708
725
  this._requestConversations.delete(oldest);
709
726
  }
710
727
  }
711
- /** Attach the hot-wallet deposit watcher (owned by `buyer start`). */
712
- setDepositWatcher(watcher) {
728
+ /**
729
+ * Attach the hot-wallet deposit watcher (owned by `buyer start`), or record
730
+ * why none runs so the status endpoint can report the cause.
731
+ */
732
+ setDepositWatcher(watcher, absenceReason = null) {
713
733
  this._depositWatcher = watcher;
734
+ this._depositWatcherAbsence = watcher ? null : absenceReason;
714
735
  }
715
736
  /** Latest relayer receipt for a sweep authNonce, if one has arrived. */
716
737
  getSweepReceipt(authNonce) {
@@ -1706,6 +1727,13 @@ export class BuyerProxy {
1706
1727
  res.end(JSON.stringify({
1707
1728
  ok: true,
1708
1729
  watcher: this._depositWatcher !== null,
1730
+ // Why no watcher runs (null while one is attached) — lets UIs say
1731
+ // "payments are disabled" vs "this chain has no deposit relay".
1732
+ reason: this._depositWatcher ? null : this._depositWatcherAbsence,
1733
+ // Live payments health: configured/active flags + chain-RPC
1734
+ // reachability from the node's background monitor. Optional call —
1735
+ // tolerate an older @antseed/node without it.
1736
+ payments: this._node.getPaymentsStatus?.() ?? null,
1709
1737
  status: this._depositWatcher?.status() ?? null,
1710
1738
  }));
1711
1739
  return;
@@ -1739,11 +1767,10 @@ export class BuyerProxy {
1739
1767
  }
1740
1768
  const watcher = this._depositWatcher;
1741
1769
  if (!watcher) {
1770
+ const reason = this._depositWatcherAbsence;
1771
+ const error = (reason && WATCHER_ABSENCE_ERRORS[reason]) ?? 'Deposit watcher unavailable.';
1742
1772
  res.writeHead(503, { 'content-type': 'application/json' });
1743
- res.end(JSON.stringify({
1744
- ok: false,
1745
- error: 'Deposit watcher unavailable — payments are disabled or this chain has no deposit relay.',
1746
- }));
1773
+ res.end(JSON.stringify({ ok: false, reason, error }));
1747
1774
  return;
1748
1775
  }
1749
1776
  if (mode === 'active')
@@ -1890,6 +1917,19 @@ export class BuyerProxy {
1890
1917
  headers,
1891
1918
  body: new Uint8Array(body),
1892
1919
  };
1920
+ const requiredParameters = parseRequiredParametersHeader(serializedReq.headers[REQUIRED_PARAMETERS_HEADER]);
1921
+ if (requiredParameters === null) {
1922
+ res.writeHead(400, { 'content-type': 'application/json' });
1923
+ res.end(JSON.stringify({
1924
+ error: {
1925
+ type: 'invalid_request_error',
1926
+ code: 'invalid_required_parameters',
1927
+ message: `${REQUIRED_PARAMETERS_HEADER} must contain at most 16 comma-separated parameter names.`,
1928
+ param: REQUIRED_PARAMETERS_HEADER,
1929
+ },
1930
+ }));
1931
+ return;
1932
+ }
1893
1933
  // Snapshot the session overrides before any await so a concurrent
1894
1934
  // _reloadSessionOverrides() cannot change routing mid-request.
1895
1935
  const effectivePinnedPeer = this._pinnedPeer;
@@ -2088,6 +2128,14 @@ export class BuyerProxy {
2088
2128
  const offer = findAdvertisedServiceOffer(peer, plan.provider, plan.serviceId);
2089
2129
  if (!offer)
2090
2130
  return null;
2131
+ const missingRequired = plan.selection?.requiresTransform
2132
+ ? requiredParameters
2133
+ : findMissingRequiredParameters(peer, plan.provider, plan.serviceId, requiredParameters);
2134
+ if (missingRequired.length > 0) {
2135
+ log(`Capability filter: peer ${peer.peerId.slice(0, 12)}... service="${plan.serviceId}" `
2136
+ + `missing required parameter(s): ${missingRequired.join(', ')}`);
2137
+ return null;
2138
+ }
2091
2139
  const requestForPolicy = withRoutedModel(serializedReq, plan.serviceId);
2092
2140
  if (!peerAllowedByPolicy(policyRouter, requestForPolicy, peer))
2093
2141
  return null;
@@ -2137,14 +2185,22 @@ export class BuyerProxy {
2137
2185
  }
2138
2186
  }
2139
2187
  if (candidates.length === 0) {
2140
- res.writeHead(502, { 'content-type': 'application/json' });
2188
+ const capabilityRequired = requiredParameters.length > 0;
2189
+ res.writeHead(capabilityRequired ? 422 : 502, { 'content-type': 'application/json' });
2141
2190
  res.end(JSON.stringify({
2142
- error: {
2143
- type: 'model_not_found',
2144
- code: 'model_not_found',
2145
- message: `No policy-allowed peer currently serves model "${requestedService}".`,
2146
- param: 'model',
2147
- },
2191
+ error: capabilityRequired
2192
+ ? {
2193
+ type: 'required_capability_unavailable',
2194
+ code: 'required_capability_unavailable',
2195
+ message: `No policy-allowed seller for model "${requestedService}" advertises required parameter(s): ${requiredParameters.join(', ')}.`,
2196
+ param: REQUIRED_PARAMETERS_HEADER,
2197
+ }
2198
+ : {
2199
+ type: 'model_not_found',
2200
+ code: 'model_not_found',
2201
+ message: `No policy-allowed peer currently serves model "${requestedService}".`,
2202
+ param: 'model',
2203
+ },
2148
2204
  }));
2149
2205
  return;
2150
2206
  }
@@ -2304,6 +2360,21 @@ export class BuyerProxy {
2304
2360
  const selectedPlan = routingPlans.get(selectedPeer.peerId)
2305
2361
  ?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider, 'lenient');
2306
2362
  const pinnedServiceId = selectedPlan?.serviceId ?? requestedService;
2363
+ const missingRequired = selectedPlan && !selectedPlan.selection?.requiresTransform
2364
+ ? findMissingRequiredParameters(selectedPeer, selectedPlan.provider, pinnedServiceId, requiredParameters)
2365
+ : requiredParameters;
2366
+ if (missingRequired.length > 0) {
2367
+ res.writeHead(422, { 'content-type': 'application/json' });
2368
+ res.end(JSON.stringify({
2369
+ error: {
2370
+ type: 'required_capability_unavailable',
2371
+ code: 'required_capability_unavailable',
2372
+ message: `Pinned seller does not advertise required parameter(s) for model "${pinnedServiceId ?? requestedService ?? 'unknown'}": ${missingRequired.join(', ')}.`,
2373
+ param: REQUIRED_PARAMETERS_HEADER,
2374
+ },
2375
+ }));
2376
+ return;
2377
+ }
2307
2378
  const pinnedRequest = pinnedServiceId ? withRoutedModel(serializedReq, pinnedServiceId) : serializedReq;
2308
2379
  if (!peerAllowedByPolicy(policyRouter, pinnedRequest, selectedPeer)) {
2309
2380
  log(`Pinned peer ${selectedPeer.peerId.slice(0, 12)}... filtered out by buyer routing policy`);
@@ -2418,7 +2489,7 @@ export class BuyerProxy {
2418
2489
  + `did not announce for this service: ${unannounced.join(', ')} — the upstream may ignore or reject them`);
2419
2490
  }
2420
2491
  }
2421
- const { 'x-antseed-pin-peer': _pinPeer, 'x-antseed-prefer-peer': _preferPeer, 'x-vpr-session-id': _vprSession,
2492
+ const { 'x-antseed-pin-peer': _pinPeer, 'x-antseed-prefer-peer': _preferPeer, [REQUIRED_PARAMETERS_HEADER]: _requiredParameters, 'x-vpr-session-id': _vprSession,
2422
2493
  // Legacy desktop builds (pre AntStation → VPR rename) still send this.
2423
2494
  'x-antstation-session-id': _antstationSession, ...headersForPeer } = serializedReq.headers;
2424
2495
  let requestForPeer = {