@hypit/hypit 0.2.3 → 0.2.5

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.
Files changed (35) hide show
  1. package/package.json +5 -1
  2. package/packages/media-execution/src/execute.ts +3 -2
  3. package/packages/provider-hiapi/README.md +75 -0
  4. package/packages/provider-hiapi/package.json +24 -0
  5. package/packages/provider-hiapi/src/activation.ts +62 -0
  6. package/packages/provider-hiapi/src/errors.ts +44 -0
  7. package/packages/provider-hiapi/src/index.ts +2 -0
  8. package/packages/provider-hiapi/src/mapping.ts +129 -0
  9. package/packages/provider-hiapi/src/provider.ts +215 -0
  10. package/packages/provider-hiapi/src/routes.ts +154 -0
  11. package/packages/provider-monid/README.md +72 -0
  12. package/packages/provider-monid/package.json +20 -0
  13. package/packages/provider-monid/src/activation.ts +62 -0
  14. package/packages/provider-monid/src/errors.ts +55 -0
  15. package/packages/provider-monid/src/index.ts +2 -0
  16. package/packages/provider-monid/src/mapping.ts +66 -0
  17. package/packages/provider-monid/src/provider.ts +245 -0
  18. package/packages/provider-monid/src/routes.ts +132 -0
  19. package/packages/provider-pollo/README.md +59 -0
  20. package/packages/provider-pollo/package.json +22 -0
  21. package/packages/provider-pollo/src/activation.ts +62 -0
  22. package/packages/provider-pollo/src/errors.ts +41 -0
  23. package/packages/provider-pollo/src/index.ts +2 -0
  24. package/packages/provider-pollo/src/mapping.ts +60 -0
  25. package/packages/provider-pollo/src/provider.ts +194 -0
  26. package/packages/provider-pollo/src/routes.ts +120 -0
  27. package/packages/provider-tokendance/README.md +67 -0
  28. package/packages/provider-tokendance/package.json +21 -0
  29. package/packages/provider-tokendance/src/activation.ts +62 -0
  30. package/packages/provider-tokendance/src/errors.ts +47 -0
  31. package/packages/provider-tokendance/src/index.ts +2 -0
  32. package/packages/provider-tokendance/src/mapping.ts +60 -0
  33. package/packages/provider-tokendance/src/provider.ts +268 -0
  34. package/packages/provider-tokendance/src/routes.ts +192 -0
  35. package/packages/video-cli/package.json +4 -0
@@ -0,0 +1,268 @@
1
+ import { requestDeadline } from "@hypit/runtime-kit";
2
+ import type { AsyncEndpoint, EndpointCredential, EndpointInvocationContext, EndpointOutcome, ImmediateEndpointHandler } 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 { tokenDanceRouteForCapability, tokenDanceRoutes } from "./routes.js";
10
+ import type { TokenDanceRoute } from "./routes.js";
11
+ import { TokenDanceHttpError, TokenDanceServiceError, tokenDanceTaskFailure } from "./errors.js";
12
+
13
+ export const tokenDanceProviderModuleRef = { name: "@hypit/provider-tokendance", version: "1" } as const;
14
+
15
+ export type CreateTokenDanceProviderOptions = {
16
+ readonly instance?: string;
17
+ readonly pool?: string;
18
+ readonly baseUrl?: string;
19
+ readonly apiKey?: CredentialRef;
20
+ readonly defaultConcurrency?: number;
21
+ readonly actionLimits?: import("@hypit/endpoint-kit").EndpointActionLimits;
22
+ readonly pollIntervalMs?: number;
23
+ readonly requestTimeoutMs?: number;
24
+ readonly operationTimeoutMs?: number;
25
+ readonly fetch?: typeof globalThis.fetch;
26
+ /** Publish a referenced Resource at a URL the service can fetch; replaces inline data URLs. */
27
+ readonly publicAssetUrl?: (artifact: BlobRef, artifacts: ResourceStore, fields?: Readonly<Record<string, string | number | boolean>>) => Promise<string>;
28
+ };
29
+
30
+ type Handle = {
31
+ readonly contract: "hypit.tokendance-operation@1";
32
+ readonly taskId: string;
33
+ readonly route: string;
34
+ readonly startedAt: number;
35
+ readonly url?: string;
36
+ };
37
+
38
+ function assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); }
39
+ function object(value: unknown, subject: string): Record<string, unknown> {
40
+ assert(value !== null && typeof value === "object" && !Array.isArray(value), `${subject} must be an object`);
41
+ return value as Record<string, unknown>;
42
+ }
43
+ function capabilityKey(capability: CapabilityRef): string { return `${capability.module.name}@${capability.module.version}#${capability.name}`; }
44
+ function apiBaseUrl(value: string): string {
45
+ let trimmed = value.trim();
46
+ while (trimmed.endsWith("/")) trimmed = trimmed.slice(0, -1);
47
+ assert(trimmed.length > 0, "TokenDance base URL is empty");
48
+ return trimmed;
49
+ }
50
+ function apiKey(credentials: Readonly<Record<string, EndpointCredential>>): string {
51
+ const value = credentials.apiKey?.secret;
52
+ assert(typeof value === "string" && value.length > 0, "TokenDance apiKey credential is unavailable; store a TokenDance API key for this Endpoint");
53
+ return value;
54
+ }
55
+ function failureMessage(error: unknown): string {
56
+ return error instanceof Error ? error.message : String(error);
57
+ }
58
+ function failure(error: unknown): EndpointOutcome {
59
+ return { status: "failed", failure: { code: error instanceof TokenDanceServiceError ? error.code : "TOKENDANCE_ERROR", message: failureMessage(error) } };
60
+ }
61
+ function httpsUrl(value: unknown, subject: string): string {
62
+ assert(typeof value === "string" && /^https?:\/\//u.test(value), `${subject} has no download URL`);
63
+ return value;
64
+ }
65
+
66
+ class TokenDanceClient {
67
+ constructor(readonly baseUrl: string, readonly timeout: number, readonly fetcher: typeof globalThis.fetch) {}
68
+ async json(path: string, key: string, init: RequestInit = {}): Promise<Record<string, unknown>> {
69
+ const deadline = requestDeadline(this.timeout);
70
+ try {
71
+ const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
72
+ ...init, signal: deadline.signal, headers: { authorization: `Bearer ${key}`, ...(init.headers ?? {}) },
73
+ }));
74
+ const text = await deadline.wait(response.text());
75
+ if (!response.ok) {
76
+ const input = typeof init.body === "string" ? JSON.parse(init.body) as Record<string, unknown> : undefined;
77
+ throw new TokenDanceHttpError(response.status, response, text, {
78
+ method: init.method ?? "GET", path, ...(typeof input?.model === "string" ? { model: input.model } : {}),
79
+ });
80
+ }
81
+ let body: unknown;
82
+ try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new Error(`TokenDance returned invalid JSON (${response.status})`); }
83
+ return object(body, "TokenDance response");
84
+ } finally { deadline.finish(); }
85
+ }
86
+ /** MiniMax's file API through the gateway; the returned id is referenced as `mm_file://{file_id}`. */
87
+ async uploadMiniMaxInput(bytes: Uint8Array, artifact: BlobRef, key: string): Promise<string> {
88
+ const form = new FormData();
89
+ form.set("purpose", "video_generation_input");
90
+ form.set("file", new Blob([new Uint8Array(bytes)], { type: artifact.mediaType }), `${artifact.resource}.${artifact.mediaType.split("/")[1] ?? "bin"}`);
91
+ const response = await this.json("/minimax/v1/files/upload", key, { method: "POST", body: form });
92
+ const status = object(response.base_resp ?? {}, "TokenDance upload base_resp").status_code;
93
+ assert(status === undefined || status === 0, `TokenDance MiniMax upload rejected: ${String(object(response.base_resp, "TokenDance upload base_resp").status_msg ?? status)}`);
94
+ const id = object(response.file, "TokenDance upload file").file_id;
95
+ assert((typeof id === "string" && id.length > 0) || typeof id === "number", "TokenDance MiniMax upload returned no file_id");
96
+ return `mm_file://${String(id)}`;
97
+ }
98
+ async download(url: string): Promise<{ readonly bytes: Uint8Array; readonly mediaType: string }> {
99
+ const deadline = requestDeadline(this.timeout);
100
+ try {
101
+ const response = await deadline.wait(this.fetcher(url, { signal: deadline.signal }));
102
+ if (!response.ok) throw new Error(`TokenDance asset returned HTTP ${response.status}`);
103
+ return { bytes: new Uint8Array(await deadline.wait(response.arrayBuffer())), mediaType: response.headers.get("content-type")?.split(";", 1)[0] ?? "application/octet-stream" };
104
+ } finally { deadline.finish(); }
105
+ }
106
+ }
107
+
108
+ function mediaKind(mediaType: string): "image" | "video" | "audio" | undefined {
109
+ const kind = mediaType.split("/", 1)[0];
110
+ return kind === "image" || kind === "video" || kind === "audio" ? kind : undefined;
111
+ }
112
+
113
+ function resolverFor(client: TokenDanceClient, route: TokenDanceRoute, context: EndpointInvocationContext, publicAssetUrl: CreateTokenDanceProviderOptions["publicAssetUrl"]): GenerationArtifactUrlResolver {
114
+ const resolved = new Map<string, Promise<string>>();
115
+ return (artifact, fields) => {
116
+ const existing = resolved.get(artifact.resource);
117
+ if (existing !== undefined) return existing;
118
+ const promise = (async () => {
119
+ if (publicAssetUrl !== undefined) return await publicAssetUrl(artifact, context.resources, fields);
120
+ const kind = mediaKind(artifact.mediaType);
121
+ const limit = kind === undefined ? undefined : route.mediaLimits[kind];
122
+ assert(limit !== undefined,
123
+ `TokenDance ${route.protocol} accepts ${artifact.mediaType} references only by public URL; configure publicAssetUrl for this Endpoint`);
124
+ assert(artifact.size <= limit,
125
+ `TokenDance ${route.protocol} accepts ${kind} references up to ${limit / 1_000_000} MB; ${artifact.resource} is ${artifact.size} bytes`);
126
+ const bytes = await context.resources.get(artifact.resource);
127
+ assert(bytes !== undefined && bytes.byteLength === artifact.size, `Reference Resource ${artifact.resource} is unavailable or has changed`);
128
+ if (route.protocol === "minimax-video") return await client.uploadMiniMaxInput(bytes, artifact, apiKey(context.credentials));
129
+ return `data:${artifact.mediaType};base64,${Buffer.from(bytes).toString("base64")}`;
130
+ })();
131
+ resolved.set(artifact.resource, promise);
132
+ return promise;
133
+ };
134
+ }
135
+
136
+ async function prepare(client: TokenDanceClient, context: EndpointInvocationContext, publicAssetUrl: CreateTokenDanceProviderOptions["publicAssetUrl"]) {
137
+ const route = tokenDanceRouteForCapability(context.need.capability);
138
+ assert(route !== undefined, "TokenDance does not implement this exact capability");
139
+ const request = route.prepare(context.need.constraints);
140
+ await context.reportProgress?.({ phase: `Preparing TokenDance request: ${request.model}` });
141
+ let body: string;
142
+ try {
143
+ body = JSON.stringify(await request.compile(resolverFor(client, route, context, publicAssetUrl)));
144
+ const cap = route.mediaLimits.body;
145
+ assert(cap === undefined || Buffer.byteLength(body) <= cap,
146
+ `TokenDance ${route.protocol} accepts request bodies up to ${(cap ?? 0) / 1_000_000} MB; inline references make this one ${Buffer.byteLength(body)} bytes`);
147
+ } catch (error) {
148
+ throw new TokenDanceServiceError(error instanceof TokenDanceServiceError ? error.code : "TOKENDANCE_ERROR",
149
+ `TokenDance request preparation failed; model=${request.model}; generation not submitted: ${failureMessage(error)}`);
150
+ }
151
+ return { route, model: request.model, body };
152
+ }
153
+
154
+ async function store(client: TokenDanceClient, urls: readonly string[], resources: ResourceStore): Promise<BlobRef[]> {
155
+ const blobs: BlobRef[] = [];
156
+ for (const url of urls) {
157
+ const downloaded = await client.download(url);
158
+ blobs.push(await resources.put(downloaded.bytes, downloaded.mediaType));
159
+ }
160
+ return blobs;
161
+ }
162
+
163
+ const paths = {
164
+ "ark-video": { submit: "/ark/v3/generations/tasks", task: (id: string) => `/ark/v3/generations/tasks/${encodeURIComponent(id)}` },
165
+ "minimax-video": { submit: "/minimax/v2/video_generation", task: (id: string) => `/minimax/v2/query/video_generation/${encodeURIComponent(id)}` },
166
+ } as const;
167
+
168
+ function taskId(protocol: keyof typeof paths, response: Record<string, unknown>): string {
169
+ const id = protocol === "ark-video" ? response.id : response.task_id;
170
+ assert(typeof id === "string" && id.length > 0, "TokenDance response has no task id");
171
+ return id;
172
+ }
173
+
174
+ /** Ark answers the task itself; MiniMax wraps it in `task`. */
175
+ function taskBody(protocol: keyof typeof paths, response: Record<string, unknown>): Record<string, unknown> {
176
+ return protocol === "ark-video" ? response : object(response.task, "TokenDance task");
177
+ }
178
+
179
+ function taskVideoUrl(protocol: keyof typeof paths, task: Record<string, unknown>): string {
180
+ const content = object(task.content, "TokenDance task content");
181
+ return httpsUrl(protocol === "ark-video" ? content.video_url : content.url, "TokenDance task");
182
+ }
183
+
184
+ function endpoint(client: TokenDanceClient, pollIntervalMs: number, maxOperationMs: number, publicAssetUrl: CreateTokenDanceProviderOptions["publicAssetUrl"]): AsyncEndpoint {
185
+ return {
186
+ async start(context) {
187
+ try {
188
+ const { route, model, body } = await prepare(client, context, publicAssetUrl);
189
+ assert(route.protocol !== "ark-image", "TokenDance image capabilities use an immediate endpoint");
190
+ await context.reportProgress?.({ phase: `Submitting TokenDance request: ${model}` });
191
+ const response = await client.json(paths[route.protocol].submit, apiKey(context.credentials), {
192
+ method: "POST", headers: { "content-type": "application/json" }, body,
193
+ });
194
+ const handle: Handle = { contract: "hypit.tokendance-operation@1", taskId: taskId(route.protocol, response), route: route.key, startedAt: Date.now() };
195
+ const receipt = { id: handle.taskId };
196
+ await context.checkpoint?.({ handle: canonicalize(handle), receipt });
197
+ return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "submitted" }), receipt };
198
+ } catch (error) {
199
+ return failure(error);
200
+ }
201
+ },
202
+ async poll(context) {
203
+ try {
204
+ const handle = object(context.handle, "TokenDance handle") as unknown as Handle;
205
+ const route = tokenDanceRouteForCapability(context.need.capability);
206
+ assert(route !== undefined && handle.contract === "hypit.tokendance-operation@1" && handle.route === route.key && route.protocol !== "ark-image", "TokenDance handle is invalid");
207
+ const receipt = { id: handle.taskId };
208
+ if (Date.now() - handle.startedAt > maxOperationMs) {
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
+ }
211
+ const task = taskBody(route.protocol, await client.json(paths[route.protocol].task(handle.taskId), apiKey(context.credentials)));
212
+ const status = String(task.status);
213
+ if (status === "queued" || status === "running") return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
214
+ const rejected = tokenDanceTaskFailure(task, handle.taskId);
215
+ if (rejected !== undefined) return { ...failure(rejected), receipt };
216
+ assert(status === "succeeded", `TokenDance returned unknown task status ${status}`);
217
+ return { status: "ready", handle: canonicalize({ ...handle, url: taskVideoUrl(route.protocol, task) }), receipt };
218
+ } catch (error) {
219
+ return failure(error);
220
+ }
221
+ },
222
+ async collect(context) {
223
+ try {
224
+ const handle = object(context.handle, "TokenDance handle") as unknown as Handle;
225
+ const route = tokenDanceRouteForCapability(context.need.capability);
226
+ assert(route !== undefined && handle.route === route.key, "TokenDance collection route differs");
227
+ await context.reportProgress?.({ phase: "Receiving generated video" });
228
+ const blobs = await store(client, [httpsUrl(handle.url, "TokenDance handle")], context.resources);
229
+ return { status: "completed", result: { value: route.packageResult(blobs) }, receipt: { id: handle.taskId } };
230
+ } catch (error) {
231
+ return failure(error);
232
+ }
233
+ },
234
+ };
235
+ }
236
+
237
+ export function createTokenDanceProvider(options: CreateTokenDanceProviderOptions = {}) {
238
+ const requestTimeoutMs = options.requestTimeoutMs ?? 300_000;
239
+ const operationTimeoutMs = options.operationTimeoutMs ?? 30 * 60_000;
240
+ for (const [name, value] of Object.entries({ requestTimeoutMs, operationTimeoutMs })) {
241
+ assert(Number.isSafeInteger(value) && value > 0, `TokenDance ${name} must be a positive integer`);
242
+ }
243
+ const client = new TokenDanceClient(apiBaseUrl(options.baseUrl ?? "https://tokendance.space/gateway"), requestTimeoutMs, options.fetch ?? globalThis.fetch);
244
+ const asyncEndpoint = endpoint(client, options.pollIntervalMs ?? 10_000, operationTimeoutMs, options.publicAssetUrl);
245
+ const imageEndpoint: ImmediateEndpointHandler = async (context) => {
246
+ const { route, model, body } = await prepare(client, context, options.publicAssetUrl);
247
+ assert(route.protocol === "ark-image", "TokenDance video capabilities use an asynchronous endpoint");
248
+ await context.reportProgress?.({ phase: `Submitting TokenDance request: ${model}` });
249
+ const response = await client.json("/ark/v3/images/generations", apiKey(context.credentials), {
250
+ method: "POST", headers: { "content-type": "application/json" }, body,
251
+ });
252
+ assert(Array.isArray(response.data) && response.data.length > 0, "TokenDance image response has no data");
253
+ const urls = response.data.map((item, index) => httpsUrl(object(item, `TokenDance image ${index + 1}`).url, `TokenDance image ${index + 1}`));
254
+ await context.reportProgress?.({ phase: "Receiving generated images" });
255
+ return { value: route.packageResult(await store(client, urls, context.resources)) };
256
+ };
257
+ return defineEndpointPackage({
258
+ module: tokenDanceProviderModuleRef, facet: "gateway", instance: options.instance ?? "tokendance.default", pool: options.pool ?? options.instance ?? "tokendance.default",
259
+ pricing: { kind: "page", url: "https://tokendance.space/models" },
260
+ credentials: { apiKey: options.apiKey ?? credentialRef("os", "tokendance.api-key") },
261
+ credentialInputs: { apiKey: { label: "TokenDance API key" } },
262
+ defaultConcurrency: options.defaultConcurrency ?? 4,
263
+ ...(options.actionLimits === undefined ? {} : { actionLimits: options.actionLimits }),
264
+ capabilities: tokenDanceRoutes.map((route) => route.protocol === "ark-image"
265
+ ? { capability: route.capability, returns: route.returns, lifecycle: "immediate" as const, handler: imageEndpoint, capacity: route.capability.name, supports: route.supports }
266
+ : { capability: route.capability, returns: route.returns, lifecycle: "asynchronous" as const, endpoint: asyncEndpoint, capacity: route.capability.name, supports: route.supports }),
267
+ });
268
+ }
@@ -0,0 +1,192 @@
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 { tokenDanceMappings } from "./mapping.js";
13
+
14
+ /** Which TokenDance protocol a capability submits through. */
15
+ export type TokenDanceProtocol = "ark-video" | "ark-image" | "minimax-video";
16
+
17
+ export type TokenDancePreparedRequest = {
18
+ readonly model: string;
19
+ readonly protocol: TokenDanceProtocol;
20
+ readonly compile: (resolve: GenerationArtifactUrlResolver) => Promise<Record<string, unknown>>;
21
+ };
22
+
23
+ /** Documented byte limits for one protocol's media inputs; an absent kind is not accepted inline. */
24
+ export type TokenDanceMediaLimits = {
25
+ readonly image?: number;
26
+ readonly video?: number;
27
+ readonly audio?: number;
28
+ /** Documented cap on the whole JSON request body, when inline media count toward it. */
29
+ readonly body?: number;
30
+ };
31
+
32
+ const MB = 1_000_000;
33
+
34
+ /**
35
+ * Ark: images under 30 MB and audio at most 15 MB may travel as Base64 within a 64 MB request body;
36
+ * videos take URLs only. MiniMax: every kind is uploaded through its file API within these sizes.
37
+ */
38
+ export const tokenDanceMediaLimits: Readonly<Record<TokenDanceProtocol, TokenDanceMediaLimits>> = {
39
+ "ark-video": { image: 30 * MB, audio: 15 * MB, body: 64 * MB },
40
+ "ark-image": { image: 30 * MB },
41
+ "minimax-video": { image: 30 * MB, video: 50 * MB, audio: 15 * MB },
42
+ };
43
+
44
+ export type TokenDanceRoute = GenerationWireMapping & {
45
+ readonly key: string;
46
+ readonly returns: TypeRef;
47
+ readonly protocol: TokenDanceProtocol;
48
+ readonly mediaLimits: TokenDanceMediaLimits;
49
+ readonly supports: (request: EndpointRequest) => EndpointSupport;
50
+ readonly prepare: (constraints: CanonicalValue) => TokenDancePreparedRequest;
51
+ readonly packageResult: (artifacts: readonly BlobRef[]) => StoredValue;
52
+ };
53
+
54
+ function scalar(request: GenerationRequest, port: string): string | number | boolean | undefined {
55
+ const value = request.ports[port]?.[0];
56
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : undefined;
57
+ }
58
+ function present(request: GenerationRequest, port: string): boolean {
59
+ return (request.ports[port]?.length ?? 0) > 0;
60
+ }
61
+ function strings(value: unknown): readonly string[] {
62
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.length > 0) : [];
63
+ }
64
+
65
+ /** Seedream 5.0 lite pixel sizes for each resolution tier and aspect ratio, from the Ark image API reference. */
66
+ const SEEDREAM_SIZES: Readonly<Record<string, Readonly<Record<string, string>>>> = {
67
+ basic: { "1:1": "2048x2048", "4:3": "2304x1728", "3:4": "1728x2304", "16:9": "2848x1600", "9:16": "1600x2848", "3:2": "2496x1664", "2:3": "1664x2496", "21:9": "3136x1344" },
68
+ high: { "1:1": "3072x3072", "4:3": "3456x2592", "3:4": "2592x3456", "16:9": "4096x2304", "9:16": "2304x4096", "3:2": "3744x2496", "2:3": "2496x3744", "21:9": "4704x2016" },
69
+ ultra: { "1:1": "4096x4096", "4:3": "4704x3520", "3:4": "3520x4704", "16:9": "5504x3040", "9:16": "3040x5504", "3:2": "4992x3328", "2:3": "3328x4992", "21:9": "6240x2656" },
70
+ };
71
+
72
+ function rejection(mapping: GenerationWireMapping, request: GenerationRequest): string | undefined {
73
+ const ratio = scalar(request, "aspectRatio");
74
+ if (mapping.capability.module.name === "@hypit/seedance") {
75
+ if (mapping.capability.name === "seedance-2.5" && present(request, "firstFrame") && ratio !== "adaptive") {
76
+ return `Seedance 2.5 frame mode on TokenDance takes only aspect-ratio adaptive, not ${String(ratio)}`;
77
+ }
78
+ return undefined;
79
+ }
80
+ if (mapping.capability.module.name === "@hypit/seedream") {
81
+ const quality = String(scalar(request, "quality"));
82
+ if (SEEDREAM_SIZES[quality]?.[String(ratio)] === undefined) {
83
+ return `TokenDance Seedream 5.0 lite has no size for quality ${quality} at ${String(ratio)}`;
84
+ }
85
+ return undefined;
86
+ }
87
+ if (mapping.capability.module.name === "@hypit/minimax-h3") {
88
+ const hasMedia = ["referenceImage", "referenceVideo", "referenceAudio", "firstFrame", "lastFrame"]
89
+ .some((port) => present(request, port));
90
+ if (!hasMedia && (ratio === undefined || ratio === "adaptive")) {
91
+ return "MiniMax H3 text-to-video on TokenDance requires an explicit aspect-ratio";
92
+ }
93
+ }
94
+ return undefined;
95
+ }
96
+
97
+ function contentItem(type: "image_url" | "video_url" | "audio_url", url: string, role: string) {
98
+ return { type, [type]: { url }, role };
99
+ }
100
+
101
+ /** Ark and MiniMax both take one `content` array of typed, role-tagged items. */
102
+ function contentArray(input: Record<string, unknown>) {
103
+ const first = input.first_frame;
104
+ const last = input.last_frame;
105
+ return [
106
+ { type: "text", text: input.text },
107
+ ...(typeof first === "string" ? [contentItem("image_url", first, "first_frame")] : []),
108
+ ...(typeof last === "string" ? [contentItem("image_url", last, "last_frame")] : []),
109
+ ...strings(input.reference_image).map((url) => contentItem("image_url", url, "reference_image")),
110
+ ...strings(input.reference_video).map((url) => contentItem("video_url", url, "reference_video")),
111
+ ...strings(input.reference_audio).map((url) => contentItem("audio_url", url, "reference_audio")),
112
+ ];
113
+ }
114
+
115
+ function arkVideoBody(model: string, input: Record<string, unknown>): Record<string, unknown> {
116
+ return {
117
+ model,
118
+ content: contentArray(input),
119
+ resolution: input.resolution,
120
+ ratio: input.ratio,
121
+ duration: input.duration,
122
+ generate_audio: input.generate_audio,
123
+ ...(input.web_search === true ? { tools: [{ type: "web_search" }] } : {}),
124
+ };
125
+ }
126
+
127
+ function arkImageBody(model: string, input: Record<string, unknown>): Record<string, unknown> {
128
+ const images = strings(input.image);
129
+ return {
130
+ model,
131
+ prompt: input.prompt,
132
+ ...(images.length === 1 ? { image: images[0] } : images.length > 1 ? { image: images } : {}),
133
+ size: SEEDREAM_SIZES[String(input.quality)]![String(input.aspect_ratio)]!,
134
+ output_format: input.output_format,
135
+ response_format: "url",
136
+ watermark: false,
137
+ };
138
+ }
139
+
140
+ function minimaxVideoBody(model: string, input: Record<string, unknown>): Record<string, unknown> {
141
+ return {
142
+ model,
143
+ content: contentArray(input),
144
+ resolution: input.resolution,
145
+ duration: input.duration,
146
+ ...(typeof input.ratio === "string" ? { ratio: input.ratio } : {}),
147
+ };
148
+ }
149
+
150
+ function capabilityKey(capability: CapabilityRef): string {
151
+ return `${capability.module.name}@${capability.module.version}#${capability.name}`;
152
+ }
153
+
154
+ export const tokenDanceRoutes: readonly TokenDanceRoute[] = tokenDanceMappings.map((mapping) => {
155
+ const protocol: TokenDanceProtocol = mapping.capability.module.name === "@hypit/minimax-h3" ? "minimax-video"
156
+ : mapping.result === "image" ? "ark-image" : "ark-video";
157
+ const body = protocol === "ark-video" ? arkVideoBody : protocol === "ark-image" ? arkImageBody : minimaxVideoBody;
158
+ return {
159
+ ...mapping,
160
+ key: capabilityKey(mapping.capability),
161
+ returns: mapping.result === "image" ? generationTypes.imageSet : generationTypes.videoSet,
162
+ protocol,
163
+ mediaLimits: tokenDanceMediaLimits[protocol],
164
+ supports: (request) => {
165
+ const reason = rejection(mapping, request.constraints as unknown as GenerationRequest);
166
+ return reason === undefined ? { status: "supported" } : { status: "unsupported", reason };
167
+ },
168
+ prepare: (constraints) => {
169
+ const request = constraints as unknown as GenerationRequest;
170
+ const reason = rejection(mapping, request);
171
+ if (reason !== undefined) throw new Error(reason);
172
+ const model = selectWireModelForRequest(mapping, request);
173
+ return {
174
+ model,
175
+ protocol,
176
+ compile: async (resolve) => body(model, (await compileWireRequest(mapping, request, resolve)).input as Record<string, unknown>),
177
+ };
178
+ },
179
+ packageResult: (artifacts) => ({
180
+ kind: "inline",
181
+ value: canonicalize(mapping.result === "image"
182
+ ? sealGeneratedImageSet({ images: artifacts })
183
+ : sealGeneratedVideoSet({ videos: artifacts })),
184
+ }),
185
+ };
186
+ });
187
+
188
+ const byCapability = new Map(tokenDanceRoutes.map((route) => [route.key, route]));
189
+
190
+ export function tokenDanceRouteForCapability(capability: CapabilityRef): TokenDanceRoute | undefined {
191
+ return byCapability.get(capabilityKey(capability));
192
+ }
@@ -23,7 +23,11 @@
23
23
  "@hypit/package-loader-node": "workspace:*",
24
24
  "@hypit/project-context-node": "workspace:*",
25
25
  "@hypit/protocol": "workspace:*",
26
+ "@hypit/provider-hiapi": "workspace:*",
26
27
  "@hypit/provider-hypihub": "workspace:*",
28
+ "@hypit/provider-monid": "workspace:*",
29
+ "@hypit/provider-pollo": "workspace:*",
30
+ "@hypit/provider-tokendance": "workspace:*",
27
31
  "@hypit/runtime": "workspace:*",
28
32
  "@hypit/runtime-kit": "workspace:*",
29
33
  "@hypit/runtime-host-node": "workspace:*",