@hypit/hypit 0.2.11 → 0.2.12

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.
@@ -383,6 +383,36 @@ declare function wakeAfter(handle: CanonicalValue, delayMs: number, now?: number
383
383
  readonly wakeAt: number;
384
384
  readonly progress?: OperationProgress;
385
385
  };
386
+ /** A service reported a failure with a stable code. */
387
+ declare class EndpointServiceError extends Error {
388
+ readonly code: string;
389
+ constructor(code: string, message: string);
390
+ }
391
+ /** A non-success HTTP response, with the wait the service asked for when it gave one. */
392
+ declare class EndpointHttpError extends EndpointServiceError {
393
+ readonly status: number;
394
+ readonly retryAfterMs?: number | undefined;
395
+ constructor(code: string, message: string, status: number, retryAfterMs?: number | undefined);
396
+ }
397
+ /** The request produced no response: connection failure, interrupted read, or request timeout. */
398
+ declare class EndpointTransportError extends Error {
399
+ }
400
+ /** A success response whose body cannot be used. */
401
+ declare class EndpointResponseError extends Error {
402
+ }
403
+ /** Report a rejected send or read as a transport error. */
404
+ declare function transport<T>(request: Promise<T>): Promise<T>;
405
+ /** The wait a `Retry-After` header asks for, in milliseconds; either delay-seconds or an HTTP-date. */
406
+ declare function retryAfterMs(headers: Headers, now?: number): number | undefined;
407
+ /**
408
+ * Decide a poll action's outcome from the error it threw. Transport errors and HTTP 429/5xx keep the
409
+ * job pending for the next poll; every other error, including credential and response errors, fails it.
410
+ */
411
+ declare function pollAgainOrFail(error: unknown, options: {
412
+ readonly handle: CanonicalValue;
413
+ readonly pollIntervalMs: number;
414
+ readonly failure: (error: unknown) => EndpointOutcome;
415
+ }): EndpointOutcome;
386
416
 
387
- export { actionResourceClaims, canonicalize, credentialRef, defineEndpointPackage, endpointActions, endpointResourceClaims, isStreamingResourceStore, verifyEndpointPricingDocument, wakeAfter };
417
+ export { EndpointHttpError, EndpointResponseError, EndpointServiceError, EndpointTransportError, actionResourceClaims, canonicalize, credentialRef, defineEndpointPackage, endpointActions, endpointResourceClaims, isStreamingResourceStore, pollAgainOrFail, retryAfterMs, transport, verifyEndpointPricingDocument, wakeAfter };
388
418
  export type { AsyncEndpoint, AsyncEndpointCapability, Awaitable, BlobRef, CanonicalValue, CapabilityRef, CapacityResourceClaim, CredentialAcquisition, CredentialRef, CredentialValue, DefineEndpointPackageOptions, EndpointAction, EndpointActionLimits, EndpointCancelOutcome, EndpointCapability, EndpointCheckpoint, EndpointCredential, EndpointCredentialDescription, EndpointFulfillment, EndpointInputSlot, EndpointInvocationContext, EndpointOffer, EndpointOutcome, EndpointPackage, EndpointPollContext, EndpointPricing, EndpointPricingDocument, EndpointPricingReader, EndpointPricingReaderContext, EndpointRegistrar, EndpointRegistrationOptions, EndpointRequest, EndpointScheduling, EndpointStartContext, EndpointSupport, ImmediateEndpointCapability, ImmediateEndpointHandler, ModuleRef, OperationFailure, OperationProgress, OperationReceipt, ResourceIOOptions, ResourceId, ResourceStore, StoredValue, TypeRef };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypit/hypit",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "homepage": "https://hypit.ai",
5
5
  "repository": {
6
6
  "type": "git",
@@ -492,3 +492,50 @@ export function wakeAfter(
492
492
  ...(progress === undefined ? {} : { progress }),
493
493
  };
494
494
  }
495
+
496
+ /** A service reported a failure with a stable code. */
497
+ export class EndpointServiceError extends Error {
498
+ constructor(readonly code: string, message: string) { super(message); }
499
+ }
500
+ /** A non-success HTTP response, with the wait the service asked for when it gave one. */
501
+ export class EndpointHttpError extends EndpointServiceError {
502
+ constructor(code: string, message: string, readonly status: number, readonly retryAfterMs?: number) { super(code, message); }
503
+ }
504
+ /** The request produced no response: connection failure, interrupted read, or request timeout. */
505
+ export class EndpointTransportError extends Error {}
506
+ /** A success response whose body cannot be used. */
507
+ export class EndpointResponseError extends Error {}
508
+
509
+ /** Report a rejected send or read as a transport error. */
510
+ export async function transport<T>(request: Promise<T>): Promise<T> {
511
+ try { return await request; } catch (error) {
512
+ if (error instanceof EndpointTransportError) throw error;
513
+ throw new EndpointTransportError(error instanceof Error ? error.message : String(error), { cause: error });
514
+ }
515
+ }
516
+
517
+ /** The wait a `Retry-After` header asks for, in milliseconds; either delay-seconds or an HTTP-date. */
518
+ export function retryAfterMs(headers: Headers, now = Date.now()): number | undefined {
519
+ const value = headers.get("retry-after")?.trim();
520
+ if (value === undefined || value.length === 0) return undefined;
521
+ const delay = /^\d+$/u.test(value) ? Number(value) * 1000 : Date.parse(value) - now;
522
+ return Number.isFinite(delay) ? Math.max(0, Math.round(delay)) : undefined;
523
+ }
524
+
525
+ /**
526
+ * Decide a poll action's outcome from the error it threw. Transport errors and HTTP 429/5xx keep the
527
+ * job pending for the next poll; every other error, including credential and response errors, fails it.
528
+ */
529
+ export function pollAgainOrFail(error: unknown, options: {
530
+ readonly handle: CanonicalValue;
531
+ readonly pollIntervalMs: number;
532
+ readonly failure: (error: unknown) => EndpointOutcome;
533
+ }): EndpointOutcome {
534
+ if (error instanceof EndpointTransportError) {
535
+ return wakeAfter(options.handle, options.pollIntervalMs, Date.now(), { phase: "retrying" });
536
+ }
537
+ if (error instanceof EndpointHttpError && (error.status === 429 || error.status >= 500)) {
538
+ return wakeAfter(options.handle, error.retryAfterMs ?? options.pollIntervalMs, Date.now(), { phase: "retrying" });
539
+ }
540
+ return options.failure(error);
541
+ }
@@ -1,3 +1,5 @@
1
+ import { EndpointHttpError, EndpointServiceError } from "@hypit/endpoint-kit";
2
+
1
3
  /** BeatAPI's `{ error: { code, message, request_id, retry_after_seconds } }` envelope and terminal task errors. */
2
4
  function record(value: unknown): Record<string, unknown> | undefined {
3
5
  return value !== null && typeof value === "object" && !Array.isArray(value)
@@ -12,12 +14,10 @@ export function safeBeatApiReason(value: string): string {
12
14
  return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
13
15
  }
14
16
 
15
- export class BeatApiServiceError extends Error {
16
- constructor(readonly code: string, message: string) { super(message); }
17
- }
17
+ export class BeatApiServiceError extends EndpointServiceError {}
18
18
 
19
- export class BeatApiHttpError extends BeatApiServiceError {
20
- constructor(readonly status: number, response: { readonly headers: Headers }, bodyText: string,
19
+ export class BeatApiHttpError extends EndpointHttpError {
20
+ constructor(status: number, response: { readonly headers: Headers }, bodyText: string,
21
21
  request: { readonly method: string; readonly path: string; readonly model?: string }) {
22
22
  let body: Record<string, unknown> | undefined;
23
23
  try { body = record(JSON.parse(bodyText)); } catch { /* Non-JSON gateway failures still have HTTP evidence. */ }
@@ -32,7 +32,8 @@ export class BeatApiHttpError extends BeatApiServiceError {
32
32
  ...(requestId === undefined ? [] : [`request=${requestId}`]),
33
33
  ...(retryAfter === undefined ? [] : [`retry-after=${retryAfter}s`]),
34
34
  ];
35
- super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeBeatApiReason(reason)}`}`);
35
+ super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeBeatApiReason(reason)}`}`,
36
+ status, retryAfter === undefined ? undefined : Math.round(retryAfter * 1000));
36
37
  }
37
38
  }
38
39
 
@@ -1,6 +1,6 @@
1
1
  import { requestDeadline } from "@hypit/runtime-kit";
2
2
  import type { AsyncEndpoint, EndpointCredential, EndpointInvocationContext, EndpointOutcome } from "@hypit/endpoint-kit";
3
- import { defineEndpointPackage, wakeAfter } from "@hypit/endpoint-kit";
3
+ import { EndpointResponseError, EndpointServiceError, EndpointTransportError, defineEndpointPackage, pollAgainOrFail, transport, wakeAfter } from "@hypit/endpoint-kit";
4
4
  import type { GenerationArtifactUrlResolver } from "@hypit/generation";
5
5
  import { canonicalize } from "@hypit/protocol";
6
6
  import type { BlobRef, CapabilityRef } from "@hypit/protocol";
@@ -75,18 +75,18 @@ function failureMessage(error: unknown): string {
75
75
  return error instanceof Error ? error.message : String(error);
76
76
  }
77
77
  function failure(error: unknown): EndpointOutcome {
78
- return { status: "failed", failure: { code: error instanceof BeatApiServiceError ? error.code : "BEATAPI_ERROR", message: failureMessage(error) } };
78
+ return { status: "failed", failure: { code: error instanceof EndpointServiceError ? error.code : "BEATAPI_ERROR", message: failureMessage(error) } };
79
79
  }
80
80
 
81
81
  class BeatApiClient {
82
82
  constructor(readonly baseUrl: string, readonly timeout: number, readonly fetcher: typeof globalThis.fetch) {}
83
83
  async json(path: string, key: string, init: RequestInit = {}): Promise<Record<string, unknown>> {
84
- const deadline = requestDeadline(this.timeout);
84
+ const deadline = requestDeadline(this.timeout, () => new EndpointTransportError("BeatAPI request timed out"));
85
85
  try {
86
- const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
86
+ const response = await transport(deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
87
87
  ...init, signal: deadline.signal, headers: { authorization: `Bearer ${key}`, ...(init.headers ?? {}) },
88
- }));
89
- const text = await deadline.wait(response.text());
88
+ })));
89
+ const text = await transport(deadline.wait(response.text()));
90
90
  if (!response.ok) {
91
91
  const input = typeof init.body === "string" ? JSON.parse(init.body) as Record<string, unknown> : undefined;
92
92
  throw new BeatApiHttpError(response.status, response, text, {
@@ -94,7 +94,7 @@ class BeatApiClient {
94
94
  });
95
95
  }
96
96
  let body: unknown;
97
- try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new Error(`BeatAPI returned invalid JSON (${response.status})`); }
97
+ try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new EndpointResponseError(`BeatAPI returned invalid JSON (${response.status})`); }
98
98
  return object(object(body, "BeatAPI response").data, "BeatAPI response data");
99
99
  } finally { deadline.finish(); }
100
100
  }
@@ -154,7 +154,7 @@ function endpoint(client: BeatApiClient, pollIntervalMs: number, maxOperationMs:
154
154
  try {
155
155
  body = await request.compile(resolverFor(client, context, publicAssetUrl));
156
156
  } catch (error) {
157
- throw new BeatApiServiceError(error instanceof BeatApiServiceError ? error.code : "BEATAPI_ERROR",
157
+ throw new BeatApiServiceError(error instanceof EndpointServiceError ? error.code : "BEATAPI_ERROR",
158
158
  `BeatAPI request preparation failed; model=${request.model}; generation not submitted: ${failureMessage(error)}`);
159
159
  }
160
160
  await context.reportProgress?.({ phase: `Submitting BeatAPI request: ${request.model}` });
@@ -181,13 +181,7 @@ function endpoint(client: BeatApiClient, pollIntervalMs: number, maxOperationMs:
181
181
  if (Date.now() - handle.startedAt > maxOperationMs) {
182
182
  return { status: "failed", receipt, failure: { code: "BEATAPI_OPERATION_TIMEOUT", message: `BeatAPI task ${handle.taskId} exceeded this Provider's operationTimeoutMs (${maxOperationMs}); remote outcome is unknown` } };
183
183
  }
184
- let task: Record<string, unknown>;
185
- try {
186
- task = await client.json(`/v1/tasks/${encodeURIComponent(handle.taskId)}`, apiKey(context.credentials));
187
- } catch (error) {
188
- if (error instanceof BeatApiHttpError && error.status < 500) return { ...failure(error), receipt };
189
- return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" }), receipt };
190
- }
184
+ const task = await client.json(`/v1/tasks/${encodeURIComponent(handle.taskId)}`, apiKey(context.credentials));
191
185
  const status = String(task.status);
192
186
  if (pendingStatuses.includes(status)) {
193
187
  return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
@@ -200,7 +194,7 @@ function endpoint(client: BeatApiClient, pollIntervalMs: number, maxOperationMs:
200
194
  const urls = media.map((item, index) => httpsUrl(object(item, `BeatAPI output ${index + 1}`).url, `BeatAPI output ${index + 1}`));
201
195
  return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
202
196
  } catch (error) {
203
- return failure(error);
197
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
204
198
  }
205
199
  },
206
200
  async collect(context) {
@@ -1,3 +1,5 @@
1
+ import { EndpointHttpError, EndpointServiceError } from "@hypit/endpoint-kit";
2
+
1
3
  /** HiAPI's `{ code, message, error_code }` envelope and task `error` object, kept at the service boundary. */
2
4
  function record(value: unknown): Record<string, unknown> | undefined {
3
5
  return value !== null && typeof value === "object" && !Array.isArray(value)
@@ -12,12 +14,10 @@ export function safeHiApiReason(value: string): string {
12
14
  return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
13
15
  }
14
16
 
15
- export class HiApiServiceError extends Error {
16
- constructor(readonly code: string, message: string) { super(message); }
17
- }
17
+ export class HiApiServiceError extends EndpointServiceError {}
18
18
 
19
- export class HiApiHttpError extends HiApiServiceError {
20
- constructor(readonly status: number, response: { readonly headers: Headers }, bodyText: string,
19
+ export class HiApiHttpError extends EndpointHttpError {
20
+ constructor(status: number, response: { readonly headers: Headers }, bodyText: string,
21
21
  request: { readonly method: string; readonly path: string; readonly model?: string }) {
22
22
  let body: Record<string, unknown> | undefined;
23
23
  try { body = record(JSON.parse(bodyText)); } catch { /* Non-JSON gateway failures still have HTTP evidence. */ }
@@ -29,7 +29,7 @@ export class HiApiHttpError extends HiApiServiceError {
29
29
  ...(request.model === undefined ? [] : [`model=${request.model}`]),
30
30
  ...(requestId === undefined ? [] : [`request=${requestId}`]),
31
31
  ];
32
- super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeHiApiReason(reason)}`}`);
32
+ super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeHiApiReason(reason)}`}`, status);
33
33
  }
34
34
  }
35
35
 
@@ -1,6 +1,6 @@
1
1
  import { requestDeadline } from "@hypit/runtime-kit";
2
2
  import type { AsyncEndpoint, EndpointCredential, EndpointInvocationContext, EndpointOutcome } from "@hypit/endpoint-kit";
3
- import { defineEndpointPackage, wakeAfter } from "@hypit/endpoint-kit";
3
+ import { EndpointResponseError, EndpointServiceError, EndpointTransportError, defineEndpointPackage, pollAgainOrFail, transport, wakeAfter } from "@hypit/endpoint-kit";
4
4
  import type { GenerationArtifactUrlResolver } from "@hypit/generation";
5
5
  import { canonicalize } from "@hypit/protocol";
6
6
  import type { BlobRef, CapabilityRef } from "@hypit/protocol";
@@ -56,18 +56,18 @@ function failureMessage(error: unknown): string {
56
56
  return error instanceof Error ? error.message : String(error);
57
57
  }
58
58
  function failure(error: unknown): EndpointOutcome {
59
- return { status: "failed", failure: { code: error instanceof HiApiServiceError ? error.code : "HIAPI_ERROR", message: failureMessage(error) } };
59
+ return { status: "failed", failure: { code: error instanceof EndpointServiceError ? error.code : "HIAPI_ERROR", message: failureMessage(error) } };
60
60
  }
61
61
 
62
62
  class HiApiClient {
63
63
  constructor(readonly baseUrl: string, readonly timeout: number, readonly fetcher: typeof globalThis.fetch) {}
64
64
  async json(path: string, key: string, init: RequestInit = {}): Promise<Record<string, unknown>> {
65
- const deadline = requestDeadline(this.timeout);
65
+ const deadline = requestDeadline(this.timeout, () => new EndpointTransportError("HiAPI request timed out"));
66
66
  try {
67
- const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
67
+ const response = await transport(deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
68
68
  ...init, signal: deadline.signal, headers: { authorization: `Bearer ${key}`, ...(init.headers ?? {}) },
69
- }));
70
- const text = await deadline.wait(response.text());
69
+ })));
70
+ const text = await transport(deadline.wait(response.text()));
71
71
  if (!response.ok) {
72
72
  const input = typeof init.body === "string" ? JSON.parse(init.body) as Record<string, unknown> : undefined;
73
73
  throw new HiApiHttpError(response.status, response, text, {
@@ -75,7 +75,7 @@ class HiApiClient {
75
75
  });
76
76
  }
77
77
  let body: unknown;
78
- try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new Error(`HiAPI returned invalid JSON (${response.status})`); }
78
+ try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new EndpointResponseError(`HiAPI returned invalid JSON (${response.status})`); }
79
79
  return object(object(body, "HiAPI response").data, "HiAPI response data");
80
80
  } finally { deadline.finish(); }
81
81
  }
@@ -130,7 +130,7 @@ function endpoint(client: HiApiClient, pollIntervalMs: number, maxOperationMs: n
130
130
  try {
131
131
  body = await request.compile(resolverFor(request.mediaLimits, context, publicAssetUrl));
132
132
  } catch (error) {
133
- throw new HiApiServiceError(error instanceof HiApiServiceError ? error.code : "HIAPI_ERROR",
133
+ throw new HiApiServiceError(error instanceof EndpointServiceError ? error.code : "HIAPI_ERROR",
134
134
  `HiAPI request preparation failed; model=${request.model}; generation not submitted: ${failureMessage(error)}`);
135
135
  }
136
136
  await context.reportProgress?.({ phase: `Submitting HiAPI request: ${request.model}` });
@@ -155,13 +155,7 @@ function endpoint(client: HiApiClient, pollIntervalMs: number, maxOperationMs: n
155
155
  if (Date.now() - handle.startedAt > maxOperationMs) {
156
156
  return { status: "failed", receipt, failure: { code: "HIAPI_OPERATION_TIMEOUT", message: `HiAPI task ${handle.taskId} exceeded this Provider's operationTimeoutMs (${maxOperationMs}); remote outcome is unknown` } };
157
157
  }
158
- let task: Record<string, unknown>;
159
- try {
160
- task = await client.json(`/v1/tasks/${encodeURIComponent(handle.taskId)}`, apiKey(context.credentials));
161
- } catch (error) {
162
- if (error instanceof HiApiHttpError && error.status < 500) return { ...failure(error), receipt };
163
- return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" }), receipt };
164
- }
158
+ const task = await client.json(`/v1/tasks/${encodeURIComponent(handle.taskId)}`, apiKey(context.credentials));
165
159
  const status = String(task.status);
166
160
  if (status === "queued" || status === "handling" || status === "archiving") {
167
161
  return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
@@ -177,7 +171,7 @@ function endpoint(client: HiApiClient, pollIntervalMs: number, maxOperationMs: n
177
171
  });
178
172
  return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
179
173
  } catch (error) {
180
- return failure(error);
174
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
181
175
  }
182
176
  },
183
177
  async collect(context) {
@@ -1,3 +1,5 @@
1
+ import { EndpointHttpError, EndpointServiceError, retryAfterMs } from "@hypit/endpoint-kit";
2
+
1
3
  /** HypiHub's public error envelope, kept at the service boundary. */
2
4
  function record(value: unknown): Record<string, unknown> | undefined {
3
5
  return value !== null && typeof value === "object" && !Array.isArray(value)
@@ -12,13 +14,10 @@ export function safeHypiHubReason(value: string): string {
12
14
  return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
13
15
  }
14
16
 
15
- export class HypiHubServiceError extends Error {
16
- constructor(readonly code: string, message: string) { super(message); }
17
- }
17
+ export class HypiHubServiceError extends EndpointServiceError {}
18
18
 
19
- export class HypiHubHttpError extends HypiHubServiceError {
20
- readonly retryAfterMs?: number;
21
- constructor(readonly status: number, response: { readonly headers: Headers }, bodyText: string,
19
+ export class HypiHubHttpError extends EndpointHttpError {
20
+ constructor(status: number, response: { readonly headers: Headers }, bodyText: string,
22
21
  request: { readonly method: string; readonly url: string; readonly model?: string }) {
23
22
  let body: Record<string, unknown> | undefined;
24
23
  try { body = record(JSON.parse(bodyText)); } catch { /* Non-JSON gateway failures still have HTTP evidence. */ }
@@ -41,10 +40,8 @@ export class HypiHubHttpError extends HypiHubServiceError {
41
40
  ...(retryAfter === undefined ? [] : [`retry-after=${retryAfter}`]),
42
41
  ];
43
42
  super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeHypiHubReason(reason)}`}`
44
- + (body === undefined && bodyText.length > 2000 ? " [response excerpt truncated]" : ""));
45
- const delay = retryAfter === undefined ? NaN : /^\d+$/u.test(retryAfter)
46
- ? Number(retryAfter) * 1000 : Date.parse(retryAfter) - Date.now();
47
- if (Number.isFinite(delay)) this.retryAfterMs = Math.max(0, delay);
43
+ + (body === undefined && bodyText.length > 2000 ? " [response excerpt truncated]" : ""),
44
+ status, retryAfterMs(response.headers));
48
45
  }
49
46
  }
50
47
 
@@ -1,4 +1,5 @@
1
1
  import type { EndpointCredential } from "@hypit/endpoint-kit";
2
+ import { EndpointTransportError, transport } from "@hypit/endpoint-kit";
2
3
  import { decodeOAuth2Credential, encodeOAuth2Credential } from "@hypit/runtime";
3
4
  import { requestDeadline } from "@hypit/runtime-kit";
4
5
  import { HypiHubHttpError } from "./errors.js";
@@ -66,9 +67,9 @@ export function createHypiHubAuth(options: {
66
67
  throw new Error("HypiHub OAuth credential is read-only; run hypit auth login with a writable Credential Store");
67
68
  }
68
69
  refreshing = (async () => {
69
- const deadline = requestDeadline(options.requestTimeoutMs, () => new Error("HypiHub OAuth refresh timed out"));
70
+ const deadline = requestDeadline(options.requestTimeoutMs, () => new EndpointTransportError("HypiHub OAuth refresh timed out"));
70
71
  try {
71
- const response = await deadline.wait(options.fetch(tokenEndpoint, {
72
+ const response = await transport(deadline.wait(options.fetch(tokenEndpoint, {
72
73
  method: "POST",
73
74
  headers: { "content-type": "application/x-www-form-urlencoded" },
74
75
  body: new URLSearchParams({
@@ -77,8 +78,8 @@ export function createHypiHubAuth(options: {
77
78
  client_id: OAUTH_CLIENT_ID,
78
79
  }),
79
80
  signal: deadline.signal,
80
- }));
81
- const text = await deadline.wait(response.text());
81
+ })));
82
+ const text = await transport(deadline.wait(response.text()));
82
83
  if (!response.ok) throw new HypiHubHttpError(response.status, response, text, {
83
84
  method: "POST", url: tokenEndpoint,
84
85
  });
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { requestDeadline } from "@hypit/runtime-kit";
3
3
  import type { AsyncEndpoint, EndpointCredential, EndpointFulfillment, EndpointInvocationContext, EndpointPollContext, EndpointPricingReader, EndpointStartContext, EndpointOutcome, ImmediateEndpointHandler } from "@hypit/endpoint-kit";
4
- import { defineEndpointPackage, wakeAfter } from "@hypit/endpoint-kit";
4
+ import { EndpointResponseError, EndpointServiceError, EndpointTransportError, defineEndpointPackage, pollAgainOrFail, transport, wakeAfter } from "@hypit/endpoint-kit";
5
5
  import { selectWireModelForRequest } from "@hypit/generation";
6
6
  import type { GenerationRequest } from "@hypit/generation";
7
7
  import { canonicalize } from "@hypit/protocol";
@@ -98,7 +98,7 @@ function failureMessage(error: unknown): string {
98
98
  }
99
99
  function failure(error: unknown): EndpointOutcome {
100
100
  const message = failureMessage(error);
101
- return { status: "failed", failure: { code: error instanceof HypiHubServiceError ? error.code : "HYPIHUB_ERROR", message } };
101
+ return { status: "failed", failure: { code: error instanceof EndpointServiceError ? error.code : "HYPIHUB_ERROR", message } };
102
102
  }
103
103
 
104
104
  function jobId(value: Record<string, unknown>): string {
@@ -150,11 +150,11 @@ class HypiHubClient {
150
150
  async json(path: string, auth: HypiHubAuth, init: RequestInit = {}, refreshOnUnauthorized = true,
151
151
  onResponse?: (response: Response) => Promise<void>): Promise<Record<string, unknown>> {
152
152
  const token = await auth.token();
153
- const deadline = requestDeadline(this.timeout);
153
+ const deadline = requestDeadline(this.timeout, () => new EndpointTransportError("HypiHub request timed out"));
154
154
  try {
155
- const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, { ...init, signal: deadline.signal, headers: { authorization: `Bearer ${token}`, ...(init.headers ?? {}) } }));
155
+ const response = await transport(deadline.wait(this.fetcher(`${this.baseUrl}${path}`, { ...init, signal: deadline.signal, headers: { authorization: `Bearer ${token}`, ...(init.headers ?? {}) } })));
156
156
  if (response.status !== 401 && onResponse !== undefined) await deadline.wait(onResponse(response));
157
- const text = await deadline.wait(response.text()); let body: unknown = {};
157
+ const text = await transport(deadline.wait(response.text())); let body: unknown = {};
158
158
  if (response.status === 401 && refreshOnUnauthorized && auth.canRefresh()) {
159
159
  deadline.finish();
160
160
  await auth.refresh();
@@ -167,7 +167,7 @@ class HypiHubClient {
167
167
  ...(typeof input?.model === "string" ? { model: input.model } : {}),
168
168
  });
169
169
  }
170
- try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new Error(`HypiHub returned invalid JSON (${response.status})`); }
170
+ try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new EndpointResponseError(`HypiHub returned invalid JSON (${response.status})`); }
171
171
  return object(body, "HypiHub response");
172
172
  } finally { deadline.finish(); }
173
173
  }
@@ -427,7 +427,7 @@ async function prepareGeneration(client: HypiHubClient, context: EndpointInvocat
427
427
  try {
428
428
  await verifyModelRoute(client, auth, request.model, request.operation);
429
429
  } catch (error) {
430
- throw new HypiHubServiceError(error instanceof HypiHubServiceError ? error.code : "HYPIHUB_ERROR",
430
+ throw new HypiHubServiceError(error instanceof EndpointServiceError ? error.code : "HYPIHUB_ERROR",
431
431
  `HypiHub model catalogue check failed; model=${request.model}; operation=${request.operation}; references uploaded=0; generation not submitted: ${failureMessage(error)}`);
432
432
  }
433
433
  await context.reportProgress?.({ phase: `Preparing HypiHub request: ${request.model} (${request.operation})` });
@@ -446,7 +446,7 @@ async function prepareGeneration(client: HypiHubClient, context: EndpointInvocat
446
446
  try {
447
447
  compiled = await request.compile(resolve);
448
448
  } catch (error) {
449
- throw new HypiHubServiceError(error instanceof HypiHubServiceError ? error.code : "HYPIHUB_ERROR",
449
+ throw new HypiHubServiceError(error instanceof EndpointServiceError ? error.code : "HYPIHUB_ERROR",
450
450
  `HypiHub request preparation failed; model=${request.model}; operation=${request.operation}; generation not submitted: ${failureMessage(error)}`);
451
451
  }
452
452
  return { route, auth, compiled, operation: request.operation };
@@ -494,21 +494,14 @@ function endpoint(client: HypiHubClient, pollIntervalMs: number, maxOperationMs:
494
494
  receipt: { id: handle.jobId },
495
495
  failure: { code: "HYPIHUB_OPERATION_TIMEOUT", message: `HypiHub job ${handle.jobId} exceeded this Provider's operationTimeoutMs (${maxOperationMs}); remote outcome is unknown` } };
496
496
  }
497
- let job: Record<string, unknown>;
498
- try {
499
- job = await client.json(`/jobs/${encodeURIComponent(handle.jobId)}`, authFor(context, client));
500
- } catch (error) {
501
- if (error instanceof HypiHubHttpError && error.status < 500) return failure(error);
502
- return wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" });
503
- }
504
- const status = job.status;
497
+ const job = await client.json(`/jobs/${encodeURIComponent(handle.jobId)}`, authFor(context, client)); const status = job.status;
505
498
  if (status === "queued" || status === "running" || status === "in_progress") return wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: String(status) });
506
499
  const rejected = hypiHubJobFailure(job, handle.jobId);
507
500
  if (rejected !== undefined) return { ...failure(rejected), receipt: { id: handle.jobId } };
508
501
  if (status !== "succeeded" && status !== "completed") throw new Error(`HypiHub returned unknown job status ${String(status)}`);
509
502
  return { status: "ready", handle: context.handle, receipt: { id: handle.jobId } };
510
503
  } catch (error) {
511
- return failure(error);
504
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
512
505
  }
513
506
  },
514
507
  async collect(context) {
@@ -1,8 +1,9 @@
1
+ import { EndpointServiceError } from "@hypit/endpoint-kit";
1
2
  import { requestDeadline } from "@hypit/runtime-kit";
2
3
  import { createHash } from "node:crypto";
3
4
 
4
5
  import type { HypiHubAuth } from "./oauth.js";
5
- import { HypiHubHttpError, HypiHubServiceError, safeHypiHubReason } from "./errors.js";
6
+ import { HypiHubHttpError, safeHypiHubReason } from "./errors.js";
6
7
 
7
8
  type UploadAuth = HypiHubAuth | string;
8
9
 
@@ -360,7 +361,7 @@ export class HypiHubUploader {
360
361
  this.log(`upload cancellation still pending upload=${uploadId} reason=${this.safeReason(cancelError)}`);
361
362
  }
362
363
  }
363
- throw error instanceof HypiHubServiceError ? error : new Error(this.safeReason(error));
364
+ throw error instanceof EndpointServiceError ? error : new Error(this.safeReason(error));
364
365
  }
365
366
  }
366
367
 
@@ -1,3 +1,5 @@
1
+ import { EndpointHttpError, EndpointServiceError } from "@hypit/endpoint-kit";
2
+
1
3
  /** Monid's `{ code, message }` error envelope and run outcome fields, kept at the service boundary. */
2
4
  function record(value: unknown): Record<string, unknown> | undefined {
3
5
  return value !== null && typeof value === "object" && !Array.isArray(value)
@@ -12,12 +14,10 @@ export function safeMonidReason(value: string): string {
12
14
  return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
13
15
  }
14
16
 
15
- export class MonidServiceError extends Error {
16
- constructor(readonly code: string, message: string) { super(message); }
17
- }
17
+ export class MonidServiceError extends EndpointServiceError {}
18
18
 
19
- export class MonidHttpError extends MonidServiceError {
20
- constructor(readonly status: number, response: { readonly headers: Headers }, bodyText: string,
19
+ export class MonidHttpError extends EndpointHttpError {
20
+ constructor(status: number, response: { readonly headers: Headers }, bodyText: string,
21
21
  request: { readonly method: string; readonly path: string }) {
22
22
  let body: Record<string, unknown> | undefined;
23
23
  try { body = record(JSON.parse(bodyText)); } catch { /* Non-JSON gateway failures still have HTTP evidence. */ }
@@ -27,7 +27,7 @@ export class MonidHttpError extends MonidServiceError {
27
27
  `Monid HTTP ${status}`, `${request.method} ${request.path}`,
28
28
  ...(requestId === undefined ? [] : [`request=${requestId}`]),
29
29
  ];
30
- super("MONID_HTTP_ERROR", `${facts.join("; ")}${reason === undefined ? "" : `: ${safeMonidReason(reason)}`}`);
30
+ super("MONID_HTTP_ERROR", `${facts.join("; ")}${reason === undefined ? "" : `: ${safeMonidReason(reason)}`}`, status);
31
31
  }
32
32
  }
33
33
 
@@ -1,6 +1,6 @@
1
1
  import { requestDeadline } from "@hypit/runtime-kit";
2
2
  import type { AsyncEndpoint, EndpointCredential, EndpointInvocationContext, EndpointOutcome } from "@hypit/endpoint-kit";
3
- import { defineEndpointPackage, wakeAfter } from "@hypit/endpoint-kit";
3
+ import { EndpointResponseError, EndpointServiceError, EndpointTransportError, defineEndpointPackage, pollAgainOrFail, transport, wakeAfter } from "@hypit/endpoint-kit";
4
4
  import type { GenerationArtifactUrlResolver } from "@hypit/generation";
5
5
  import { canonicalize } from "@hypit/protocol";
6
6
  import type { BlobRef, CapabilityRef } from "@hypit/protocol";
@@ -55,7 +55,7 @@ function failureMessage(error: unknown): string {
55
55
  return error instanceof Error ? error.message : String(error);
56
56
  }
57
57
  function failure(error: unknown): EndpointOutcome {
58
- return { status: "failed", failure: { code: error instanceof MonidServiceError ? error.code : "MONID_ERROR", message: failureMessage(error) } };
58
+ return { status: "failed", failure: { code: error instanceof EndpointServiceError ? error.code : "MONID_ERROR", message: failureMessage(error) } };
59
59
  }
60
60
  function runId(run: Record<string, unknown>): string {
61
61
  assert(typeof run.runId === "string" && run.runId.length > 0, "Monid response has no runId");
@@ -90,18 +90,18 @@ const extensions: Readonly<Record<string, string>> = {
90
90
  class MonidClient {
91
91
  constructor(readonly baseUrl: string, readonly timeout: number, readonly pollIntervalMs: number, readonly fetcher: typeof globalThis.fetch) {}
92
92
  async json(path: string, key: string, init: RequestInit = {}): Promise<{ readonly status: number; readonly body: Record<string, unknown> }> {
93
- const deadline = requestDeadline(this.timeout);
93
+ const deadline = requestDeadline(this.timeout, () => new EndpointTransportError("Monid request timed out"));
94
94
  try {
95
- const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
95
+ const response = await transport(deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
96
96
  ...init, signal: deadline.signal, headers: { authorization: `Bearer ${key}`, ...(init.headers ?? {}) },
97
- }));
98
- const text = await deadline.wait(response.text());
97
+ })));
98
+ const text = await transport(deadline.wait(response.text()));
99
99
  let body: unknown;
100
100
  try { body = text.length === 0 ? {} : JSON.parse(text); } catch { body = undefined; }
101
101
  // A synchronous run mirrors the provider's HTTP status while still returning the run itself.
102
102
  const run = body !== null && typeof body === "object" && !Array.isArray(body) && typeof (body as Record<string, unknown>).runId === "string";
103
103
  if (!response.ok && !run) throw new MonidHttpError(response.status, response, text, { method: init.method ?? "GET", path });
104
- assert(body !== undefined, `Monid returned invalid JSON (${response.status})`);
104
+ if (body === undefined) throw new EndpointResponseError(`Monid returned invalid JSON (${response.status})`);
105
105
  return { status: response.status, body: object(body, "Monid response") };
106
106
  } finally { deadline.finish(); }
107
107
  }
@@ -176,7 +176,7 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
176
176
  try {
177
177
  input = await request.compile(resolverFor(client, context, publicAssetUrl));
178
178
  } catch (error) {
179
- throw new MonidServiceError(error instanceof MonidServiceError ? error.code : "MONID_ERROR",
179
+ throw new MonidServiceError(error instanceof EndpointServiceError ? error.code : "MONID_ERROR",
180
180
  `Monid request preparation failed; endpoint=${request.endpoint}; generation not submitted: ${failureMessage(error)}`);
181
181
  }
182
182
  await context.reportProgress?.({ phase: `Submitting Monid request: ${request.service} ${request.endpoint}` });
@@ -202,13 +202,7 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
202
202
  if (Date.now() - handle.startedAt > maxOperationMs) {
203
203
  return { status: "failed", receipt, failure: { code: "MONID_OPERATION_TIMEOUT", message: `Monid run ${handle.runId} exceeded this Provider's operationTimeoutMs (${maxOperationMs}); remote outcome is unknown` } };
204
204
  }
205
- let run: Record<string, unknown>;
206
- try {
207
- run = await client.getRun(handle.runId, apiKey(context.credentials));
208
- } catch (error) {
209
- if (error instanceof MonidHttpError && error.status < 500) return { ...failure(error), receipt };
210
- return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" }), receipt };
211
- }
205
+ const run = await client.getRun(handle.runId, apiKey(context.credentials));
212
206
  const status = String(run.status);
213
207
  if (!monidTerminalStatuses.includes(status as typeof monidTerminalStatuses[number])) {
214
208
  return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
@@ -217,7 +211,7 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
217
211
  if (rejected !== undefined) return { ...failure(rejected), receipt };
218
212
  return { status: "ready", handle: canonicalize({ ...handle, urls: outputUrls(run) }), receipt };
219
213
  } catch (error) {
220
- return failure(error);
214
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
221
215
  }
222
216
  },
223
217
  async collect(context) {
@@ -1,3 +1,5 @@
1
+ import { EndpointHttpError, EndpointServiceError } from "@hypit/endpoint-kit";
2
+
1
3
  /** Pollo's `{ errorCode, message, code, requestId }` envelope and generation `failMsg`, kept at the service boundary. */
2
4
  function record(value: unknown): Record<string, unknown> | undefined {
3
5
  return value !== null && typeof value === "object" && !Array.isArray(value)
@@ -12,12 +14,10 @@ export function safePolloReason(value: string): string {
12
14
  return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
13
15
  }
14
16
 
15
- export class PolloServiceError extends Error {
16
- constructor(readonly code: string, message: string) { super(message); }
17
- }
17
+ export class PolloServiceError extends EndpointServiceError {}
18
18
 
19
- export class PolloHttpError extends PolloServiceError {
20
- constructor(readonly status: number, bodyText: string, request: { readonly method: string; readonly path: string }) {
19
+ export class PolloHttpError extends EndpointHttpError {
20
+ constructor(status: number, bodyText: string, request: { readonly method: string; readonly path: string }) {
21
21
  let body: Record<string, unknown> | undefined;
22
22
  try { body = record(JSON.parse(bodyText)); } catch { /* Non-JSON gateway failures still have HTTP evidence. */ }
23
23
  const code = text(body?.errorCode) ?? "POLLO_HTTP_ERROR";
@@ -27,7 +27,7 @@ export class PolloHttpError extends PolloServiceError {
27
27
  `Pollo HTTP ${status}`, code, `${request.method} ${request.path}`,
28
28
  ...(requestId === undefined ? [] : [`request=${requestId}`]),
29
29
  ];
30
- super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safePolloReason(reason)}`}`);
30
+ super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safePolloReason(reason)}`}`, status);
31
31
  }
32
32
  }
33
33
 
@@ -1,6 +1,6 @@
1
1
  import { requestDeadline } from "@hypit/runtime-kit";
2
2
  import type { AsyncEndpoint, EndpointCredential, EndpointInvocationContext, EndpointOutcome } from "@hypit/endpoint-kit";
3
- import { defineEndpointPackage, wakeAfter } from "@hypit/endpoint-kit";
3
+ import { EndpointResponseError, EndpointServiceError, EndpointTransportError, defineEndpointPackage, pollAgainOrFail, transport, wakeAfter } from "@hypit/endpoint-kit";
4
4
  import type { GenerationArtifactUrlResolver } from "@hypit/generation";
5
5
  import { canonicalize } from "@hypit/protocol";
6
6
  import type { BlobRef, CapabilityRef } from "@hypit/protocol";
@@ -55,21 +55,21 @@ function failureMessage(error: unknown): string {
55
55
  return error instanceof Error ? error.message : String(error);
56
56
  }
57
57
  function failure(error: unknown): EndpointOutcome {
58
- return { status: "failed", failure: { code: error instanceof PolloServiceError ? error.code : "POLLO_ERROR", message: failureMessage(error) } };
58
+ return { status: "failed", failure: { code: error instanceof EndpointServiceError ? error.code : "POLLO_ERROR", message: failureMessage(error) } };
59
59
  }
60
60
 
61
61
  class PolloClient {
62
62
  constructor(readonly baseUrl: string, readonly timeout: number, readonly fetcher: typeof globalThis.fetch) {}
63
63
  async json(path: string, key: string, init: RequestInit = {}): Promise<Record<string, unknown>> {
64
- const deadline = requestDeadline(this.timeout);
64
+ const deadline = requestDeadline(this.timeout, () => new EndpointTransportError("Pollo request timed out"));
65
65
  try {
66
- const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
66
+ const response = await transport(deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
67
67
  ...init, signal: deadline.signal, headers: { "x-api-key": key, ...(init.headers ?? {}) },
68
- }));
69
- const text = await deadline.wait(response.text());
68
+ })));
69
+ const text = await transport(deadline.wait(response.text()));
70
70
  if (!response.ok) throw new PolloHttpError(response.status, text, { method: init.method ?? "GET", path });
71
71
  let body: unknown;
72
- try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new Error(`Pollo returned invalid JSON (${response.status})`); }
72
+ try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new EndpointResponseError(`Pollo returned invalid JSON (${response.status})`); }
73
73
  return object(body, "Pollo response");
74
74
  } finally { deadline.finish(); }
75
75
  }
@@ -108,7 +108,7 @@ function endpoint(client: PolloClient, pollIntervalMs: number, maxOperationMs: n
108
108
  try {
109
109
  body = await request.compile(resolverFor(context, publicAssetUrl));
110
110
  } catch (error) {
111
- throw new PolloServiceError(error instanceof PolloServiceError ? error.code : "POLLO_ERROR",
111
+ throw new PolloServiceError(error instanceof EndpointServiceError ? error.code : "POLLO_ERROR",
112
112
  `Pollo request preparation failed; path=${request.path}; generation not submitted: ${failureMessage(error)}`);
113
113
  }
114
114
  await context.reportProgress?.({ phase: `Submitting Pollo request: ${request.path}` });
@@ -133,13 +133,7 @@ function endpoint(client: PolloClient, pollIntervalMs: number, maxOperationMs: n
133
133
  if (Date.now() - handle.startedAt > maxOperationMs) {
134
134
  return { status: "failed", receipt, failure: { code: "POLLO_OPERATION_TIMEOUT", message: `Pollo task ${handle.taskId} exceeded this Provider's operationTimeoutMs (${maxOperationMs}); remote outcome is unknown` } };
135
135
  }
136
- let task: Record<string, unknown>;
137
- try {
138
- task = await client.json(`/v1/generation/${encodeURIComponent(handle.taskId)}/status`, apiKey(context.credentials));
139
- } catch (error) {
140
- if (error instanceof PolloHttpError && error.status < 500) return { ...failure(error), receipt };
141
- return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" }), receipt };
142
- }
136
+ const task = await client.json(`/v1/generation/${encodeURIComponent(handle.taskId)}/status`, apiKey(context.credentials));
143
137
  assert(Array.isArray(task.generations) && task.generations.length > 0, "Pollo task has no generations");
144
138
  const generations = task.generations.map((item, index) => object(item, `Pollo generation ${index + 1}`));
145
139
  const rejected = polloTaskFailure(generations, handle.taskId);
@@ -156,7 +150,7 @@ function endpoint(client: PolloClient, pollIntervalMs: number, maxOperationMs: n
156
150
  });
157
151
  return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
158
152
  } catch (error) {
159
- return failure(error);
153
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
160
154
  }
161
155
  },
162
156
  async collect(context) {
@@ -1,3 +1,5 @@
1
+ import { EndpointHttpError, EndpointServiceError } from "@hypit/endpoint-kit";
2
+
1
3
  /** TokenDance relays each protocol's own error body; keep the code, the message and the HTTP facts. */
2
4
  function record(value: unknown): Record<string, unknown> | undefined {
3
5
  return value !== null && typeof value === "object" && !Array.isArray(value)
@@ -12,12 +14,10 @@ export function safeTokenDanceReason(value: string): string {
12
14
  return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
13
15
  }
14
16
 
15
- export class TokenDanceServiceError extends Error {
16
- constructor(readonly code: string, message: string) { super(message); }
17
- }
17
+ export class TokenDanceServiceError extends EndpointServiceError {}
18
18
 
19
- export class TokenDanceHttpError extends TokenDanceServiceError {
20
- constructor(readonly status: number, response: { readonly headers: Headers }, bodyText: string,
19
+ export class TokenDanceHttpError extends EndpointHttpError {
20
+ constructor(status: number, response: { readonly headers: Headers }, bodyText: string,
21
21
  request: { readonly method: string; readonly path: string; readonly model?: string }) {
22
22
  let body: Record<string, unknown> | undefined;
23
23
  try { body = record(JSON.parse(bodyText)); } catch { /* Non-JSON gateway failures still have HTTP evidence. */ }
@@ -31,7 +31,7 @@ export class TokenDanceHttpError extends TokenDanceServiceError {
31
31
  ...(request.model === undefined ? [] : [`model=${request.model}`]),
32
32
  ...(requestId === undefined ? [] : [`request=${requestId}`]),
33
33
  ];
34
- super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeTokenDanceReason(reason)}`}`);
34
+ super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeTokenDanceReason(reason)}`}`, status);
35
35
  }
36
36
  }
37
37
 
@@ -1,6 +1,6 @@
1
1
  import { requestDeadline } from "@hypit/runtime-kit";
2
2
  import type { AsyncEndpoint, EndpointCredential, EndpointInvocationContext, EndpointOutcome, ImmediateEndpointHandler } from "@hypit/endpoint-kit";
3
- import { defineEndpointPackage, wakeAfter } from "@hypit/endpoint-kit";
3
+ import { EndpointResponseError, EndpointServiceError, EndpointTransportError, defineEndpointPackage, pollAgainOrFail, transport, wakeAfter } from "@hypit/endpoint-kit";
4
4
  import type { GenerationArtifactUrlResolver } from "@hypit/generation";
5
5
  import { canonicalize } from "@hypit/protocol";
6
6
  import type { BlobRef, CapabilityRef } from "@hypit/protocol";
@@ -56,7 +56,7 @@ function failureMessage(error: unknown): string {
56
56
  return error instanceof Error ? error.message : String(error);
57
57
  }
58
58
  function failure(error: unknown): EndpointOutcome {
59
- return { status: "failed", failure: { code: error instanceof TokenDanceServiceError ? error.code : "TOKENDANCE_ERROR", message: failureMessage(error) } };
59
+ return { status: "failed", failure: { code: error instanceof EndpointServiceError ? error.code : "TOKENDANCE_ERROR", message: failureMessage(error) } };
60
60
  }
61
61
  function httpsUrl(value: unknown, subject: string): string {
62
62
  assert(typeof value === "string" && /^https?:\/\//u.test(value), `${subject} has no download URL`);
@@ -66,12 +66,12 @@ function httpsUrl(value: unknown, subject: string): string {
66
66
  class TokenDanceClient {
67
67
  constructor(readonly baseUrl: string, readonly timeout: number, readonly fetcher: typeof globalThis.fetch) {}
68
68
  async json(path: string, key: string, init: RequestInit = {}): Promise<Record<string, unknown>> {
69
- const deadline = requestDeadline(this.timeout);
69
+ const deadline = requestDeadline(this.timeout, () => new EndpointTransportError("TokenDance request timed out"));
70
70
  try {
71
- const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
71
+ const response = await transport(deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
72
72
  ...init, signal: deadline.signal, headers: { authorization: `Bearer ${key}`, ...(init.headers ?? {}) },
73
- }));
74
- const text = await deadline.wait(response.text());
73
+ })));
74
+ const text = await transport(deadline.wait(response.text()));
75
75
  if (!response.ok) {
76
76
  const input = typeof init.body === "string" ? JSON.parse(init.body) as Record<string, unknown> : undefined;
77
77
  throw new TokenDanceHttpError(response.status, response, text, {
@@ -79,7 +79,7 @@ class TokenDanceClient {
79
79
  });
80
80
  }
81
81
  let body: unknown;
82
- try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new Error(`TokenDance returned invalid JSON (${response.status})`); }
82
+ try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new EndpointResponseError(`TokenDance returned invalid JSON (${response.status})`); }
83
83
  return object(body, "TokenDance response");
84
84
  } finally { deadline.finish(); }
85
85
  }
@@ -145,7 +145,7 @@ async function prepare(client: TokenDanceClient, context: EndpointInvocationCont
145
145
  assert(cap === undefined || Buffer.byteLength(body) <= cap,
146
146
  `TokenDance ${route.protocol} accepts request bodies up to ${(cap ?? 0) / 1_000_000} MB; inline references make this one ${Buffer.byteLength(body)} bytes`);
147
147
  } catch (error) {
148
- throw new TokenDanceServiceError(error instanceof TokenDanceServiceError ? error.code : "TOKENDANCE_ERROR",
148
+ throw new TokenDanceServiceError(error instanceof EndpointServiceError ? error.code : "TOKENDANCE_ERROR",
149
149
  `TokenDance request preparation failed; model=${request.model}; generation not submitted: ${failureMessage(error)}`);
150
150
  }
151
151
  return { route, model: request.model, body };
@@ -208,14 +208,7 @@ function endpoint(client: TokenDanceClient, pollIntervalMs: number, maxOperation
208
208
  if (Date.now() - handle.startedAt > maxOperationMs) {
209
209
  return { status: "failed", receipt, failure: { code: "TOKENDANCE_OPERATION_TIMEOUT", message: `TokenDance task ${handle.taskId} exceeded this Provider's operationTimeoutMs (${maxOperationMs}); remote outcome is unknown` } };
210
210
  }
211
- let response: Record<string, unknown>;
212
- try {
213
- response = await client.json(paths[route.protocol].task(handle.taskId), apiKey(context.credentials));
214
- } catch (error) {
215
- if (error instanceof TokenDanceHttpError && error.status < 500) return { ...failure(error), receipt };
216
- return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" }), receipt };
217
- }
218
- const task = taskBody(route.protocol, response);
211
+ const task = taskBody(route.protocol, await client.json(paths[route.protocol].task(handle.taskId), apiKey(context.credentials)));
219
212
  const status = String(task.status);
220
213
  if (status === "queued" || status === "running") return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
221
214
  const rejected = tokenDanceTaskFailure(task, handle.taskId);
@@ -223,7 +216,7 @@ function endpoint(client: TokenDanceClient, pollIntervalMs: number, maxOperation
223
216
  assert(status === "succeeded", `TokenDance returned unknown task status ${status}`);
224
217
  return { status: "ready", handle: canonicalize({ ...handle, url: taskVideoUrl(route.protocol, task) }), receipt };
225
218
  } catch (error) {
226
- return failure(error);
219
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
227
220
  }
228
221
  },
229
222
  async collect(context) {