@hypit/hypit 0.2.10 → 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.
package/README.md CHANGED
@@ -17,7 +17,11 @@
17
17
 
18
18
  <p align="center">
19
19
  <a href="https://trendshift.io/repositories/229030"><img alt="Trendshift #1 Repository of the Day" src="https://trendshift.io/api/badge/trendshift/repositories/229030/daily"></a>
20
+ </p>
21
+
22
+ <p align="center">
20
23
  <a href="https://trendshift.io/repositories/229030"><img alt="Trendshift #1 TypeScript Repository of the Day" src="https://trendshift.io/api/badge/trendshift/repositories/229030/daily?language=TypeScript"></a>
24
+ <a href="https://trendshift.io/repositories/229030?utm_source=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-229030" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/229030/weekly?language=TypeScript" alt="hypit-ai/hypit | Trendshift TypeScript Weekly" width="250" height="55"></a>
21
25
  </p>
22
26
 
23
27
  <p align="center">
@@ -154,14 +158,14 @@ commands and the repository layout.
154
158
  <table>
155
159
  <tr>
156
160
  <td width="220" align="center">
157
- <a href="https://watcha.cn">
161
+ <a href="https://watcha.cn/r/geIiJJ">
158
162
  <picture>
159
163
  <source media="(prefers-color-scheme: dark)" srcset="./docs/public/watcha-dark.png">
160
164
  <img alt="Watcha" src="./docs/public/watcha-light.png" width="170">
161
165
  </picture>
162
166
  </a>
163
167
  </td>
164
- <td><a href="https://watcha.cn">Watcha</a> is an innovative ecosystem and community built for AI builders, connecting great products with high-quality users to turn every Demo into reality. Starting from AI product discovery and genuine feedback, Watcha provides product publishing, distribution, and reputation tracking to support builders from launch to early adoption.</td>
168
+ <td><a href="https://watcha.cn/r/geIiJJ">Watcha</a> is an innovative ecosystem and community built for AI builders, connecting great products with high-quality users to turn every Demo into reality. Starting from AI product discovery and genuine feedback, Watcha provides product publishing, distribution, and reputation tracking to support builders from launch to early adoption.</td>
165
169
  </tr>
166
170
  <tr>
167
171
  <td width="220" align="center">
@@ -211,9 +215,9 @@ commands and the repository layout.
211
215
  </tr>
212
216
  <tr>
213
217
  <td width="220" align="center">
214
- <a href="https://beatapi.io"><img alt="BeatAPI" src="./docs/public/beatapi.png" width="165"></a>
218
+ <a href="https://beatapi.io/sign-up?aff=hypit"><img alt="BeatAPI" src="./docs/public/beatapi.png" width="165"></a>
215
219
  </td>
216
- <td><a href="https://beatapi.io">BeatAPI</a> is the professional capability layer for any agent. We provide AI models, real-world data, task-ready capabilities, and professional workspaces that help agents complete real-world tasks.</td>
220
+ <td><a href="https://beatapi.io/sign-up?aff=hypit">BeatAPI</a> is the professional capability layer for any agent. We provide AI models, real-world data, task-ready capabilities, and professional workspaces that help agents complete real-world tasks.</td>
217
221
  </tr>
218
222
  </table>
219
223
 
@@ -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.10",
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
+ }
@@ -4,7 +4,7 @@ Exact author/compute contracts and package-owned author Surfaces for PixVerse. T
4
4
  two exact models, `pixverse-v6` and `pixverse-c1`, and selects no Provider, API key or network
5
5
  execution. The selected Provider implements the exact capability.
6
6
 
7
- Both models render 1 to 15 seconds at `360p`, `540p`, `720p` or `1080p` from a prompt of up to
7
+ Both models render 1 to 15 seconds at `540p` or `720p` from a prompt of up to
8
8
  5,000 characters. `<pix:Video>` generates from the prompt, from a first frame, or from a first and
9
9
  last frame; `<pix:ReferenceVideo>` generates from the image and video subjects its `Reference`
10
10
  children carry.
@@ -17,7 +17,7 @@ export const pixverseModuleRef = { name: "@hypit/pixverse", version: "1" } as co
17
17
  export const pixverseModels = ["pixverse-v6", "pixverse-c1"] as const;
18
18
  export type PixverseModel = typeof pixverseModels[number];
19
19
 
20
- const PIXVERSE_QUALITIES = ["360p", "540p", "720p", "1080p"] as const;
20
+ const PIXVERSE_QUALITIES = ["540p", "720p"] as const;
21
21
  const PIXVERSE_ASPECT_RATIOS = ["16:9", "4:3", "1:1", "3:4", "9:16", "2:3", "3:2", "21:9"] as const;
22
22
  /** V6 reads `auto` as the shape of the reference videos it generates from. */
23
23
  const PIXVERSE_V6_ASPECT_RATIOS = [...PIXVERSE_ASPECT_RATIOS, "auto"] as const;
@@ -142,7 +142,7 @@ const pixverseVideoPort: readonly SurfacePortVocabulary[] = [{
142
142
  summary: "The generated video, addressed as `<id>.video`.",
143
143
  }];
144
144
 
145
- const pixverseQualityNote = "`quality` is `360p`, `540p`, `720p` or `1080p`, and `duration` is 1 to 15 seconds.";
145
+ const pixverseQualityNote = "`quality` is `540p` or `720p`, and `duration` is 1 to 15 seconds.";
146
146
  const pixversePromptNote = "A spoken line belongs in the prompt; the model exposes no separate voice, language or dialogue field.";
147
147
  const pixverseModelNote = "V6 accepts `seed` and up to ten references; C1 accepts up to seven references.";
148
148
 
@@ -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}` });
@@ -194,7 +194,7 @@ function endpoint(client: BeatApiClient, pollIntervalMs: number, maxOperationMs:
194
194
  const urls = media.map((item, index) => httpsUrl(object(item, `BeatAPI output ${index + 1}`).url, `BeatAPI output ${index + 1}`));
195
195
  return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
196
196
  } catch (error) {
197
- return failure(error);
197
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
198
198
  }
199
199
  },
200
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}` });
@@ -171,7 +171,7 @@ function endpoint(client: HiApiClient, pollIntervalMs: number, maxOperationMs: n
171
171
  });
172
172
  return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
173
173
  } catch (error) {
174
- return failure(error);
174
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
175
175
  }
176
176
  },
177
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 };
@@ -501,7 +501,7 @@ function endpoint(client: HypiHubClient, pollIntervalMs: number, maxOperationMs:
501
501
  if (status !== "succeeded" && status !== "completed") throw new Error(`HypiHub returned unknown job status ${String(status)}`);
502
502
  return { status: "ready", handle: context.handle, receipt: { id: handle.jobId } };
503
503
  } catch (error) {
504
- return failure(error);
504
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
505
505
  }
506
506
  },
507
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}` });
@@ -211,7 +211,7 @@ function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: n
211
211
  if (rejected !== undefined) return { ...failure(rejected), receipt };
212
212
  return { status: "ready", handle: canonicalize({ ...handle, urls: outputUrls(run) }), receipt };
213
213
  } catch (error) {
214
- return failure(error);
214
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
215
215
  }
216
216
  },
217
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}` });
@@ -150,7 +150,7 @@ function endpoint(client: PolloClient, pollIntervalMs: number, maxOperationMs: n
150
150
  });
151
151
  return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
152
152
  } catch (error) {
153
- return failure(error);
153
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
154
154
  }
155
155
  },
156
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 };
@@ -216,7 +216,7 @@ function endpoint(client: TokenDanceClient, pollIntervalMs: number, maxOperation
216
216
  assert(status === "succeeded", `TokenDance returned unknown task status ${status}`);
217
217
  return { status: "ready", handle: canonicalize({ ...handle, url: taskVideoUrl(route.protocol, task) }), receipt };
218
218
  } catch (error) {
219
- return failure(error);
219
+ return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
220
220
  }
221
221
  },
222
222
  async collect(context) {