@hypit/hypit 0.2.8 → 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.
@@ -1,6 +1,6 @@
1
1
  import { artifactTypes } from "@hypit/artifact";
2
2
  import { generationPort, sealGenerationMediaBinding, sealGenerationRequestDraft } from "@hypit/generation";
3
- import type { GenerationMediaPort, GenerationPortValue } from "@hypit/generation";
3
+ import type { GenerationMediaPort, GenerationMediaRole, GenerationPortValue } from "@hypit/generation";
4
4
  import {
5
5
  createExactModelPrimaryGenerationFragment,
6
6
  exactModelMediaInputNames,
@@ -16,23 +16,31 @@ import type {
16
16
  import type { CanonicalValue, TypeRef } from "@hypit/protocol";
17
17
  import { textTypes, verifyText } from "@hypit/text";
18
18
 
19
- import { pixverseEndpoints } from "./index.js";
19
+ import { pixverseEndpointsByModel } from "./index.js";
20
+ import type { PixverseModel } from "./index.js";
21
+
22
+ type MediaInput = {
23
+ readonly port: "firstFrame" | "lastFrame" | "referenceImage" | "referenceVideo";
24
+ readonly role: GenerationMediaRole;
25
+ readonly source: SurfaceResolvedReference;
26
+ };
20
27
 
21
28
  function assert(condition: unknown, message: string): asserts condition {
22
29
  if (!condition) throw new Error(message);
23
30
  }
24
31
 
32
+ function localName(name: string): string {
33
+ return name.includes(":") ? name.slice(name.lastIndexOf(":") + 1) : name;
34
+ }
35
+
25
36
  function sameType(left: TypeRef, right: TypeRef): boolean {
26
37
  return left.name === right.name && left.module.name === right.module.name && left.module.version === right.module.version;
27
38
  }
28
39
 
29
- const attributes = ["id", "prompt", "first-frame", "last-frame", "duration", "quality",
30
- "aspect-ratio", "generate-audio", "multi-clip", "seed"] as const;
31
-
32
- function exact(element: StructuredElement): void {
33
- const unknown = Object.keys(element.attributes).filter((name) => !attributes.includes(name as typeof attributes[number]));
40
+ function exact(element: StructuredElement, allowed: readonly string[], required: readonly string[]): void {
41
+ const unknown = Object.keys(element.attributes).filter((name) => !allowed.includes(name));
34
42
  assert(unknown.length === 0, `${element.name} does not accept ${unknown[0]}`);
35
- const missing = ["id", "prompt", "duration", "quality"].filter((name) => element.attributes[name] === undefined);
43
+ const missing = required.filter((name) => element.attributes[name] === undefined);
36
44
  assert(missing.length === 0, `${element.name} requires ${missing.join(", ")}`);
37
45
  }
38
46
 
@@ -60,6 +68,10 @@ function integer(element: StructuredElement, name: string): readonly GenerationP
60
68
  return [Number(value)];
61
69
  }
62
70
 
71
+ function optionalInteger(element: StructuredElement, name: string): readonly GenerationPortValue[] | undefined {
72
+ return element.attributes[name] === undefined ? undefined : integer(element, name);
73
+ }
74
+
63
75
  function ref(
64
76
  element: StructuredElement,
65
77
  name: string,
@@ -73,74 +85,171 @@ function ref(
73
85
  return result;
74
86
  }
75
87
 
76
- function frame(
88
+ function media(
77
89
  element: StructuredElement,
78
90
  name: string,
91
+ role: GenerationMediaRole,
79
92
  resolve: (path: string) => SurfaceResolvedReference | undefined,
80
- ): SurfaceResolvedReference | undefined {
81
- if (element.attributes[name] === undefined) return undefined;
82
- const image = ref(element, name, artifactTypes.blob, resolve);
83
- if (image.record !== undefined) {
84
- assert(image.record.value.kind === "blob" && image.record.value.mediaType.startsWith("image/"),
85
- `${element.name}.${name} must reference image media`);
93
+ ): SurfaceResolvedReference {
94
+ const artifact = ref(element, name, artifactTypes.blob, resolve);
95
+ if (artifact.record !== undefined) {
96
+ assert(artifact.record.value.kind === "blob" && artifact.record.value.mediaType.startsWith(`${role}/`),
97
+ `${element.name}.${name} must reference ${role} media`);
86
98
  }
87
- return image;
88
- }
89
-
90
- function decoder(endpoint: ExactModelEndpoint): StructuredSurfaceHandler {
91
- return ({ element, resolveReference }) => {
92
- exact(element);
93
- assert(!element.children.some((item) => item.kind === "element" || item.value.trim()),
94
- `${element.name} accepts no children`);
95
- const id = text(element, "id");
96
- const prompt = ref(element, "prompt", textTypes.text, resolveReference);
97
- if (prompt.record !== undefined) {
98
- assert(prompt.record.value.kind === "inline", `${element.name}.prompt must reference Text`);
99
- verifyText(prompt.record.value.value);
100
- }
101
- const frames = ([["firstFrame", "first-frame"], ["lastFrame", "last-frame"]] as const)
102
- .flatMap(([port, attribute]) => {
103
- const image = frame(element, attribute, resolveReference);
104
- return image === undefined ? [] : [{ port, image }];
105
- });
106
- const stated: Record<string, readonly GenerationPortValue[] | undefined> = {
107
- duration: integer(element, "duration"),
108
- quality: [text(element, "quality")],
109
- aspectRatio: optionalText(element, "aspect-ratio"),
110
- generateAudio: optionalFlag(element, "generate-audio"),
111
- multiClip: optionalFlag(element, "multi-clip"),
112
- seed: element.attributes["seed"] === undefined ? undefined : integer(element, "seed"),
113
- };
114
- const draft = sealGenerationRequestDraft(endpoint.ports, Object.fromEntries(
115
- Object.entries(stated).filter((entry): entry is [string, readonly GenerationPortValue[]] => entry[1] !== undefined),
116
- ));
117
- const records: Array<{ id: string; type: TypeRef; value: { kind: "inline"; value: CanonicalValue }; range: StructuredElement["range"] }> = [{
118
- id: `${id}.draft`, type: endpoint.draftType,
119
- value: { kind: "inline", value: draft as unknown as CanonicalValue }, range: element.range,
120
- }];
121
- const inputs: Record<string, SurfaceResolvedReference["ref"] | { kind: "record"; id: string }> = {
122
- draft: { kind: "record", id: `${id}.draft` }, [exactModelTextInputName("prompt")]: prompt.ref,
123
- };
124
- const media = frames.map(({ port, image }, index) => {
125
- const name = `media-${String(index + 1).padStart(4, "0")}`;
126
- const bindingId = `${id}.${name}.binding`;
127
- const mediaPort = generationPort(endpoint.ports, port);
128
- assert(mediaPort.value.kind === "media", `PixVerse port ${port} is not media`);
129
- records.push({
130
- id: bindingId, type: endpoint.mediaBindings[port]!.type,
131
- value: { kind: "inline", value: sealGenerationMediaBinding(mediaPort as GenerationMediaPort, { role: "image" }) as unknown as CanonicalValue },
132
- range: element.range,
133
- });
134
- const names = exactModelMediaInputNames(name);
135
- inputs[names.binding] = { kind: "record", id: bindingId };
136
- inputs[names.artifact] = image.ref;
137
- return { name, port } as const;
138
- });
139
- const fragment = createExactModelPrimaryGenerationFragment(endpoint, media, [{ name: "prompt", port: "prompt" }]);
140
- return { records, fragments: [fragment], components: [{
141
- id, fragment: fragment.id, inputs, outputs: { video: `${id}.video` }, range: element.range,
142
- }] };
99
+ return artifact;
100
+ }
101
+
102
+ function empty(element: StructuredElement): void {
103
+ assert(!element.children.some((item) => item.kind === "element" || item.value.trim()),
104
+ `${element.name} accepts no children`);
105
+ }
106
+
107
+ function selectModel(element: StructuredElement): { endpoint: ExactModelEndpoint; model: PixverseModel } {
108
+ const requested = text(element, "model");
109
+ if (requested === "v6" || requested === "pixverse-v6") {
110
+ return { endpoint: pixverseEndpointsByModel["pixverse-v6"], model: "pixverse-v6" };
111
+ }
112
+ if (requested === "c1" || requested === "pixverse-c1") {
113
+ return { endpoint: pixverseEndpointsByModel["pixverse-c1"], model: "pixverse-c1" };
114
+ }
115
+ throw new Error(`${element.name}.model must be v6 or c1`);
116
+ }
117
+
118
+ /** A switch one model carries and the other does not is refused where it was authored. */
119
+ function modelScoped(
120
+ element: StructuredElement,
121
+ endpoint: ExactModelEndpoint,
122
+ attribute: string,
123
+ port: string,
124
+ read: (element: StructuredElement, name: string) => readonly GenerationPortValue[] | undefined,
125
+ ): readonly GenerationPortValue[] | undefined {
126
+ if (element.attributes[attribute] === undefined) return undefined;
127
+ assert(endpoint.ports.ports.some((item) => item.name === port),
128
+ `${element.name}.${attribute} is not accepted by ${endpoint.ports.model}`);
129
+ return read(element, attribute);
130
+ }
131
+
132
+ function capacity(element: StructuredElement, endpoint: ExactModelEndpoint, port: string, used: number): void {
133
+ if (used === 0) return;
134
+ const declared = endpoint.ports.ports.find((item) => item.name === port);
135
+ assert(declared !== undefined, `${element.name} references are not accepted by ${endpoint.ports.model}`);
136
+ assert(used <= declared.maxItems,
137
+ `${element.name} accepts at most ${declared.maxItems} ${port === "referenceVideo" ? "video" : "image"} references on ${endpoint.ports.model}`);
138
+ }
139
+
140
+ function assemble(
141
+ element: StructuredElement,
142
+ endpoint: ExactModelEndpoint,
143
+ prompt: SurfaceResolvedReference,
144
+ inputs: readonly MediaInput[],
145
+ stated: Readonly<Record<string, readonly GenerationPortValue[] | undefined>>,
146
+ ) {
147
+ const id = text(element, "id");
148
+ const draft = sealGenerationRequestDraft(endpoint.ports, Object.fromEntries(
149
+ Object.entries(stated).filter((entry): entry is [string, readonly GenerationPortValue[]] => entry[1] !== undefined),
150
+ ));
151
+ const records: Array<{ id: string; type: TypeRef; value: { kind: "inline"; value: CanonicalValue }; range: StructuredElement["range"] }> = [{
152
+ id: `${id}.draft`, type: endpoint.draftType,
153
+ value: { kind: "inline", value: draft as unknown as CanonicalValue }, range: element.range,
154
+ }];
155
+ const componentInputs: Record<string, SurfaceResolvedReference["ref"] | { kind: "record"; id: string }> = {
156
+ draft: { kind: "record", id: `${id}.draft` }, [exactModelTextInputName("prompt")]: prompt.ref,
143
157
  };
158
+ const attached = inputs.map(({ port, role, source }, index) => {
159
+ const name = `media-${String(index + 1).padStart(4, "0")}`;
160
+ const bindingId = `${id}.${name}.binding`;
161
+ const mediaPort = generationPort(endpoint.ports, port);
162
+ assert(mediaPort.value.kind === "media", `PixVerse port ${port} is not media`);
163
+ records.push({
164
+ id: bindingId, type: endpoint.mediaBindings[port]!.type,
165
+ value: { kind: "inline", value: sealGenerationMediaBinding(mediaPort as GenerationMediaPort, { role }) as unknown as CanonicalValue },
166
+ range: element.range,
167
+ });
168
+ const names = exactModelMediaInputNames(name);
169
+ componentInputs[names.binding] = { kind: "record", id: bindingId };
170
+ componentInputs[names.artifact] = source.ref;
171
+ return { name, port } as const;
172
+ });
173
+ const fragment = createExactModelPrimaryGenerationFragment(endpoint, attached, [{ name: "prompt", port: "prompt" }]);
174
+ return { records, fragments: [fragment], components: [{
175
+ id, fragment: fragment.id, inputs: componentInputs, outputs: { video: `${id}.video` }, range: element.range,
176
+ }] };
177
+ }
178
+
179
+ function promptReference(
180
+ element: StructuredElement,
181
+ resolveReference: (path: string) => SurfaceResolvedReference | undefined,
182
+ ): SurfaceResolvedReference {
183
+ const prompt = ref(element, "prompt", textTypes.text, resolveReference);
184
+ if (prompt.record !== undefined) {
185
+ assert(prompt.record.value.kind === "inline", `${element.name}.prompt must reference Text`);
186
+ verifyText(prompt.record.value.value);
187
+ }
188
+ return prompt;
144
189
  }
145
190
 
146
- export const decodePixverseVideoSurface = decoder(pixverseEndpoints.video!);
191
+ const VIDEO_ATTRIBUTES = ["id", "model", "prompt", "first-frame", "last-frame", "duration", "quality",
192
+ "aspect-ratio", "generate-audio", "multi-clip", "seed"] as const;
193
+
194
+ export const decodePixverseVideoSurface: StructuredSurfaceHandler = ({ element, resolveReference }) => {
195
+ exact(element, VIDEO_ATTRIBUTES, ["id", "model", "prompt", "duration", "quality"]);
196
+ empty(element);
197
+ const { endpoint } = selectModel(element);
198
+ const frames = ([["firstFrame", "first-frame"], ["lastFrame", "last-frame"]] as const)
199
+ .filter(([, attribute]) => element.attributes[attribute] !== undefined)
200
+ .map(([port, attribute]) => ({
201
+ port, role: "image" as const, source: media(element, attribute, "image", resolveReference),
202
+ }));
203
+ return assemble(element, endpoint, promptReference(element, resolveReference), frames, {
204
+ duration: integer(element, "duration"),
205
+ quality: [text(element, "quality")],
206
+ aspectRatio: optionalText(element, "aspect-ratio"),
207
+ generateAudio: optionalFlag(element, "generate-audio"),
208
+ multiClip: modelScoped(element, endpoint, "multi-clip", "multiClip", optionalFlag),
209
+ seed: modelScoped(element, endpoint, "seed", "seed", optionalInteger),
210
+ });
211
+ };
212
+
213
+ const REFERENCE_ATTRIBUTES = ["id", "model", "prompt", "duration", "quality",
214
+ "aspect-ratio", "generate-audio", "seed"] as const;
215
+
216
+ export const decodePixverseReferenceVideoSurface: StructuredSurfaceHandler = ({ element, resolveReference }) => {
217
+ exact(element, REFERENCE_ATTRIBUTES, ["id", "model", "prompt", "quality"]);
218
+ const { endpoint } = selectModel(element);
219
+ const references: MediaInput[] = [];
220
+ for (const child of element.children) {
221
+ if (child.kind === "text") {
222
+ assert(child.value.trim().length === 0, `${element.name} accepts only Reference children`);
223
+ continue;
224
+ }
225
+ assert(localName(child.name) === "Reference", `${element.name} accepts only Reference children`);
226
+ empty(child);
227
+ const roles = (["image", "video"] as const).filter((role) => child.attributes[role] !== undefined);
228
+ exact(child, ["image", "video"], []);
229
+ assert(roles.length === 1, `${child.name} requires exactly one of image, video`);
230
+ const role = roles[0]!;
231
+ references.push({
232
+ port: role === "image" ? "referenceImage" : "referenceVideo",
233
+ role,
234
+ source: media(child, role, role, resolveReference),
235
+ });
236
+ }
237
+ assert(references.length > 0, `${element.name} requires at least one Reference`);
238
+ for (const port of ["referenceImage", "referenceVideo"] as const) {
239
+ capacity(element, endpoint, port, references.filter((item) => item.port === port).length);
240
+ }
241
+ const videos = references.some((item) => item.port === "referenceVideo");
242
+ if (videos) {
243
+ assert(element.attributes["duration"] === undefined,
244
+ `${element.name} takes no duration; its video references carry the length of the run`);
245
+ } else {
246
+ assert(element.attributes["duration"] !== undefined, `${element.name} requires duration`);
247
+ }
248
+ return assemble(element, endpoint, promptReference(element, resolveReference), references, {
249
+ duration: optionalInteger(element, "duration"),
250
+ quality: [text(element, "quality")],
251
+ aspectRatio: optionalText(element, "aspect-ratio"),
252
+ generateAudio: optionalFlag(element, "generate-audio"),
253
+ seed: modelScoped(element, endpoint, "seed", "seed", optionalInteger),
254
+ });
255
+ };
@@ -0,0 +1,13 @@
1
+ import type { GenerationRequest } from "@hypit/generation";
2
+ import type { PixverseModel } from "./index.js";
3
+
4
+ /**
5
+ * Reference videos carry the length of the run, so that mode states no duration.
6
+ * Every other mode states its own.
7
+ */
8
+ export function pixverseRequestValidator(model: PixverseModel): (request: GenerationRequest) => void {
9
+ return (request) => {
10
+ if (request.ports.referenceVideo !== undefined || request.ports.duration !== undefined) return;
11
+ throw new Error(`${model} requires duration`);
12
+ };
13
+ }
@@ -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
+ ];