@hypit/hypit 0.2.9 → 0.2.11

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">
@@ -209,6 +213,12 @@ commands and the repository layout.
209
213
  </td>
210
214
  <td><a href="https://api.pollo.ai/?utm_source=hypit">Pollo API</a> gives developers access to 300+ AI image and video models through a single API, with competitive rates. One API key connects you to multiple model providers and scales from 10 to 10,000+ requests.</td>
211
215
  </tr>
216
+ <tr>
217
+ <td width="220" align="center">
218
+ <a href="https://beatapi.io/sign-up?aff=hypit"><img alt="BeatAPI" src="./docs/public/beatapi.png" width="165"></a>
219
+ </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>
221
+ </tr>
212
222
  </table>
213
223
 
214
224
  ## ⭐ Star History
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypit/hypit",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "homepage": "https://hypit.ai",
5
5
  "repository": {
6
6
  "type": "git",
@@ -236,8 +236,9 @@
236
236
  "@hypit/program-space": "workspace:*",
237
237
  "@hypit/project-context-node": "workspace:*",
238
238
  "@hypit/protocol": "workspace:*",
239
- "@hypit/provider-hyperframes-local": "workspace:*",
239
+ "@hypit/provider-beatapi": "workspace:*",
240
240
  "@hypit/provider-hiapi": "workspace:*",
241
+ "@hypit/provider-hyperframes-local": "workspace:*",
241
242
  "@hypit/provider-hypihub": "workspace:*",
242
243
  "@hypit/provider-image-opencv-local": "workspace:*",
243
244
  "@hypit/provider-media-local": "workspace:*",
@@ -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
 
@@ -0,0 +1,84 @@
1
+ # `@hypit/provider-beatapi`
2
+
3
+ Hypit Runtime Provider for a [BeatAPI](https://docs.beatapi.io/quick-guide) account. Each capability
4
+ posts one task to `https://api.beatapi.io` with a `Bearer` API key, polls `GET /v1/tasks/{task_id}`
5
+ until it reaches `succeeded`, downloads every entry of `output.media` and stores the files in the
6
+ current Build.
7
+
8
+ | Capability | BeatAPI model | Task path |
9
+ | --- | --- | --- |
10
+ | `@hypit/seedance@1#seedance-2` | `seedance-2` | `/v1/videos/tasks` |
11
+ | `@hypit/seedance@1#seedance-2-fast` | `seedance-2-fast` | `/v1/videos/tasks` |
12
+ | `@hypit/seedance@1#seedance-2-mini` | `seedance-2-mini` | `/v1/videos/tasks` |
13
+ | `@hypit/seedance@1#seedance-2.5` | `seedance-2.5` | `/v1/videos/tasks` |
14
+ | `@hypit/minimax-h3@1#minimax-h3` | `minimax-h3` | `/v1/videos/tasks` |
15
+ | `@hypit/grok-imagine@1#grok-imagine-video-1.5-preview` | `grok-imagine-video-1.5` | `/v1/videos/tasks` |
16
+ | `@hypit/gpt-image@1#gpt-image-2` | `gpt-image-2` | `/v1/images/tasks` |
17
+ | `@hypit/nano-banana@1#nano-banana-2` | `nano-banana-2` | `/v1/images/tasks` |
18
+ | `@hypit/nano-banana@1#nano-banana-pro` | `nano-banana-pro` | `/v1/images/tasks` |
19
+
20
+ BeatAPI documents further models, including Veo, Kling, Wan 3.0 and HappyHorse; this Provider maps
21
+ only the models the Distribution already describes.
22
+
23
+ One BeatAPI alias serves every input mode, so the request shape alone decides which arrays travel:
24
+ `reference_images`, `reference_videos` and `reference_audios` carry subject references, while the
25
+ opening and closing frames travel as one ordered `images` array. `duration`, `aspect_ratio`,
26
+ `resolution` and Seedance's `generate_audio` keep their authored values. Nano Banana 2 spells its
27
+ JPEG output `jpeg`, so an authored `output-format="jpg"` is sent as that.
28
+
29
+ Service limits this Provider reports as unsupported before uploading any reference:
30
+
31
+ - Seedance has no `web_search` field, and `seedance-2-mini` renders no generated audio. An authored
32
+ `false` is dropped; an authored `true` is refused.
33
+ - Seedance 2 does not render 1080p together with reference images.
34
+ - Seedance 2.5 takes 4 to 30 seconds, so `duration="-1"` for an automatic length is refused.
35
+ - MiniMax H3 orders its two frames first then last, so a last frame needs a first frame.
36
+ - Grok Imagine 1.5 accepts at most one image at 1080p.
37
+ - GPT Image 2 has no `background` field.
38
+ - Nano Banana 2 accepts up to ten reference images and does not render `1:4`, `4:1`, `1:8` or `8:1`.
39
+
40
+ Seedance visual references carry `person-reference`; the Provider accepts the declaration and
41
+ transmits nothing for it, since BeatAPI's task body has no field for it.
42
+
43
+ Referenced Resources are uploaded to `POST /v1/files` with `purpose=input`, and the returned HTTPS
44
+ URL travels in the task body. The service accepts PNG, JPEG and WebP images and MP3, WAV, AAC and
45
+ M4A audio up to 50 MB each, and MP4 and MOV video up to 100 MB; a reference outside those types or
46
+ sizes is reported before submission. One Resource is uploaded once per Runtime operation. Embedded
47
+ callers may replace that transport with `publicAssetUrl(artifact, resources, fields)`, which must
48
+ return a URL BeatAPI can fetch.
49
+
50
+ Runtime Profile example:
51
+
52
+ ```json
53
+ {
54
+ "format": "hypit.runtime-local@1",
55
+ "dataRoot": ".hypit/runtimes/local",
56
+ "credentials": {
57
+ "platform": {
58
+ "use": "@hypit/credential-store-platform"
59
+ }
60
+ },
61
+ "endpoints": {
62
+ "beatapi.default": {
63
+ "use": "@hypit/provider-beatapi",
64
+ "pool": "beatapi.default",
65
+ "config": {
66
+ "apiKey": { "store": "platform", "key": "beatapi.api-key" },
67
+ "defaultConcurrency": 3,
68
+ "pollIntervalMs": 10000
69
+ }
70
+ }
71
+ },
72
+ "bindings": {}
73
+ }
74
+ ```
75
+
76
+ `baseUrl` defaults to `https://api.beatapi.io`. Store the API key with
77
+ `hypit auth login beatapi.default --runtime hypit.runtime.json`. Optional `requestTimeoutMs`,
78
+ `operationTimeoutMs` and `actionLimits` bound single HTTP calls, the whole remote task and action
79
+ concurrency. Each submission sends the Runtime operation as its `Idempotency-Key`, so a retried
80
+ submission of the same body resolves to the same task.
81
+
82
+ HTTP failures keep BeatAPI's `error.code`, message, `request_id` and any `retry_after_seconds`; a
83
+ `failed` task keeps its `error_code` and `error_message`. Any URL inside a surfaced reason is
84
+ redacted, since a hosted result URL is an access capability rather than diagnostic content.
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@hypit/provider-beatapi",
3
+ "version": "0.0.0-dev",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "private": true,
6
+ "type": "module",
7
+ "exports": { ".": "./src/index.ts" },
8
+ "hypit": { "activation": "./src/activation.ts" },
9
+ "dependencies": {
10
+ "@hypit/endpoint-kit": "workspace:*",
11
+ "@hypit/generation": "workspace:*",
12
+ "@hypit/protocol": "workspace:*",
13
+ "@hypit/runtime": "workspace:*",
14
+ "@hypit/runtime-kit": "workspace:*"
15
+ },
16
+ "devDependencies": {
17
+ "@hypit/gpt-image": "workspace:*",
18
+ "@hypit/grok-imagine": "workspace:*",
19
+ "@hypit/minimax-h3": "workspace:*",
20
+ "@hypit/nano-banana": "workspace:*",
21
+ "@hypit/seedance": "workspace:*"
22
+ }
23
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ createRuntimeEndpointAdapterFacet,
3
+ runtimeConfigCredentialRef,
4
+ runtimeConfigActionLimits,
5
+ runtimeConfigExact,
6
+ runtimeConfigObject,
7
+ runtimeConfigPositiveInteger,
8
+ runtimeConfigString,
9
+ } from "@hypit/runtime-kit";
10
+
11
+ import { createBeatApiProvider } from "./provider.js";
12
+
13
+ const adapter = createRuntimeEndpointAdapterFacet({
14
+ use: "@hypit/provider-beatapi",
15
+ activate(context) {
16
+ if (context.pool === undefined) throw new Error("BeatAPI Provider Pool is required");
17
+ const config = runtimeConfigObject(context.config, "BeatAPI");
18
+ runtimeConfigExact(config, [
19
+ "baseUrl",
20
+ "apiKey",
21
+ "defaultConcurrency",
22
+ "actionLimits",
23
+ "pollIntervalMs",
24
+ "requestTimeoutMs",
25
+ "operationTimeoutMs",
26
+ ], "BeatAPI");
27
+ const baseUrl = runtimeConfigString(config.baseUrl, "BeatAPI baseUrl");
28
+ if (baseUrl !== undefined) {
29
+ const url = new URL(baseUrl);
30
+ if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
31
+ throw new Error("BeatAPI baseUrl must use HTTPS or loopback");
32
+ }
33
+ }
34
+ const apiKey = runtimeConfigCredentialRef(config.apiKey, "BeatAPI apiKey");
35
+ if (apiKey === undefined) throw new Error("BeatAPI apiKey CredentialRef is required");
36
+ const actionLimits = runtimeConfigActionLimits(config.actionLimits);
37
+ const defaultConcurrency = runtimeConfigPositiveInteger(config.defaultConcurrency, "BeatAPI defaultConcurrency");
38
+ const pollIntervalMs = runtimeConfigPositiveInteger(config.pollIntervalMs, "BeatAPI pollIntervalMs");
39
+ const requestTimeoutMs = runtimeConfigPositiveInteger(config.requestTimeoutMs, "BeatAPI requestTimeoutMs");
40
+ const operationTimeoutMs = runtimeConfigPositiveInteger(config.operationTimeoutMs, "BeatAPI operationTimeoutMs");
41
+ return {
42
+ endpoint: createBeatApiProvider({
43
+ instance: context.instance,
44
+ pool: context.pool,
45
+ ...(baseUrl === undefined ? {} : { baseUrl }),
46
+ apiKey,
47
+ ...(defaultConcurrency === undefined ? {} : { defaultConcurrency }),
48
+ ...(actionLimits === undefined ? {} : { actionLimits }),
49
+ ...(pollIntervalMs === undefined ? {} : { pollIntervalMs }),
50
+ ...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
51
+ ...(operationTimeoutMs === undefined ? {} : { operationTimeoutMs }),
52
+ }),
53
+ };
54
+ },
55
+ });
56
+
57
+ export const hypitPackage = {
58
+ format: "hypit.node-package@1" as const,
59
+ hostFacets: [adapter],
60
+ };
61
+
62
+ export default hypitPackage;
@@ -0,0 +1,46 @@
1
+ /** BeatAPI's `{ error: { code, message, request_id, retry_after_seconds } }` envelope and terminal task errors. */
2
+ function record(value: unknown): Record<string, unknown> | undefined {
3
+ return value !== null && typeof value === "object" && !Array.isArray(value)
4
+ ? value as Record<string, unknown> : undefined;
5
+ }
6
+ function text(value: unknown): string | undefined {
7
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
8
+ }
9
+
10
+ // Responses may mention a hosted result URL. Keep the reason, not its access capability.
11
+ export function safeBeatApiReason(value: string): string {
12
+ return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
13
+ }
14
+
15
+ export class BeatApiServiceError extends Error {
16
+ constructor(readonly code: string, message: string) { super(message); }
17
+ }
18
+
19
+ export class BeatApiHttpError extends BeatApiServiceError {
20
+ constructor(readonly status: number, response: { readonly headers: Headers }, bodyText: string,
21
+ request: { readonly method: string; readonly path: string; readonly model?: string }) {
22
+ let body: Record<string, unknown> | undefined;
23
+ try { body = record(JSON.parse(bodyText)); } catch { /* Non-JSON gateway failures still have HTTP evidence. */ }
24
+ const error = record(body?.error);
25
+ const code = text(error?.code) ?? "BEATAPI_HTTP_ERROR";
26
+ const reason = text(error?.message) ?? (error === undefined ? text(bodyText.slice(0, 2000)) : undefined);
27
+ const requestId = text(error?.request_id) ?? text(response.headers.get("x-request-id"));
28
+ const retryAfter = typeof error?.retry_after_seconds === "number" ? error.retry_after_seconds : undefined;
29
+ const facts = [
30
+ `BeatAPI HTTP ${status}`, code, `${request.method} ${request.path}`,
31
+ ...(request.model === undefined ? [] : [`model=${request.model}`]),
32
+ ...(requestId === undefined ? [] : [`request=${requestId}`]),
33
+ ...(retryAfter === undefined ? [] : [`retry-after=${retryAfter}s`]),
34
+ ];
35
+ super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeBeatApiReason(reason)}`}`);
36
+ }
37
+ }
38
+
39
+ /** A terminal `failed` task; `undefined` otherwise. */
40
+ export function beatApiTaskFailure(task: Record<string, unknown>, id: string): BeatApiServiceError | undefined {
41
+ if (task.status !== "failed") return undefined;
42
+ const code = text(task.error_code) ?? "BEATAPI_TASK_FAILED";
43
+ const reason = text(task.error_message);
44
+ return new BeatApiServiceError(code,
45
+ `BeatAPI task ${id} failed; ${code}${reason === undefined ? "" : `: ${safeBeatApiReason(reason)}`}`);
46
+ }
@@ -0,0 +1,2 @@
1
+ export { createBeatApiProvider, beatApiProviderModuleRef } from "./provider.js";
2
+ export type { CreateBeatApiProviderOptions } from "./provider.js";
@@ -0,0 +1,89 @@
1
+ import type { ModuleRef } from "@hypit/protocol";
2
+ import type { GenerationWireMapping } from "@hypit/generation";
3
+
4
+ const SEEDANCE: ModuleRef = { name: "@hypit/seedance", version: "1" };
5
+ const MINIMAX: ModuleRef = { name: "@hypit/minimax-h3", version: "1" };
6
+ const GROK: ModuleRef = { name: "@hypit/grok-imagine", version: "1" };
7
+ const GPT_IMAGE: ModuleRef = { name: "@hypit/gpt-image", version: "1" };
8
+ const NANO_BANANA: ModuleRef = { name: "@hypit/nano-banana", version: "1" };
9
+
10
+ /**
11
+ * BeatAPI model aliases and the body fields each one documents. One alias serves every input mode,
12
+ * so each Capability has a single route and the request shape alone decides which arrays travel.
13
+ *
14
+ * BeatAPI carries the opening and closing frames as one ordered `images` array rather than two
15
+ * fields. The mapping writes them to `first_frame` and `last_frame`, which routes.ts folds into
16
+ * that array; a mapping cannot write one wire field from two ports.
17
+ */
18
+ const seedanceFields = {
19
+ prompt: { as: "value", field: "prompt" },
20
+ firstFrame: { as: "url", field: "first_frame", resourceFields: ["personReference"] },
21
+ lastFrame: { as: "url", field: "last_frame", resourceFields: ["personReference"] },
22
+ referenceImage: { as: "urlArray", field: "reference_images", resourceFields: ["personReference"] },
23
+ referenceVideo: { as: "urlArray", field: "reference_videos", resourceFields: ["personReference"] },
24
+ referenceAudio: { as: "urlArray", field: "reference_audios" },
25
+ duration: { as: "value", field: "duration" },
26
+ aspectRatio: { as: "value", field: "aspect_ratio" },
27
+ resolution: { as: "value", field: "resolution" },
28
+ generateAudio: { as: "value", field: "generate_audio" },
29
+ webSearch: { as: "value", field: "web_search" },
30
+ } as const satisfies GenerationWireMapping["fields"];
31
+
32
+ const seedance = (name: string): GenerationWireMapping => ({
33
+ capability: { module: SEEDANCE, name }, result: "video", routes: [{ model: name }], fields: seedanceFields,
34
+ });
35
+
36
+ export const beatApiMappings: readonly GenerationWireMapping[] = [
37
+ seedance("seedance-2"),
38
+ seedance("seedance-2-fast"),
39
+ seedance("seedance-2-mini"),
40
+ seedance("seedance-2.5"),
41
+ {
42
+ capability: { module: MINIMAX, name: "minimax-h3" }, result: "video",
43
+ routes: [{ model: "minimax-h3" }],
44
+ fields: {
45
+ prompt: { as: "value", field: "prompt" },
46
+ firstFrame: { as: "url", field: "first_frame" },
47
+ lastFrame: { as: "url", field: "last_frame" },
48
+ referenceImage: { as: "urlArray", field: "reference_images" },
49
+ referenceVideo: { as: "urlArray", field: "reference_videos" },
50
+ referenceAudio: { as: "urlArray", field: "reference_audios" },
51
+ duration: { as: "value", field: "duration" },
52
+ aspectRatio: { as: "value", field: "aspect_ratio" },
53
+ resolution: { as: "value", field: "resolution" },
54
+ },
55
+ },
56
+ {
57
+ capability: { module: GROK, name: "grok-imagine-video-1.5-preview" }, result: "video",
58
+ routes: [{ model: "grok-imagine-video-1.5" }],
59
+ fields: {
60
+ prompt: { as: "value", field: "prompt" },
61
+ images: { as: "urlArray", field: "reference_images" },
62
+ duration: { as: "value", field: "duration" },
63
+ aspectRatio: { as: "value", field: "aspect_ratio" },
64
+ resolution: { as: "value", field: "resolution" },
65
+ },
66
+ },
67
+ {
68
+ capability: { module: GPT_IMAGE, name: "gpt-image-2" }, result: "image",
69
+ routes: [{ model: "gpt-image-2" }],
70
+ fields: {
71
+ prompt: { as: "value", field: "prompt" },
72
+ images: { as: "urlArray", field: "images" },
73
+ aspectRatio: { as: "value", field: "aspect_ratio" },
74
+ resolution: { as: "value", field: "resolution" },
75
+ background: { as: "value", field: "background" },
76
+ },
77
+ },
78
+ ...(["nano-banana-2", "nano-banana-pro"] as const).map((model) => ({
79
+ capability: { module: NANO_BANANA, name: model }, result: "image" as const,
80
+ routes: [{ model }],
81
+ fields: {
82
+ prompt: { as: "value" as const, field: "prompt" },
83
+ images: { as: "urlArray" as const, field: "images" },
84
+ aspectRatio: { as: "value" as const, field: "aspect_ratio" },
85
+ resolution: { as: "value" as const, field: "resolution" },
86
+ outputFormat: { as: "value" as const, field: "output_format" },
87
+ },
88
+ })),
89
+ ];
@@ -0,0 +1,244 @@
1
+ import { requestDeadline } from "@hypit/runtime-kit";
2
+ import type { AsyncEndpoint, EndpointCredential, EndpointInvocationContext, EndpointOutcome } from "@hypit/endpoint-kit";
3
+ import { defineEndpointPackage, wakeAfter } from "@hypit/endpoint-kit";
4
+ import type { GenerationArtifactUrlResolver } from "@hypit/generation";
5
+ import { canonicalize } from "@hypit/protocol";
6
+ import type { BlobRef, CapabilityRef } from "@hypit/protocol";
7
+ import { credentialRef } from "@hypit/runtime";
8
+ import type { CredentialRef, ResourceStore } from "@hypit/runtime";
9
+ import { beatApiRouteForCapability, beatApiRoutes } from "./routes.js";
10
+ import { BeatApiHttpError, BeatApiServiceError, beatApiTaskFailure } from "./errors.js";
11
+
12
+ export const beatApiProviderModuleRef = { name: "@hypit/provider-beatapi", version: "1" } as const;
13
+
14
+ export type CreateBeatApiProviderOptions = {
15
+ readonly instance?: string;
16
+ readonly pool?: string;
17
+ readonly baseUrl?: string;
18
+ readonly apiKey?: CredentialRef;
19
+ readonly defaultConcurrency?: number;
20
+ readonly actionLimits?: import("@hypit/endpoint-kit").EndpointActionLimits;
21
+ readonly pollIntervalMs?: number;
22
+ readonly requestTimeoutMs?: number;
23
+ readonly operationTimeoutMs?: number;
24
+ readonly fetch?: typeof globalThis.fetch;
25
+ /** Publish a referenced Resource at a URL the service can fetch; replaces the `/v1/files` upload. */
26
+ readonly publicAssetUrl?: (artifact: BlobRef, artifacts: ResourceStore, fields?: Readonly<Record<string, string | number | boolean>>) => Promise<string>;
27
+ };
28
+
29
+ type Handle = {
30
+ readonly contract: "hypit.beatapi-operation@1";
31
+ readonly taskId: string;
32
+ readonly route: string;
33
+ readonly startedAt: number;
34
+ readonly urls?: readonly string[];
35
+ };
36
+
37
+ const MB = 1_000_000;
38
+
39
+ /** Per-file sizes and containers `POST /v1/files` accepts, by media kind. */
40
+ const uploadLimits: Readonly<Record<string, { readonly bytes: number; readonly mediaTypes: readonly string[] }>> = {
41
+ image: { bytes: 50 * MB, mediaTypes: ["image/png", "image/jpeg", "image/jpg", "image/webp"] },
42
+ audio: { bytes: 50 * MB, mediaTypes: ["audio/mpeg", "audio/mp3", "audio/wav", "audio/x-wav", "audio/aac", "audio/mp4", "audio/x-m4a"] },
43
+ video: { bytes: 100 * MB, mediaTypes: ["video/mp4", "video/quicktime"] },
44
+ };
45
+
46
+ const extensions: Readonly<Record<string, string>> = {
47
+ "image/png": "png", "image/jpeg": "jpg", "image/jpg": "jpg", "image/webp": "webp",
48
+ "audio/mpeg": "mp3", "audio/mp3": "mp3", "audio/wav": "wav", "audio/x-wav": "wav",
49
+ "audio/aac": "aac", "audio/mp4": "m4a", "audio/x-m4a": "m4a",
50
+ "video/mp4": "mp4", "video/quicktime": "mov",
51
+ };
52
+
53
+ function assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); }
54
+ function object(value: unknown, subject: string): Record<string, unknown> {
55
+ assert(value !== null && typeof value === "object" && !Array.isArray(value), `${subject} must be an object`);
56
+ return value as Record<string, unknown>;
57
+ }
58
+ function capabilityKey(capability: CapabilityRef): string { return `${capability.module.name}@${capability.module.version}#${capability.name}`; }
59
+ function apiBaseUrl(value: string): string {
60
+ let trimmed = value.trim();
61
+ while (trimmed.endsWith("/")) trimmed = trimmed.slice(0, -1);
62
+ assert(trimmed.length > 0, "BeatAPI base URL is empty");
63
+ return trimmed;
64
+ }
65
+ function apiKey(credentials: Readonly<Record<string, EndpointCredential>>): string {
66
+ const value = credentials.apiKey?.secret;
67
+ assert(typeof value === "string" && value.length > 0, "BeatAPI apiKey credential is unavailable; store a BeatAPI API key for this Endpoint");
68
+ return value;
69
+ }
70
+ function httpsUrl(value: unknown, subject: string): string {
71
+ assert(typeof value === "string" && /^https:\/\//u.test(value), `${subject} has no HTTPS URL`);
72
+ return value;
73
+ }
74
+ function failureMessage(error: unknown): string {
75
+ return error instanceof Error ? error.message : String(error);
76
+ }
77
+ function failure(error: unknown): EndpointOutcome {
78
+ return { status: "failed", failure: { code: error instanceof BeatApiServiceError ? error.code : "BEATAPI_ERROR", message: failureMessage(error) } };
79
+ }
80
+
81
+ class BeatApiClient {
82
+ constructor(readonly baseUrl: string, readonly timeout: number, readonly fetcher: typeof globalThis.fetch) {}
83
+ async json(path: string, key: string, init: RequestInit = {}): Promise<Record<string, unknown>> {
84
+ const deadline = requestDeadline(this.timeout);
85
+ try {
86
+ const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
87
+ ...init, signal: deadline.signal, headers: { authorization: `Bearer ${key}`, ...(init.headers ?? {}) },
88
+ }));
89
+ const text = await deadline.wait(response.text());
90
+ if (!response.ok) {
91
+ const input = typeof init.body === "string" ? JSON.parse(init.body) as Record<string, unknown> : undefined;
92
+ throw new BeatApiHttpError(response.status, response, text, {
93
+ method: init.method ?? "GET", path, ...(typeof input?.model === "string" ? { model: input.model } : {}),
94
+ });
95
+ }
96
+ let body: unknown;
97
+ try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new Error(`BeatAPI returned invalid JSON (${response.status})`); }
98
+ return object(object(body, "BeatAPI response").data, "BeatAPI response data");
99
+ } finally { deadline.finish(); }
100
+ }
101
+ /** Upload one referenced Resource and return the HTTPS URL the task request carries. */
102
+ async upload(artifact: BlobRef, resources: ResourceStore, key: string): Promise<string> {
103
+ const kind = artifact.mediaType.split("/", 1)[0] ?? "";
104
+ const limits = uploadLimits[kind];
105
+ assert(limits !== undefined, `BeatAPI accepts image, audio and video references, not ${artifact.mediaType}`);
106
+ assert(limits.mediaTypes.includes(artifact.mediaType),
107
+ `BeatAPI accepts ${limits.mediaTypes.join(", ")} for ${kind} references, not ${artifact.mediaType}`);
108
+ assert(artifact.size <= limits.bytes,
109
+ `BeatAPI accepts ${kind} references up to ${limits.bytes / MB} MB; ${artifact.resource} is ${artifact.size} bytes`);
110
+ const bytes = await resources.get(artifact.resource);
111
+ assert(bytes !== undefined && bytes.byteLength === artifact.size, `Reference Resource ${artifact.resource} is unavailable or has changed`);
112
+ const form = new FormData();
113
+ form.append("file", new Blob([new Uint8Array(bytes)], { type: artifact.mediaType }),
114
+ `${artifact.resource}.${extensions[artifact.mediaType] ?? "bin"}`);
115
+ form.append("purpose", "input");
116
+ const file = await this.json("/v1/files", key, { method: "POST", body: form });
117
+ return httpsUrl(file.url, "BeatAPI file upload");
118
+ }
119
+ async download(url: string): Promise<{ readonly bytes: Uint8Array; readonly mediaType: string }> {
120
+ const deadline = requestDeadline(this.timeout);
121
+ try {
122
+ const response = await deadline.wait(this.fetcher(url, { signal: deadline.signal }));
123
+ if (!response.ok) throw new Error(`BeatAPI asset returned HTTP ${response.status}`);
124
+ return { bytes: new Uint8Array(await deadline.wait(response.arrayBuffer())), mediaType: response.headers.get("content-type")?.split(";", 1)[0] ?? "application/octet-stream" };
125
+ } finally { deadline.finish(); }
126
+ }
127
+ }
128
+
129
+ function resolverFor(client: BeatApiClient, context: EndpointInvocationContext, publicAssetUrl: CreateBeatApiProviderOptions["publicAssetUrl"]): GenerationArtifactUrlResolver {
130
+ const resolved = new Map<string, Promise<string>>();
131
+ return (artifact, fields) => {
132
+ const existing = resolved.get(artifact.resource);
133
+ if (existing !== undefined) return existing;
134
+ const promise = publicAssetUrl === undefined
135
+ ? client.upload(artifact, context.resources, apiKey(context.credentials))
136
+ : publicAssetUrl(artifact, context.resources, fields);
137
+ resolved.set(artifact.resource, promise);
138
+ return promise;
139
+ };
140
+ }
141
+
142
+ /** Statuses a queued image or video task passes through before it is terminal. */
143
+ const pendingStatuses: readonly string[] = ["queued", "processing"];
144
+
145
+ function endpoint(client: BeatApiClient, pollIntervalMs: number, maxOperationMs: number, publicAssetUrl: CreateBeatApiProviderOptions["publicAssetUrl"]): AsyncEndpoint {
146
+ return {
147
+ async start(context) {
148
+ try {
149
+ const route = beatApiRouteForCapability(context.need.capability);
150
+ assert(route !== undefined, "BeatAPI does not implement this exact capability");
151
+ const request = route.prepare(context.need.constraints);
152
+ await context.reportProgress?.({ phase: `Preparing BeatAPI request: ${request.model}` });
153
+ let body: Record<string, unknown>;
154
+ try {
155
+ body = await request.compile(resolverFor(client, context, publicAssetUrl));
156
+ } catch (error) {
157
+ throw new BeatApiServiceError(error instanceof BeatApiServiceError ? error.code : "BEATAPI_ERROR",
158
+ `BeatAPI request preparation failed; model=${request.model}; generation not submitted: ${failureMessage(error)}`);
159
+ }
160
+ await context.reportProgress?.({ phase: `Submitting BeatAPI request: ${request.model}` });
161
+ const response = await client.json(`/v1/${request.media}s/tasks`, apiKey(context.credentials), {
162
+ method: "POST",
163
+ headers: { "content-type": "application/json", "idempotency-key": context.operation },
164
+ body: JSON.stringify(body),
165
+ });
166
+ assert(typeof response.id === "string" && response.id.length > 0, "BeatAPI response has no task id");
167
+ const handle: Handle = { contract: "hypit.beatapi-operation@1", taskId: response.id, route: route.key, startedAt: Date.now() };
168
+ const receipt = { id: handle.taskId };
169
+ await context.checkpoint?.({ handle: canonicalize(handle), receipt });
170
+ return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "submitted" }), receipt };
171
+ } catch (error) {
172
+ return failure(error);
173
+ }
174
+ },
175
+ async poll(context) {
176
+ try {
177
+ const handle = object(context.handle, "BeatAPI handle") as unknown as Handle;
178
+ const route = beatApiRouteForCapability(context.need.capability);
179
+ assert(route !== undefined && handle.contract === "hypit.beatapi-operation@1" && handle.route === route.key, "BeatAPI handle is invalid");
180
+ const receipt = { id: handle.taskId };
181
+ if (Date.now() - handle.startedAt > maxOperationMs) {
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
+ }
184
+ let task: Record<string, unknown>;
185
+ try {
186
+ task = await client.json(`/v1/tasks/${encodeURIComponent(handle.taskId)}`, apiKey(context.credentials));
187
+ } catch (error) {
188
+ if (error instanceof BeatApiHttpError && error.status < 500) return { ...failure(error), receipt };
189
+ return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" }), receipt };
190
+ }
191
+ const status = String(task.status);
192
+ if (pendingStatuses.includes(status)) {
193
+ return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
194
+ }
195
+ const rejected = beatApiTaskFailure(task, handle.taskId);
196
+ if (rejected !== undefined) return { ...failure(rejected), receipt };
197
+ assert(status === "succeeded", `BeatAPI returned unknown task status ${status}`);
198
+ const media = object(task.output, "BeatAPI task output").media;
199
+ assert(Array.isArray(media) && media.length > 0, "BeatAPI task succeeded without output media");
200
+ const urls = media.map((item, index) => httpsUrl(object(item, `BeatAPI output ${index + 1}`).url, `BeatAPI output ${index + 1}`));
201
+ return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
202
+ } catch (error) {
203
+ return failure(error);
204
+ }
205
+ },
206
+ async collect(context) {
207
+ try {
208
+ const handle = object(context.handle, "BeatAPI handle") as unknown as Handle;
209
+ const route = beatApiRouteForCapability(context.need.capability);
210
+ assert(route !== undefined && handle.route === route.key && Array.isArray(handle.urls), "BeatAPI collection route differs");
211
+ await context.reportProgress?.({ phase: "Receiving generated files" });
212
+ const blobs: BlobRef[] = [];
213
+ for (const url of handle.urls) {
214
+ const downloaded = await client.download(url);
215
+ blobs.push(await context.resources.put(downloaded.bytes, downloaded.mediaType));
216
+ }
217
+ return { status: "completed", result: { value: route.packageResult(blobs) }, receipt: { id: handle.taskId } };
218
+ } catch (error) {
219
+ return failure(error);
220
+ }
221
+ },
222
+ };
223
+ }
224
+
225
+ export function createBeatApiProvider(options: CreateBeatApiProviderOptions = {}) {
226
+ const requestTimeoutMs = options.requestTimeoutMs ?? 300_000;
227
+ const operationTimeoutMs = options.operationTimeoutMs ?? 30 * 60_000;
228
+ for (const [name, value] of Object.entries({ requestTimeoutMs, operationTimeoutMs })) {
229
+ assert(Number.isSafeInteger(value) && value > 0, `BeatAPI ${name} must be a positive integer`);
230
+ }
231
+ const client = new BeatApiClient(apiBaseUrl(options.baseUrl ?? "https://api.beatapi.io"), requestTimeoutMs, options.fetch ?? globalThis.fetch);
232
+ const asyncEndpoint = endpoint(client, options.pollIntervalMs ?? 10_000, operationTimeoutMs, options.publicAssetUrl);
233
+ return defineEndpointPackage({
234
+ module: beatApiProviderModuleRef, facet: "gateway", instance: options.instance ?? "beatapi.default", pool: options.pool ?? options.instance ?? "beatapi.default",
235
+ pricing: { kind: "page", url: "https://docs.beatapi.io/pricing" },
236
+ credentials: { apiKey: options.apiKey ?? credentialRef("os", "beatapi.api-key") },
237
+ credentialInputs: { apiKey: { label: "BeatAPI API key" } },
238
+ defaultConcurrency: options.defaultConcurrency ?? 4,
239
+ ...(options.actionLimits === undefined ? {} : { actionLimits: options.actionLimits }),
240
+ capabilities: beatApiRoutes.map((route) => ({
241
+ capability: route.capability, returns: route.returns, lifecycle: "asynchronous" as const, endpoint: asyncEndpoint, capacity: route.capability.name, supports: route.supports,
242
+ })),
243
+ });
244
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ compileWireRequest,
3
+ selectWireModelForRequest,
4
+ generationTypes,
5
+ sealGeneratedImageSet,
6
+ sealGeneratedVideoSet,
7
+ } from "@hypit/generation";
8
+ import type { GenerationArtifactUrlResolver, GenerationRequest, GenerationWireMapping } from "@hypit/generation";
9
+ import { canonicalize } from "@hypit/protocol";
10
+ import type { BlobRef, CapabilityRef, CanonicalValue, StoredValue, TypeRef } from "@hypit/protocol";
11
+ import type { EndpointRequest, EndpointSupport } from "@hypit/endpoint-kit";
12
+ import { beatApiMappings } from "./mapping.js";
13
+
14
+ export type BeatApiPreparedRequest = {
15
+ readonly model: string;
16
+ readonly media: "image" | "video";
17
+ readonly compile: (resolve: GenerationArtifactUrlResolver) => Promise<Record<string, unknown>>;
18
+ };
19
+
20
+ export type BeatApiRoute = GenerationWireMapping & {
21
+ readonly key: string;
22
+ readonly returns: TypeRef;
23
+ readonly supports: (request: EndpointRequest) => EndpointSupport;
24
+ readonly prepare: (constraints: CanonicalValue) => BeatApiPreparedRequest;
25
+ readonly packageResult: (artifacts: readonly BlobRef[]) => StoredValue;
26
+ };
27
+
28
+ function scalar(request: GenerationRequest, port: string): string | number | boolean | undefined {
29
+ const value = request.ports[port]?.[0];
30
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : undefined;
31
+ }
32
+ function count(request: GenerationRequest, port: string): number {
33
+ return request.ports[port]?.length ?? 0;
34
+ }
35
+
36
+ /** Ratios the Nano Banana 2 model package offers that BeatAPI's schema for it does not list. */
37
+ const NANO_BANANA_2_UNAVAILABLE: readonly string[] = ["1:4", "4:1", "1:8", "8:1"];
38
+
39
+ /** BeatAPI's documented input ranges per model, checked before any reference is uploaded. */
40
+ function rejection(mapping: GenerationWireMapping, request: GenerationRequest): string | undefined {
41
+ const { name } = mapping.capability;
42
+ const model = selectWireModelForRequest(mapping, request);
43
+ const ratio = scalar(request, "aspectRatio");
44
+ const resolution = scalar(request, "resolution");
45
+ if (mapping.capability.module.name === "@hypit/seedance") {
46
+ if (scalar(request, "webSearch") === true) return `BeatAPI ${model} has no web_search field`;
47
+ if (name === "seedance-2-mini" && scalar(request, "generateAudio") === true) {
48
+ return "BeatAPI seedance-2-mini renders no generated audio";
49
+ }
50
+ if (name === "seedance-2" && resolution === "1080p" && count(request, "referenceImage") > 0) {
51
+ return "BeatAPI seedance-2 does not render 1080p with reference images";
52
+ }
53
+ if (name === "seedance-2.5" && scalar(request, "duration") === -1) {
54
+ return "BeatAPI seedance-2.5 takes a duration of 4 to 30 seconds, not automatic";
55
+ }
56
+ }
57
+ if (name === "minimax-h3" && count(request, "lastFrame") > 0 && count(request, "firstFrame") === 0) {
58
+ return "BeatAPI minimax-h3 orders its images first then last, so a last frame needs a first frame";
59
+ }
60
+ if (name === "grok-imagine-video-1.5-preview" && resolution === "1080p" && count(request, "images") > 1) {
61
+ return "BeatAPI grok-imagine-video-1.5 accepts at most one image at 1080p";
62
+ }
63
+ if (name === "gpt-image-2" && scalar(request, "background") !== undefined) {
64
+ return "BeatAPI gpt-image-2 has no background field";
65
+ }
66
+ if (name === "nano-banana-2") {
67
+ if (count(request, "images") > 10) return "BeatAPI nano-banana-2 accepts up to ten reference images";
68
+ if (NANO_BANANA_2_UNAVAILABLE.includes(String(ratio))) {
69
+ return `BeatAPI nano-banana-2 does not render ${String(ratio)}`;
70
+ }
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ /** Fields the model package states that BeatAPI names differently, spells differently or omits. */
76
+ function normalize(mapping: GenerationWireMapping, input: Record<string, unknown>): Record<string, unknown> {
77
+ const { name } = mapping.capability;
78
+ // BeatAPI carries the opening and closing frames as one ordered array.
79
+ const frames = [input.first_frame, input.last_frame].filter((item): item is string => typeof item === "string");
80
+ delete input.first_frame;
81
+ delete input.last_frame;
82
+ if (frames.length > 0) input.images = frames;
83
+ if (input.web_search === false) delete input.web_search;
84
+ if (name === "seedance-2-mini" && input.generate_audio === false) delete input.generate_audio;
85
+ if (name === "nano-banana-2" && input.output_format === "jpg") input.output_format = "jpeg";
86
+ return input;
87
+ }
88
+
89
+ function capabilityKey(capability: CapabilityRef): string {
90
+ return `${capability.module.name}@${capability.module.version}#${capability.name}`;
91
+ }
92
+
93
+ export const beatApiRoutes: readonly BeatApiRoute[] = beatApiMappings.map((mapping) => ({
94
+ ...mapping,
95
+ key: capabilityKey(mapping.capability),
96
+ returns: mapping.result === "image" ? generationTypes.imageSet : generationTypes.videoSet,
97
+ supports: (request) => {
98
+ const reason = rejection(mapping, request.constraints as unknown as GenerationRequest);
99
+ return reason === undefined ? { status: "supported" } : { status: "unsupported", reason };
100
+ },
101
+ prepare: (constraints) => {
102
+ const request = constraints as unknown as GenerationRequest;
103
+ const reason = rejection(mapping, request);
104
+ if (reason !== undefined) throw new Error(reason);
105
+ const model = selectWireModelForRequest(mapping, request);
106
+ return {
107
+ model,
108
+ media: mapping.result === "image" ? "image" : "video",
109
+ compile: async (resolve) => ({
110
+ model,
111
+ ...normalize(mapping, (await compileWireRequest(mapping, request, resolve)).input as Record<string, unknown>),
112
+ }),
113
+ };
114
+ },
115
+ packageResult: (artifacts) => ({
116
+ kind: "inline",
117
+ value: canonicalize(mapping.result === "image"
118
+ ? sealGeneratedImageSet({ images: artifacts })
119
+ : sealGeneratedVideoSet({ videos: artifacts })),
120
+ }),
121
+ }));
122
+
123
+ const byCapability = new Map(beatApiRoutes.map((route) => [route.key, route]));
124
+
125
+ export function beatApiRouteForCapability(capability: CapabilityRef): BeatApiRoute | undefined {
126
+ return byCapability.get(capabilityKey(capability));
127
+ }
@@ -38,9 +38,7 @@ Service limits this Provider reports as unsupported before submitting:
38
38
  - Grok Imagine renders 480p or 720p; `grok-imagine-1.5/image-to-video` animates exactly one image.
39
39
 
40
40
  Seedance visual references require `person-reference`; the Provider accepts the declaration and
41
- transmits nothing for it, since HiAPI has no field for it. Seedance rejects reference images and
42
- videos that contain a real human face; HiAPI offers no way to register authorized portrait material,
43
- so such a request fails with the service's moderation error.
41
+ transmits nothing for it, since HiAPI has no field for it.
44
42
 
45
43
  Reference images and audio travel inline as `data:` URLs, which HiAPI documents for its Seedance
46
44
  inputs, within the per-file sizes each model page states: 30 MB images and 15 MB audio for
@@ -155,7 +155,13 @@ 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
- const task = await client.json(`/v1/tasks/${encodeURIComponent(handle.taskId)}`, apiKey(context.credentials));
158
+ let task: Record<string, unknown>;
159
+ try {
160
+ task = await client.json(`/v1/tasks/${encodeURIComponent(handle.taskId)}`, apiKey(context.credentials));
161
+ } catch (error) {
162
+ if (error instanceof HiApiHttpError && error.status < 500) return { ...failure(error), receipt };
163
+ return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" }), receipt };
164
+ }
159
165
  const status = String(task.status);
160
166
  if (status === "queued" || status === "handling" || status === "archiving") {
161
167
  return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
@@ -494,7 +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
- const job = await client.json(`/jobs/${encodeURIComponent(handle.jobId)}`, authFor(context, client)); const status = job.status;
497
+ let job: Record<string, unknown>;
498
+ try {
499
+ job = await client.json(`/jobs/${encodeURIComponent(handle.jobId)}`, authFor(context, client));
500
+ } catch (error) {
501
+ if (error instanceof HypiHubHttpError && error.status < 500) return failure(error);
502
+ return wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" });
503
+ }
504
+ const status = job.status;
498
505
  if (status === "queued" || status === "running" || status === "in_progress") return wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: String(status) });
499
506
  const rejected = hypiHubJobFailure(job, handle.jobId);
500
507
  if (rejected !== undefined) return { ...failure(rejected), receipt: { id: handle.jobId } };
@@ -27,8 +27,6 @@ The Seedance endpoints add `generate_audio`. Monid documents no web search field
27
27
  `web-search="true"` is unsupported, and Seedance 2.5 frame mode (`first-frame` present) requires
28
28
  `aspect-ratio="adaptive"`. Seedance visual references require `person-reference`; the Provider
29
29
  accepts the declaration and transmits nothing for it, since the endpoint has no field for it.
30
- Seedance rejects reference images and videos that contain a real human face; Monid offers no way to
31
- register authorized portrait material, so such a request fails with the upstream moderation error.
32
30
 
33
31
  MiniMax H3 names its model in the body and takes neither of those two fields. The endpoint requires
34
32
  a resolution, so a request that states none is sent at `2K`, the resolution the HypiHub Provider
@@ -202,7 +202,13 @@ 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
- const run = await client.getRun(handle.runId, apiKey(context.credentials));
205
+ let run: Record<string, unknown>;
206
+ try {
207
+ run = await client.getRun(handle.runId, apiKey(context.credentials));
208
+ } catch (error) {
209
+ if (error instanceof MonidHttpError && error.status < 500) return { ...failure(error), receipt };
210
+ return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" }), receipt };
211
+ }
206
212
  const status = String(run.status);
207
213
  if (!monidTerminalStatuses.includes(status as typeof monidTerminalStatuses[number])) {
208
214
  return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
@@ -133,7 +133,13 @@ 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
- const task = await client.json(`/v1/generation/${encodeURIComponent(handle.taskId)}/status`, apiKey(context.credentials));
136
+ let task: Record<string, unknown>;
137
+ try {
138
+ task = await client.json(`/v1/generation/${encodeURIComponent(handle.taskId)}/status`, apiKey(context.credentials));
139
+ } catch (error) {
140
+ if (error instanceof PolloHttpError && error.status < 500) return { ...failure(error), receipt };
141
+ return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" }), receipt };
142
+ }
137
143
  assert(Array.isArray(task.generations) && task.generations.length > 0, "Pollo task has no generations");
138
144
  const generations = task.generations.map((item, index) => object(item, `Pollo generation ${index + 1}`));
139
145
  const rejected = polloTaskFailure(generations, handle.taskId);
@@ -208,7 +208,14 @@ 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
- const task = taskBody(route.protocol, await client.json(paths[route.protocol].task(handle.taskId), apiKey(context.credentials)));
211
+ let response: Record<string, unknown>;
212
+ try {
213
+ response = await client.json(paths[route.protocol].task(handle.taskId), apiKey(context.credentials));
214
+ } catch (error) {
215
+ if (error instanceof TokenDanceHttpError && error.status < 500) return { ...failure(error), receipt };
216
+ return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "retrying" }), receipt };
217
+ }
218
+ const task = taskBody(route.protocol, response);
212
219
  const status = String(task.status);
213
220
  if (status === "queued" || status === "running") return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
214
221
  const rejected = tokenDanceTaskFailure(task, handle.taskId);
@@ -722,12 +722,27 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
722
722
  response.end();
723
723
  return;
724
724
  }
725
- response.statusCode = 200;
725
+ const size = file.bytes.byteLength;
726
+ let range: BuildResultFileRange | undefined;
727
+ try {
728
+ range = requestedByteRange(request.headers.range, size);
729
+ } catch (error) {
730
+ if (!(error instanceof RangeError)) throw error;
731
+ response.statusCode = 416;
732
+ response.setHeader("content-range", `bytes */${size}`);
733
+ response.end();
734
+ return;
735
+ }
736
+ response.statusCode = range === undefined ? 200 : 206;
726
737
  response.setHeader("content-type", file.mediaType);
738
+ response.setHeader("content-length", String(range === undefined ? size : range.endExclusive - range.start));
727
739
  response.setHeader("cache-control", "no-store");
728
740
  response.setHeader("accept-ranges", "bytes");
741
+ if (range !== undefined) {
742
+ response.setHeader("content-range", `bytes ${range.start}-${range.endExclusive - 1}/${size}`);
743
+ }
729
744
  if (request.method === "HEAD") response.end();
730
- else response.end(Buffer.from(file.bytes));
745
+ else response.end(range === undefined ? file.bytes : file.bytes.subarray(range.start, range.endExclusive));
731
746
  return;
732
747
  }
733
748
  if (url.pathname === "/__studio/artifact") {