@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.
- package/dist/public/endpoint-kit.d.ts +31 -1
- package/package.json +1 -1
- package/packages/endpoint-kit/src/index.ts +47 -0
- package/packages/provider-beatapi/src/errors.ts +7 -6
- package/packages/provider-beatapi/src/provider.ts +10 -16
- package/packages/provider-hiapi/src/errors.ts +6 -6
- package/packages/provider-hiapi/src/provider.ts +10 -16
- package/packages/provider-hypihub/src/errors.ts +7 -10
- package/packages/provider-hypihub/src/oauth.ts +5 -4
- package/packages/provider-hypihub/src/provider.ts +10 -17
- package/packages/provider-hypihub/src/upload.ts +3 -2
- package/packages/provider-monid/src/errors.ts +6 -6
- package/packages/provider-monid/src/provider.ts +10 -16
- package/packages/provider-pollo/src/errors.ts +6 -6
- package/packages/provider-pollo/src/provider.ts +10 -16
- package/packages/provider-tokendance/src/errors.ts +6 -6
- package/packages/provider-tokendance/src/provider.ts +10 -17
|
@@ -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
|
@@ -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
|
|
16
|
-
constructor(readonly code: string, message: string) { super(message); }
|
|
17
|
-
}
|
|
17
|
+
export class BeatApiServiceError extends EndpointServiceError {}
|
|
18
18
|
|
|
19
|
-
export class BeatApiHttpError extends
|
|
20
|
-
constructor(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
16
|
-
constructor(readonly code: string, message: string) { super(message); }
|
|
17
|
-
}
|
|
17
|
+
export class HiApiServiceError extends EndpointServiceError {}
|
|
18
18
|
|
|
19
|
-
export class HiApiHttpError extends
|
|
20
|
-
constructor(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
16
|
-
constructor(readonly code: string, message: string) { super(message); }
|
|
17
|
-
}
|
|
17
|
+
export class HypiHubServiceError extends EndpointServiceError {}
|
|
18
18
|
|
|
19
|
-
export class HypiHubHttpError extends
|
|
20
|
-
readonly
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
16
|
-
constructor(readonly code: string, message: string) { super(message); }
|
|
17
|
-
}
|
|
17
|
+
export class MonidServiceError extends EndpointServiceError {}
|
|
18
18
|
|
|
19
|
-
export class MonidHttpError extends
|
|
20
|
-
constructor(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
16
|
-
constructor(readonly code: string, message: string) { super(message); }
|
|
17
|
-
}
|
|
17
|
+
export class PolloServiceError extends EndpointServiceError {}
|
|
18
18
|
|
|
19
|
-
export class PolloHttpError extends
|
|
20
|
-
constructor(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
16
|
-
constructor(readonly code: string, message: string) { super(message); }
|
|
17
|
-
}
|
|
17
|
+
export class TokenDanceServiceError extends EndpointServiceError {}
|
|
18
18
|
|
|
19
|
-
export class TokenDanceHttpError extends
|
|
20
|
-
constructor(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
219
|
+
return pollAgainOrFail(error, { handle: context.handle, pollIntervalMs, failure });
|
|
227
220
|
}
|
|
228
221
|
},
|
|
229
222
|
async collect(context) {
|