@getanyapi/sdk 0.9.8 → 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
@@ -218,17 +218,20 @@ var DEFAULT_BASE_URL = "https://api.getanyapi.com";
218
218
  function malformed(path) {
219
219
  throw new AnyAPIError(`malformed discovery response: ${path}`, 0);
220
220
  }
221
- function rejectInternalKeys(value, path) {
221
+ function rejectUnsafeDiscoveryFields(value, path) {
222
222
  if (Array.isArray(value)) {
223
223
  value.forEach(
224
- (item, index) => rejectInternalKeys(item, `${path}[${index}]`)
224
+ (item, index) => rejectUnsafeDiscoveryFields(item, `${path}[${index}]`)
225
225
  );
226
226
  return;
227
227
  }
228
228
  if (typeof value !== "object" || value === null) return;
229
229
  for (const [key, item] of Object.entries(value)) {
230
230
  if (key.toLowerCase().includes("credit")) malformed(`${path}.${key}`);
231
- rejectInternalKeys(item, `${path}.${key}`);
231
+ if (key === "provider" && item !== "AnyAPI") {
232
+ malformed(`${path}.${key}`);
233
+ }
234
+ rejectUnsafeDiscoveryFields(item, `${path}.${key}`);
232
235
  }
233
236
  }
234
237
  function record(value, path) {
@@ -237,12 +240,6 @@ function record(value, path) {
237
240
  }
238
241
  return value;
239
242
  }
240
- function exactKeys(raw, allowed, path) {
241
- const keys = new Set(allowed);
242
- for (const key of Object.keys(raw)) {
243
- if (!keys.has(key)) malformed(`${path}.${key}`);
244
- }
245
- }
246
243
  function stringField(raw, key, path) {
247
244
  const value = raw[key];
248
245
  if (typeof value !== "string") return malformed(`${path}.${key}`);
@@ -273,14 +270,12 @@ function parseOffer(value, path) {
273
270
  const unit = stringField(raw, "unit", path);
274
271
  const maxUsd = numberField(raw, "maxUsd", path);
275
272
  if (model === "flat") {
276
- exactKeys(raw, ["model", "unit", "maxUsd"], path);
277
- if (unit !== "request" || "baseUsd" in raw || "perUnitUsd" in raw) {
273
+ if (unit !== "request") {
278
274
  return malformed(path);
279
275
  }
280
276
  return { model, unit, maxUsd };
281
277
  }
282
278
  if (model === "linear") {
283
- exactKeys(raw, ["model", "unit", "baseUsd", "perUnitUsd", "maxUsd"], path);
284
279
  if (unit.length === 0) return malformed(`${path}.unit`);
285
280
  return {
286
281
  model,
@@ -294,7 +289,6 @@ function parseOffer(value, path) {
294
289
  }
295
290
  function parsePricing(value, path) {
296
291
  const raw = record(value, path);
297
- exactKeys(raw, ["from", "failoverMaxUsd"], path);
298
292
  return {
299
293
  from: parseOffer(raw["from"], `${path}.from`),
300
294
  failoverMaxUsd: numberField(raw, "failoverMaxUsd", path)
@@ -302,10 +296,8 @@ function parsePricing(value, path) {
302
296
  }
303
297
  function parseHealth(value, path) {
304
298
  const raw = record(value, path);
305
- exactKeys(raw, ["window", "uptimePct", "latencyP50Ms", "requests"], path);
306
- if (raw["window"] !== "30d") return malformed(`${path}.window`);
307
299
  return {
308
- window: "30d",
300
+ window: stringField(raw, "window", path),
309
301
  uptimePct: boundedNumberField(raw, "uptimePct", path, void 0, 100),
310
302
  latencyP50Ms: integerField(raw, "latencyP50Ms", path),
311
303
  requests: integerField(raw, "requests", path)
@@ -313,7 +305,6 @@ function parseHealth(value, path) {
313
305
  }
314
306
  function parseLane(value, path) {
315
307
  const raw = record(value, path);
316
- exactKeys(raw, ["pricing", "health"], path);
317
308
  const lane = {
318
309
  pricing: parseOffer(raw["pricing"], `${path}.pricing`)
319
310
  };
@@ -331,7 +322,6 @@ function parseSchema(value, path) {
331
322
  }
332
323
  function parseHighlight(value, path) {
333
324
  const raw = record(value, path);
334
- exactKeys(raw, ["path", "type", "why"], path);
335
325
  const field = {
336
326
  path: stringField(raw, "path", path),
337
327
  type: stringField(raw, "type", path)
@@ -352,28 +342,10 @@ function mapProfile(raw) {
352
342
  return profile;
353
343
  }
354
344
  function mapCatalogEntry(raw) {
355
- rejectInternalKeys(raw, "api");
345
+ rejectUnsafeDiscoveryFields(raw, "api");
356
346
  const value = record(raw, "api");
357
- exactKeys(
358
- value,
359
- [
360
- "id",
361
- "slug",
362
- "category",
363
- "name",
364
- "description",
365
- "provider",
366
- "pricing",
367
- "lanes",
368
- "heavy",
369
- "tryEligible",
370
- "inputSchema",
371
- "outputSchema"
372
- ],
373
- "api"
374
- );
375
347
  const lanesRaw = value["lanes"];
376
- if (!Array.isArray(lanesRaw) || lanesRaw.length === 0) {
348
+ if (!Array.isArray(lanesRaw)) {
377
349
  return malformed("api.lanes");
378
350
  }
379
351
  const entry = {
@@ -395,32 +367,24 @@ function mapCatalogEntry(raw) {
395
367
  }
396
368
  if (typeof value["tryEligible"] !== "boolean")
397
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
+ }
398
380
  if (value["inputSchema"] !== void 0) {
399
381
  entry.inputSchema = parseSchema(value["inputSchema"], "api.inputSchema");
400
382
  }
401
383
  if (value["outputSchema"] !== void 0) {
402
384
  entry.outputSchema = parseSchema(value["outputSchema"], "api.outputSchema");
403
385
  }
404
- if (!offersEqual(entry.pricing.from, entry.lanes[0].pricing)) {
405
- return malformed("api.pricing.from");
406
- }
407
- const failoverMaxUsd = Math.max(
408
- ...entry.lanes.map((lane) => lane.pricing.maxUsd)
409
- );
410
- if (entry.pricing.failoverMaxUsd !== failoverMaxUsd) {
411
- return malformed("api.pricing.failoverMaxUsd");
412
- }
413
386
  return entry;
414
387
  }
415
- function offersEqual(left, right) {
416
- if (left.model !== right.model || left.unit !== right.unit || left.maxUsd !== right.maxUsd) {
417
- return false;
418
- }
419
- if (left.model === "flat" || right.model === "flat") {
420
- return left.model === right.model;
421
- }
422
- return left.baseUsd === right.baseUsd && left.perUnitUsd === right.perUnitUsd;
423
- }
424
388
  function mapCatalogDetail(raw) {
425
389
  const entry = mapCatalogEntry(raw);
426
390
  if (entry.inputSchema === void 0) return malformed("api.inputSchema");
@@ -428,28 +392,13 @@ function mapCatalogDetail(raw) {
428
392
  return entry;
429
393
  }
430
394
  function mapCatalogList(raw) {
395
+ rejectUnsafeDiscoveryFields(raw, "catalog");
431
396
  const envelope = record(raw, "catalog");
432
- exactKeys(envelope, ["apis"], "catalog");
433
397
  if (!Array.isArray(envelope["apis"])) return malformed("catalog.apis");
434
398
  return envelope["apis"].map(mapCatalogEntry);
435
399
  }
436
400
  function mapSearchResult(value, path) {
437
401
  const raw = record(value, path);
438
- exactKeys(
439
- raw,
440
- [
441
- "slug",
442
- "platformId",
443
- "name",
444
- "description",
445
- "category",
446
- "provider",
447
- "pricing",
448
- "relevance",
449
- "highlightFields"
450
- ],
451
- path
452
- );
453
402
  const result = {
454
403
  slug: stringField(raw, "slug", path),
455
404
  platformId: stringField(raw, "platformId", path),
@@ -470,9 +419,8 @@ function mapSearchResult(value, path) {
470
419
  return result;
471
420
  }
472
421
  function mapCatalogSearch(raw) {
473
- rejectInternalKeys(raw, "search");
422
+ rejectUnsafeDiscoveryFields(raw, "search");
474
423
  const envelope = record(raw, "search");
475
- exactKeys(envelope, ["results", "total", "ranking"], "search");
476
424
  if (!Array.isArray(envelope["results"])) return malformed("search.results");
477
425
  const ranking = envelope["ranking"];
478
426
  if (ranking !== "semantic" && ranking !== "keyword")
@@ -549,6 +497,8 @@ var DEFAULT_TIMEOUT_MS = 6e4;
549
497
  var DEFAULT_MAX_RETRIES = 2;
550
498
  var RETRY_BASE_DELAY_MS = 500;
551
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";
552
502
  var PRE_SEND_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
553
503
  "EADDRNOTAVAIL",
554
504
  "EAI_AGAIN",
@@ -592,7 +542,7 @@ function backoffDelay(attempt) {
592
542
  const factor = 0.5 + Math.random();
593
543
  return base * factor;
594
544
  }
595
- function parseRetryAfter(header) {
545
+ function parseRetryAfterMs(header) {
596
546
  if (header === null) {
597
547
  return void 0;
598
548
  }
@@ -602,13 +552,11 @@ function parseRetryAfter(header) {
602
552
  }
603
553
  const seconds = Number(trimmed);
604
554
  if (Number.isFinite(seconds)) {
605
- const ms = seconds * 1e3;
606
- return Math.min(Math.max(ms, 0), RETRY_MAX_DELAY_MS);
555
+ return Math.max(seconds * 1e3, 0);
607
556
  }
608
557
  const dateMs = Date.parse(trimmed);
609
558
  if (Number.isFinite(dateMs)) {
610
- const delta = dateMs - Date.now();
611
- return Math.min(Math.max(delta, 0), RETRY_MAX_DELAY_MS);
559
+ return Math.max(dateMs - Date.now(), 0);
612
560
  }
613
561
  return void 0;
614
562
  }
@@ -713,6 +661,7 @@ var AnyAPI = class {
713
661
  maxRetries;
714
662
  timeoutMs;
715
663
  idempotency;
664
+ maxInProgressWaitMs;
716
665
  /**
717
666
  * The network seam the generated per-platform namespaces target. The base client IS a
718
667
  * ClientCore (it implements `run`), so the generated subclass hands `this._core` to each
@@ -735,6 +684,7 @@ var AnyAPI = class {
735
684
  this.fetchImpl = resolvedFetch;
736
685
  this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
737
686
  this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
687
+ this.maxInProgressWaitMs = options.maxInProgressWaitMs ?? DEFAULT_MAX_IN_PROGRESS_WAIT_MS;
738
688
  const idempotency = options.idempotency ?? "auto";
739
689
  if (idempotency !== "auto" && idempotency !== "off") {
740
690
  throw new TypeError('idempotency must be "auto" or "off"');
@@ -755,6 +705,7 @@ var AnyAPI = class {
755
705
  body,
756
706
  timeoutMs: options?.timeoutMs ?? this.timeoutMs,
757
707
  maxRetries: options?.maxRetries ?? this.maxRetries,
708
+ maxInProgressWaitMs: options?.maxInProgressWaitMs ?? this.maxInProgressWaitMs,
758
709
  ...options?.idempotencyKey !== void 0 ? { idempotencyKey: options.idempotencyKey } : {},
759
710
  ...options?.signal ? { signal: options.signal } : {}
760
711
  }
@@ -827,6 +778,11 @@ var AnyAPI = class {
827
778
  headers["Idempotency-Key"] = key;
828
779
  }
829
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
+ );
830
786
  for (; ; ) {
831
787
  const { signal, timeoutSignal } = composeSignal(
832
788
  opts.timeoutMs,
@@ -875,13 +831,22 @@ var AnyAPI = class {
875
831
  }
876
832
  const body = await response.text().catch(() => "");
877
833
  const { message, code } = messageFromBody(body, response.status);
834
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
878
835
  if (response.status === 429 && attempt < opts.maxRetries) {
879
- const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
880
- const delay = retryAfter ?? backoffDelay(attempt);
836
+ const delay = retryAfterMs !== void 0 ? Math.min(retryAfterMs, RETRY_MAX_DELAY_MS) : backoffDelay(attempt);
881
837
  await sleep(delay, opts.signal);
882
838
  attempt += 1;
883
839
  continue;
884
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
+ }
885
850
  throw errorFromStatus(response.status, message, requestId, code);
886
851
  }
887
852
  }