@getanyapi/sdk 0.9.7 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,7 +73,11 @@ console.log(api.pricing.from, api.pricing.failoverMaxUsd, api.inputSchema);
73
73
 
74
74
  `catalog` is category-only browsing. Ranked queries always use `search`, which returns
75
75
  `results`, `total`, and `ranking`; `describe` includes schemas. Discovery prices are nested
76
- USD flat/linear offers, lanes are anonymous, and provider is always `"AnyAPI"`.
76
+ USD flat/linear offers, lanes are anonymous, and provider is always `"AnyAPI"`. The gateway
77
+ owns validation, routing, lane order, failover, pricing relationships, health semantics, and
78
+ billing. This handwritten discovery client safety-scans the response, projects known fields,
79
+ preserves schemas as opaque JSON, and ignores safe additions. It does not recompute gateway
80
+ business rules. Generated per-SKU methods remain a separate OpenAPI-driven surface.
77
81
 
78
82
  ## Pagination
79
83
 
@@ -113,7 +117,8 @@ await client.google.search(
113
117
  );
114
118
  ```
115
119
 
116
- Per-call transport overrides: `timeoutMs`, `maxRetries`, and an `AbortSignal` via `signal`.
120
+ Per-call transport overrides: `timeoutMs`, `maxRetries`, `maxInProgressWaitMs`, and an
121
+ `AbortSignal` via `signal`.
117
122
 
118
123
  ## Errors and retries
119
124
 
@@ -129,12 +134,30 @@ Per-call transport overrides: `timeoutMs`, `maxRetries`, and an `AbortSignal` vi
129
134
  | `ConnectionError` | 0 | Network or transport failure |
130
135
  | `TimeoutError` | 0 | Request exceeded its timeout (not retried) |
131
136
 
132
- All extend `AnyAPIError` (with `status` and `requestId`). Retries cover only 429 and network
133
- failures proven to happen before a request was sent, with jittered exponential backoff honoring
134
- `Retry-After`. Default `maxRetries` is 2 (up to 3 attempts); set it on the client or per request.
135
- Timeouts are never retried. Connection failures during or after a billed `POST /v1/run` are not
136
- retried because the call may already have been charged. When the send phase is unknown, the SDK
137
- does not retry. Configure with `new AnyAPI({ timeoutMs, maxRetries })`.
137
+ All extend `AnyAPIError` (with `status` and `requestId`). Retries cover 429, one specific 409
138
+ (below), and network failures proven to happen before a request was sent, with jittered
139
+ exponential backoff honoring `Retry-After`. Default `maxRetries` is 2 (up to 3 attempts); set it
140
+ on the client or per request. Timeouts are never retried. Connection failures during or after a
141
+ billed `POST /v1/run` are not retried because the call may already have been charged. When the
142
+ send phase is unknown, the SDK does not retry. Configure with
143
+ `new AnyAPI({ timeoutMs, maxRetries })`.
144
+
145
+ ### Waiting out a run that is still in flight
146
+
147
+ Settlement is detached from your connection, so a run keeps going after a connection drops. When
148
+ you re-issue a call whose `Idempotency-Key` is still executing, the gateway answers `409` with
149
+ `code: "idempotency_in_progress"` and `Retry-After: 30`. The SDK waits that full delay and
150
+ retries, so you get the original run's replayed result (`replayed: true`, no second charge)
151
+ instead of an error.
152
+
153
+ The 8s ordinary-backoff ceiling does not apply here; a separate whole-call budget does.
154
+ `maxInProgressWaitMs` (default `60000`) caps the TOTAL time one `run()` may block on these
155
+ waits, across every retry. A wait that does not fit the remaining budget is refused and the
156
+ `409` is thrown rather than truncated into an attempt that would fail anyway. Set
157
+ `maxInProgressWaitMs: 0` to surface the `409` immediately and handle it yourself.
158
+
159
+ No other `409` retries: `idempotency_conflict` (the same key with different input) and
160
+ `idempotency_needs_review` are caller-side problems a retry cannot fix.
138
161
 
139
162
  Automatic network retry of a billed `run()` requires structured runtime evidence that the
140
163
  request body was not sent:
package/dist/index.cjs CHANGED
@@ -101,7 +101,10 @@ module.exports = __toCommonJS(index_exports);
101
101
  var AnyAPIError = class extends Error {
102
102
  /** HTTP status code, or 0 for transport-level failures (connection/timeout). */
103
103
  status;
104
- /** The x-request-id response header when present, else undefined. */
104
+ /**
105
+ * The gateway's X-Anyapi-Request-Id response header when present (falling back to a
106
+ * proxy-set x-request-id), else undefined. Quote it to AnyAPI support.
107
+ */
105
108
  requestId;
106
109
  /** Stable gateway error code when the JSON body includes one, else undefined. */
107
110
  code;
@@ -136,6 +139,14 @@ var ConnectionError = class extends AnyAPIError {
136
139
  };
137
140
  var TimeoutError = class extends AnyAPIError {
138
141
  };
142
+ var REQUEST_ID_HEADERS = ["x-anyapi-request-id", "x-request-id"];
143
+ function requestIdOf(headers) {
144
+ for (const name of REQUEST_ID_HEADERS) {
145
+ const value = headers.get(name);
146
+ if (value) return value;
147
+ }
148
+ return void 0;
149
+ }
139
150
  function errorFromStatus(status, message, requestId, code) {
140
151
  switch (status) {
141
152
  case 400:
@@ -207,17 +218,20 @@ var DEFAULT_BASE_URL = "https://api.getanyapi.com";
207
218
  function malformed(path) {
208
219
  throw new AnyAPIError(`malformed discovery response: ${path}`, 0);
209
220
  }
210
- function rejectInternalKeys(value, path) {
221
+ function rejectUnsafeDiscoveryFields(value, path) {
211
222
  if (Array.isArray(value)) {
212
223
  value.forEach(
213
- (item, index) => rejectInternalKeys(item, `${path}[${index}]`)
224
+ (item, index) => rejectUnsafeDiscoveryFields(item, `${path}[${index}]`)
214
225
  );
215
226
  return;
216
227
  }
217
228
  if (typeof value !== "object" || value === null) return;
218
229
  for (const [key, item] of Object.entries(value)) {
219
230
  if (key.toLowerCase().includes("credit")) malformed(`${path}.${key}`);
220
- rejectInternalKeys(item, `${path}.${key}`);
231
+ if (key === "provider" && item !== "AnyAPI") {
232
+ malformed(`${path}.${key}`);
233
+ }
234
+ rejectUnsafeDiscoveryFields(item, `${path}.${key}`);
221
235
  }
222
236
  }
223
237
  function record(value, path) {
@@ -226,12 +240,6 @@ function record(value, path) {
226
240
  }
227
241
  return value;
228
242
  }
229
- function exactKeys(raw, allowed, path) {
230
- const keys = new Set(allowed);
231
- for (const key of Object.keys(raw)) {
232
- if (!keys.has(key)) malformed(`${path}.${key}`);
233
- }
234
- }
235
243
  function stringField(raw, key, path) {
236
244
  const value = raw[key];
237
245
  if (typeof value !== "string") return malformed(`${path}.${key}`);
@@ -262,14 +270,12 @@ function parseOffer(value, path) {
262
270
  const unit = stringField(raw, "unit", path);
263
271
  const maxUsd = numberField(raw, "maxUsd", path);
264
272
  if (model === "flat") {
265
- exactKeys(raw, ["model", "unit", "maxUsd"], path);
266
- if (unit !== "request" || "baseUsd" in raw || "perUnitUsd" in raw) {
273
+ if (unit !== "request") {
267
274
  return malformed(path);
268
275
  }
269
276
  return { model, unit, maxUsd };
270
277
  }
271
278
  if (model === "linear") {
272
- exactKeys(raw, ["model", "unit", "baseUsd", "perUnitUsd", "maxUsd"], path);
273
279
  if (unit.length === 0) return malformed(`${path}.unit`);
274
280
  return {
275
281
  model,
@@ -283,7 +289,6 @@ function parseOffer(value, path) {
283
289
  }
284
290
  function parsePricing(value, path) {
285
291
  const raw = record(value, path);
286
- exactKeys(raw, ["from", "failoverMaxUsd"], path);
287
292
  return {
288
293
  from: parseOffer(raw["from"], `${path}.from`),
289
294
  failoverMaxUsd: numberField(raw, "failoverMaxUsd", path)
@@ -291,10 +296,8 @@ function parsePricing(value, path) {
291
296
  }
292
297
  function parseHealth(value, path) {
293
298
  const raw = record(value, path);
294
- exactKeys(raw, ["window", "uptimePct", "latencyP50Ms", "requests"], path);
295
- if (raw["window"] !== "30d") return malformed(`${path}.window`);
296
299
  return {
297
- window: "30d",
300
+ window: stringField(raw, "window", path),
298
301
  uptimePct: boundedNumberField(raw, "uptimePct", path, void 0, 100),
299
302
  latencyP50Ms: integerField(raw, "latencyP50Ms", path),
300
303
  requests: integerField(raw, "requests", path)
@@ -302,7 +305,6 @@ function parseHealth(value, path) {
302
305
  }
303
306
  function parseLane(value, path) {
304
307
  const raw = record(value, path);
305
- exactKeys(raw, ["pricing", "health"], path);
306
308
  const lane = {
307
309
  pricing: parseOffer(raw["pricing"], `${path}.pricing`)
308
310
  };
@@ -320,7 +322,6 @@ function parseSchema(value, path) {
320
322
  }
321
323
  function parseHighlight(value, path) {
322
324
  const raw = record(value, path);
323
- exactKeys(raw, ["path", "type", "why"], path);
324
325
  const field = {
325
326
  path: stringField(raw, "path", path),
326
327
  type: stringField(raw, "type", path)
@@ -341,28 +342,10 @@ function mapProfile(raw) {
341
342
  return profile;
342
343
  }
343
344
  function mapCatalogEntry(raw) {
344
- rejectInternalKeys(raw, "api");
345
+ rejectUnsafeDiscoveryFields(raw, "api");
345
346
  const value = record(raw, "api");
346
- exactKeys(
347
- value,
348
- [
349
- "id",
350
- "slug",
351
- "category",
352
- "name",
353
- "description",
354
- "provider",
355
- "pricing",
356
- "lanes",
357
- "heavy",
358
- "tryEligible",
359
- "inputSchema",
360
- "outputSchema"
361
- ],
362
- "api"
363
- );
364
347
  const lanesRaw = value["lanes"];
365
- if (!Array.isArray(lanesRaw) || lanesRaw.length === 0) {
348
+ if (!Array.isArray(lanesRaw)) {
366
349
  return malformed("api.lanes");
367
350
  }
368
351
  const entry = {
@@ -384,32 +367,24 @@ function mapCatalogEntry(raw) {
384
367
  }
385
368
  if (typeof value["tryEligible"] !== "boolean")
386
369
  return malformed("api.tryEligible");
370
+ if (value["failover"] !== void 0) {
371
+ if (typeof value["failover"] !== "boolean")
372
+ return malformed("api.failover");
373
+ entry.failover = value["failover"];
374
+ }
375
+ if (value["excludesCallerDelay"] !== void 0) {
376
+ if (typeof value["excludesCallerDelay"] !== "boolean")
377
+ return malformed("api.excludesCallerDelay");
378
+ entry.excludesCallerDelay = value["excludesCallerDelay"];
379
+ }
387
380
  if (value["inputSchema"] !== void 0) {
388
381
  entry.inputSchema = parseSchema(value["inputSchema"], "api.inputSchema");
389
382
  }
390
383
  if (value["outputSchema"] !== void 0) {
391
384
  entry.outputSchema = parseSchema(value["outputSchema"], "api.outputSchema");
392
385
  }
393
- if (!offersEqual(entry.pricing.from, entry.lanes[0].pricing)) {
394
- return malformed("api.pricing.from");
395
- }
396
- const failoverMaxUsd = Math.max(
397
- ...entry.lanes.map((lane) => lane.pricing.maxUsd)
398
- );
399
- if (entry.pricing.failoverMaxUsd !== failoverMaxUsd) {
400
- return malformed("api.pricing.failoverMaxUsd");
401
- }
402
386
  return entry;
403
387
  }
404
- function offersEqual(left, right) {
405
- if (left.model !== right.model || left.unit !== right.unit || left.maxUsd !== right.maxUsd) {
406
- return false;
407
- }
408
- if (left.model === "flat" || right.model === "flat") {
409
- return left.model === right.model;
410
- }
411
- return left.baseUsd === right.baseUsd && left.perUnitUsd === right.perUnitUsd;
412
- }
413
388
  function mapCatalogDetail(raw) {
414
389
  const entry = mapCatalogEntry(raw);
415
390
  if (entry.inputSchema === void 0) return malformed("api.inputSchema");
@@ -417,28 +392,13 @@ function mapCatalogDetail(raw) {
417
392
  return entry;
418
393
  }
419
394
  function mapCatalogList(raw) {
395
+ rejectUnsafeDiscoveryFields(raw, "catalog");
420
396
  const envelope = record(raw, "catalog");
421
- exactKeys(envelope, ["apis"], "catalog");
422
397
  if (!Array.isArray(envelope["apis"])) return malformed("catalog.apis");
423
398
  return envelope["apis"].map(mapCatalogEntry);
424
399
  }
425
400
  function mapSearchResult(value, path) {
426
401
  const raw = record(value, path);
427
- exactKeys(
428
- raw,
429
- [
430
- "slug",
431
- "platformId",
432
- "name",
433
- "description",
434
- "category",
435
- "provider",
436
- "pricing",
437
- "relevance",
438
- "highlightFields"
439
- ],
440
- path
441
- );
442
402
  const result = {
443
403
  slug: stringField(raw, "slug", path),
444
404
  platformId: stringField(raw, "platformId", path),
@@ -459,9 +419,8 @@ function mapSearchResult(value, path) {
459
419
  return result;
460
420
  }
461
421
  function mapCatalogSearch(raw) {
462
- rejectInternalKeys(raw, "search");
422
+ rejectUnsafeDiscoveryFields(raw, "search");
463
423
  const envelope = record(raw, "search");
464
- exactKeys(envelope, ["results", "total", "ranking"], "search");
465
424
  if (!Array.isArray(envelope["results"])) return malformed("search.results");
466
425
  const ranking = envelope["ranking"];
467
426
  if (ranking !== "semantic" && ranking !== "keyword")
@@ -506,7 +465,7 @@ async function agentSignup(options = {}) {
506
465
  0
507
466
  );
508
467
  }
509
- const requestId = response.headers.get("x-request-id") ?? void 0;
468
+ const requestId = requestIdOf(response.headers);
510
469
  const text = await response.text().catch(() => "");
511
470
  if (response.status !== 200) {
512
471
  let message = `request failed with status ${response.status}`;
@@ -538,6 +497,8 @@ var DEFAULT_TIMEOUT_MS = 6e4;
538
497
  var DEFAULT_MAX_RETRIES = 2;
539
498
  var RETRY_BASE_DELAY_MS = 500;
540
499
  var RETRY_MAX_DELAY_MS = 8e3;
500
+ var DEFAULT_MAX_IN_PROGRESS_WAIT_MS = 6e4;
501
+ var IDEMPOTENCY_IN_PROGRESS_CODE = "idempotency_in_progress";
541
502
  var PRE_SEND_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
542
503
  "EADDRNOTAVAIL",
543
504
  "EAI_AGAIN",
@@ -581,7 +542,7 @@ function backoffDelay(attempt) {
581
542
  const factor = 0.5 + Math.random();
582
543
  return base * factor;
583
544
  }
584
- function parseRetryAfter(header) {
545
+ function parseRetryAfterMs(header) {
585
546
  if (header === null) {
586
547
  return void 0;
587
548
  }
@@ -591,13 +552,11 @@ function parseRetryAfter(header) {
591
552
  }
592
553
  const seconds = Number(trimmed);
593
554
  if (Number.isFinite(seconds)) {
594
- const ms = seconds * 1e3;
595
- return Math.min(Math.max(ms, 0), RETRY_MAX_DELAY_MS);
555
+ return Math.max(seconds * 1e3, 0);
596
556
  }
597
557
  const dateMs = Date.parse(trimmed);
598
558
  if (Number.isFinite(dateMs)) {
599
- const delta = dateMs - Date.now();
600
- return Math.min(Math.max(delta, 0), RETRY_MAX_DELAY_MS);
559
+ return Math.max(dateMs - Date.now(), 0);
601
560
  }
602
561
  return void 0;
603
562
  }
@@ -702,6 +661,7 @@ var AnyAPI = class {
702
661
  maxRetries;
703
662
  timeoutMs;
704
663
  idempotency;
664
+ maxInProgressWaitMs;
705
665
  /**
706
666
  * The network seam the generated per-platform namespaces target. The base client IS a
707
667
  * ClientCore (it implements `run`), so the generated subclass hands `this._core` to each
@@ -724,6 +684,7 @@ var AnyAPI = class {
724
684
  this.fetchImpl = resolvedFetch;
725
685
  this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
726
686
  this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
687
+ this.maxInProgressWaitMs = options.maxInProgressWaitMs ?? DEFAULT_MAX_IN_PROGRESS_WAIT_MS;
727
688
  const idempotency = options.idempotency ?? "auto";
728
689
  if (idempotency !== "auto" && idempotency !== "off") {
729
690
  throw new TypeError('idempotency must be "auto" or "off"');
@@ -744,6 +705,7 @@ var AnyAPI = class {
744
705
  body,
745
706
  timeoutMs: options?.timeoutMs ?? this.timeoutMs,
746
707
  maxRetries: options?.maxRetries ?? this.maxRetries,
708
+ maxInProgressWaitMs: options?.maxInProgressWaitMs ?? this.maxInProgressWaitMs,
747
709
  ...options?.idempotencyKey !== void 0 ? { idempotencyKey: options.idempotencyKey } : {},
748
710
  ...options?.signal ? { signal: options.signal } : {}
749
711
  }
@@ -816,6 +778,11 @@ var AnyAPI = class {
816
778
  headers["Idempotency-Key"] = key;
817
779
  }
818
780
  let attempt = 0;
781
+ let inProgressWaitedMs = 0;
782
+ const inProgressBudgetMs = Math.max(
783
+ opts.maxInProgressWaitMs ?? DEFAULT_MAX_IN_PROGRESS_WAIT_MS,
784
+ 0
785
+ );
819
786
  for (; ; ) {
820
787
  const { signal, timeoutSignal } = composeSignal(
821
788
  opts.timeoutMs,
@@ -849,7 +816,7 @@ var AnyAPI = class {
849
816
  }
850
817
  throw connErr;
851
818
  }
852
- const requestId = response.headers.get("x-request-id") ?? void 0;
819
+ const requestId = requestIdOf(response.headers);
853
820
  if (response.status === 200) {
854
821
  const text = await response.text();
855
822
  try {
@@ -864,13 +831,22 @@ var AnyAPI = class {
864
831
  }
865
832
  const body = await response.text().catch(() => "");
866
833
  const { message, code } = messageFromBody(body, response.status);
834
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
867
835
  if (response.status === 429 && attempt < opts.maxRetries) {
868
- const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
869
- const delay = retryAfter ?? backoffDelay(attempt);
836
+ const delay = retryAfterMs !== void 0 ? Math.min(retryAfterMs, RETRY_MAX_DELAY_MS) : backoffDelay(attempt);
870
837
  await sleep(delay, opts.signal);
871
838
  attempt += 1;
872
839
  continue;
873
840
  }
841
+ if (billedPost && response.status === 409 && code === IDEMPOTENCY_IN_PROGRESS_CODE && attempt < opts.maxRetries) {
842
+ const delay = retryAfterMs ?? backoffDelay(attempt);
843
+ if (inProgressWaitedMs + delay <= inProgressBudgetMs) {
844
+ await sleep(delay, opts.signal);
845
+ inProgressWaitedMs += delay;
846
+ attempt += 1;
847
+ continue;
848
+ }
849
+ }
874
850
  throw errorFromStatus(response.status, message, requestId, code);
875
851
  }
876
852
  }