@hypit/hypit 0.2.9 → 0.2.10

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
@@ -209,6 +209,12 @@ commands and the repository layout.
209
209
  </td>
210
210
  <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
211
  </tr>
212
+ <tr>
213
+ <td width="220" align="center">
214
+ <a href="https://beatapi.io"><img alt="BeatAPI" src="./docs/public/beatapi.png" width="165"></a>
215
+ </td>
216
+ <td><a href="https://beatapi.io">BeatAPI</a> is the professional capability layer for any agent. We provide AI models, real-world data, task-ready capabilities, and professional workspaces that help agents complete real-world tasks.</td>
217
+ </tr>
212
218
  </table>
213
219
 
214
220
  ## ⭐ 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.10",
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:*",
@@ -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,238 @@
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
+ const task = await client.json(`/v1/tasks/${encodeURIComponent(handle.taskId)}`, apiKey(context.credentials));
185
+ const status = String(task.status);
186
+ if (pendingStatuses.includes(status)) {
187
+ return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
188
+ }
189
+ const rejected = beatApiTaskFailure(task, handle.taskId);
190
+ if (rejected !== undefined) return { ...failure(rejected), receipt };
191
+ assert(status === "succeeded", `BeatAPI returned unknown task status ${status}`);
192
+ const media = object(task.output, "BeatAPI task output").media;
193
+ assert(Array.isArray(media) && media.length > 0, "BeatAPI task succeeded without output media");
194
+ const urls = media.map((item, index) => httpsUrl(object(item, `BeatAPI output ${index + 1}`).url, `BeatAPI output ${index + 1}`));
195
+ return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
196
+ } catch (error) {
197
+ return failure(error);
198
+ }
199
+ },
200
+ async collect(context) {
201
+ try {
202
+ const handle = object(context.handle, "BeatAPI handle") as unknown as Handle;
203
+ const route = beatApiRouteForCapability(context.need.capability);
204
+ assert(route !== undefined && handle.route === route.key && Array.isArray(handle.urls), "BeatAPI collection route differs");
205
+ await context.reportProgress?.({ phase: "Receiving generated files" });
206
+ const blobs: BlobRef[] = [];
207
+ for (const url of handle.urls) {
208
+ const downloaded = await client.download(url);
209
+ blobs.push(await context.resources.put(downloaded.bytes, downloaded.mediaType));
210
+ }
211
+ return { status: "completed", result: { value: route.packageResult(blobs) }, receipt: { id: handle.taskId } };
212
+ } catch (error) {
213
+ return failure(error);
214
+ }
215
+ },
216
+ };
217
+ }
218
+
219
+ export function createBeatApiProvider(options: CreateBeatApiProviderOptions = {}) {
220
+ const requestTimeoutMs = options.requestTimeoutMs ?? 300_000;
221
+ const operationTimeoutMs = options.operationTimeoutMs ?? 30 * 60_000;
222
+ for (const [name, value] of Object.entries({ requestTimeoutMs, operationTimeoutMs })) {
223
+ assert(Number.isSafeInteger(value) && value > 0, `BeatAPI ${name} must be a positive integer`);
224
+ }
225
+ const client = new BeatApiClient(apiBaseUrl(options.baseUrl ?? "https://api.beatapi.io"), requestTimeoutMs, options.fetch ?? globalThis.fetch);
226
+ const asyncEndpoint = endpoint(client, options.pollIntervalMs ?? 10_000, operationTimeoutMs, options.publicAssetUrl);
227
+ return defineEndpointPackage({
228
+ module: beatApiProviderModuleRef, facet: "gateway", instance: options.instance ?? "beatapi.default", pool: options.pool ?? options.instance ?? "beatapi.default",
229
+ pricing: { kind: "page", url: "https://docs.beatapi.io/pricing" },
230
+ credentials: { apiKey: options.apiKey ?? credentialRef("os", "beatapi.api-key") },
231
+ credentialInputs: { apiKey: { label: "BeatAPI API key" } },
232
+ defaultConcurrency: options.defaultConcurrency ?? 4,
233
+ ...(options.actionLimits === undefined ? {} : { actionLimits: options.actionLimits }),
234
+ capabilities: beatApiRoutes.map((route) => ({
235
+ capability: route.capability, returns: route.returns, lifecycle: "asynchronous" as const, endpoint: asyncEndpoint, capacity: route.capability.name, supports: route.supports,
236
+ })),
237
+ });
238
+ }
@@ -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
@@ -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
@@ -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") {