@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 +12 -2
- package/package.json +3 -2
- package/packages/pixverse/README.md +1 -1
- package/packages/pixverse/src/index.ts +2 -2
- package/packages/provider-beatapi/README.md +84 -0
- package/packages/provider-beatapi/package.json +23 -0
- package/packages/provider-beatapi/src/activation.ts +62 -0
- package/packages/provider-beatapi/src/errors.ts +46 -0
- package/packages/provider-beatapi/src/index.ts +2 -0
- package/packages/provider-beatapi/src/mapping.ts +89 -0
- package/packages/provider-beatapi/src/provider.ts +244 -0
- package/packages/provider-beatapi/src/routes.ts +127 -0
- package/packages/provider-hiapi/README.md +1 -3
- package/packages/provider-hiapi/src/provider.ts +7 -1
- package/packages/provider-hypihub/src/provider.ts +8 -1
- package/packages/provider-monid/README.md +0 -2
- package/packages/provider-monid/src/provider.ts +7 -1
- package/packages/provider-pollo/src/provider.ts +7 -1
- package/packages/provider-tokendance/src/provider.ts +8 -1
- package/packages/studio/src/server.ts +17 -2
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&utm_medium=badge&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.
|
|
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-
|
|
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 `
|
|
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 = ["
|
|
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 `
|
|
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,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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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") {
|