@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.
- package/package.json +5 -1
- package/packages/media-execution/src/execute.ts +3 -2
- package/packages/provider-hiapi/README.md +75 -0
- package/packages/provider-hiapi/package.json +24 -0
- package/packages/provider-hiapi/src/activation.ts +62 -0
- package/packages/provider-hiapi/src/errors.ts +44 -0
- package/packages/provider-hiapi/src/index.ts +2 -0
- package/packages/provider-hiapi/src/mapping.ts +129 -0
- package/packages/provider-hiapi/src/provider.ts +215 -0
- package/packages/provider-hiapi/src/routes.ts +154 -0
- package/packages/provider-monid/README.md +72 -0
- package/packages/provider-monid/package.json +20 -0
- package/packages/provider-monid/src/activation.ts +62 -0
- package/packages/provider-monid/src/errors.ts +55 -0
- package/packages/provider-monid/src/index.ts +2 -0
- package/packages/provider-monid/src/mapping.ts +66 -0
- package/packages/provider-monid/src/provider.ts +245 -0
- package/packages/provider-monid/src/routes.ts +132 -0
- package/packages/provider-pollo/README.md +59 -0
- package/packages/provider-pollo/package.json +22 -0
- package/packages/provider-pollo/src/activation.ts +62 -0
- package/packages/provider-pollo/src/errors.ts +41 -0
- package/packages/provider-pollo/src/index.ts +2 -0
- package/packages/provider-pollo/src/mapping.ts +60 -0
- package/packages/provider-pollo/src/provider.ts +194 -0
- package/packages/provider-pollo/src/routes.ts +120 -0
- package/packages/provider-tokendance/README.md +67 -0
- package/packages/provider-tokendance/package.json +21 -0
- package/packages/provider-tokendance/src/activation.ts +62 -0
- package/packages/provider-tokendance/src/errors.ts +47 -0
- package/packages/provider-tokendance/src/index.ts +2 -0
- package/packages/provider-tokendance/src/mapping.ts +60 -0
- package/packages/provider-tokendance/src/provider.ts +268 -0
- package/packages/provider-tokendance/src/routes.ts +192 -0
- package/packages/video-cli/package.json +4 -0
|
@@ -0,0 +1,194 @@
|
|
|
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 { polloRouteForCapability, polloRoutes } from "./routes.js";
|
|
10
|
+
import { PolloHttpError, PolloServiceError, polloTaskFailure } from "./errors.js";
|
|
11
|
+
|
|
12
|
+
export const polloProviderModuleRef = { name: "@hypit/provider-pollo", version: "1" } as const;
|
|
13
|
+
|
|
14
|
+
export type CreatePolloProviderOptions = {
|
|
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 an HTTP(S) URL Pollo can fetch. Pollo accepts no inline media. */
|
|
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.pollo-operation@1";
|
|
31
|
+
readonly taskId: string;
|
|
32
|
+
readonly route: string;
|
|
33
|
+
readonly startedAt: number;
|
|
34
|
+
readonly urls?: readonly string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); }
|
|
38
|
+
function object(value: unknown, subject: string): Record<string, unknown> {
|
|
39
|
+
assert(value !== null && typeof value === "object" && !Array.isArray(value), `${subject} must be an object`);
|
|
40
|
+
return value as Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
function capabilityKey(capability: CapabilityRef): string { return `${capability.module.name}@${capability.module.version}#${capability.name}`; }
|
|
43
|
+
function apiBaseUrl(value: string): string {
|
|
44
|
+
let trimmed = value.trim();
|
|
45
|
+
while (trimmed.endsWith("/")) trimmed = trimmed.slice(0, -1);
|
|
46
|
+
assert(trimmed.length > 0, "Pollo base URL is empty");
|
|
47
|
+
return trimmed;
|
|
48
|
+
}
|
|
49
|
+
function apiKey(credentials: Readonly<Record<string, EndpointCredential>>): string {
|
|
50
|
+
const value = credentials.apiKey?.secret;
|
|
51
|
+
assert(typeof value === "string" && value.length > 0, "Pollo apiKey credential is unavailable; store a Pollo API key for this Endpoint");
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function failureMessage(error: unknown): string {
|
|
55
|
+
return error instanceof Error ? error.message : String(error);
|
|
56
|
+
}
|
|
57
|
+
function failure(error: unknown): EndpointOutcome {
|
|
58
|
+
return { status: "failed", failure: { code: error instanceof PolloServiceError ? error.code : "POLLO_ERROR", message: failureMessage(error) } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class PolloClient {
|
|
62
|
+
constructor(readonly baseUrl: string, readonly timeout: number, readonly fetcher: typeof globalThis.fetch) {}
|
|
63
|
+
async json(path: string, key: string, init: RequestInit = {}): Promise<Record<string, unknown>> {
|
|
64
|
+
const deadline = requestDeadline(this.timeout);
|
|
65
|
+
try {
|
|
66
|
+
const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
|
|
67
|
+
...init, signal: deadline.signal, headers: { "x-api-key": key, ...(init.headers ?? {}) },
|
|
68
|
+
}));
|
|
69
|
+
const text = await deadline.wait(response.text());
|
|
70
|
+
if (!response.ok) throw new PolloHttpError(response.status, text, { method: init.method ?? "GET", path });
|
|
71
|
+
let body: unknown;
|
|
72
|
+
try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new Error(`Pollo returned invalid JSON (${response.status})`); }
|
|
73
|
+
return object(body, "Pollo response");
|
|
74
|
+
} finally { deadline.finish(); }
|
|
75
|
+
}
|
|
76
|
+
async download(url: string): Promise<{ readonly bytes: Uint8Array; readonly mediaType: string }> {
|
|
77
|
+
const deadline = requestDeadline(this.timeout);
|
|
78
|
+
try {
|
|
79
|
+
const response = await deadline.wait(this.fetcher(url, { signal: deadline.signal }));
|
|
80
|
+
if (!response.ok) throw new Error(`Pollo asset returned HTTP ${response.status}`);
|
|
81
|
+
return { bytes: new Uint8Array(await deadline.wait(response.arrayBuffer())), mediaType: response.headers.get("content-type")?.split(";", 1)[0] ?? "application/octet-stream" };
|
|
82
|
+
} finally { deadline.finish(); }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolverFor(context: EndpointInvocationContext, publicAssetUrl: CreatePolloProviderOptions["publicAssetUrl"]): GenerationArtifactUrlResolver {
|
|
87
|
+
const resolved = new Map<string, Promise<string>>();
|
|
88
|
+
return (artifact, fields) => {
|
|
89
|
+
const existing = resolved.get(artifact.resource);
|
|
90
|
+
if (existing !== undefined) return existing;
|
|
91
|
+
assert(publicAssetUrl !== undefined,
|
|
92
|
+
"Pollo accepts reference media only by HTTP(S) URL; configure publicAssetUrl for this Endpoint");
|
|
93
|
+
const promise = publicAssetUrl(artifact, context.resources, fields);
|
|
94
|
+
resolved.set(artifact.resource, promise);
|
|
95
|
+
return promise;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function endpoint(client: PolloClient, pollIntervalMs: number, maxOperationMs: number, publicAssetUrl: CreatePolloProviderOptions["publicAssetUrl"]): AsyncEndpoint {
|
|
100
|
+
return {
|
|
101
|
+
async start(context) {
|
|
102
|
+
try {
|
|
103
|
+
const route = polloRouteForCapability(context.need.capability);
|
|
104
|
+
assert(route !== undefined, "Pollo does not implement this exact capability");
|
|
105
|
+
const request = route.prepare(context.need.constraints);
|
|
106
|
+
await context.reportProgress?.({ phase: `Preparing Pollo request: ${request.path}` });
|
|
107
|
+
let body: Record<string, unknown>;
|
|
108
|
+
try {
|
|
109
|
+
body = await request.compile(resolverFor(context, publicAssetUrl));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new PolloServiceError(error instanceof PolloServiceError ? error.code : "POLLO_ERROR",
|
|
112
|
+
`Pollo request preparation failed; path=${request.path}; generation not submitted: ${failureMessage(error)}`);
|
|
113
|
+
}
|
|
114
|
+
await context.reportProgress?.({ phase: `Submitting Pollo request: ${request.path}` });
|
|
115
|
+
const response = await client.json(request.path, apiKey(context.credentials), {
|
|
116
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body),
|
|
117
|
+
});
|
|
118
|
+
assert(typeof response.taskId === "string" && response.taskId.length > 0, "Pollo response has no taskId");
|
|
119
|
+
const handle: Handle = { contract: "hypit.pollo-operation@1", taskId: response.taskId, route: route.key, startedAt: Date.now() };
|
|
120
|
+
const receipt = { id: handle.taskId };
|
|
121
|
+
await context.checkpoint?.({ handle: canonicalize(handle), receipt });
|
|
122
|
+
return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: String(response.status ?? "submitted") }), receipt };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return failure(error);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
async poll(context) {
|
|
128
|
+
try {
|
|
129
|
+
const handle = object(context.handle, "Pollo handle") as unknown as Handle;
|
|
130
|
+
const route = polloRouteForCapability(context.need.capability);
|
|
131
|
+
assert(route !== undefined && handle.contract === "hypit.pollo-operation@1" && handle.route === route.key, "Pollo handle is invalid");
|
|
132
|
+
const receipt = { id: handle.taskId };
|
|
133
|
+
if (Date.now() - handle.startedAt > maxOperationMs) {
|
|
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
|
+
}
|
|
136
|
+
const task = await client.json(`/v1/generation/${encodeURIComponent(handle.taskId)}/status`, apiKey(context.credentials));
|
|
137
|
+
assert(Array.isArray(task.generations) && task.generations.length > 0, "Pollo task has no generations");
|
|
138
|
+
const generations = task.generations.map((item, index) => object(item, `Pollo generation ${index + 1}`));
|
|
139
|
+
const rejected = polloTaskFailure(generations, handle.taskId);
|
|
140
|
+
if (rejected !== undefined) return { ...failure(rejected), receipt };
|
|
141
|
+
const pending = generations.find((generation) => generation.status !== "succeed");
|
|
142
|
+
if (pending !== undefined) {
|
|
143
|
+
const status = String(pending.status);
|
|
144
|
+
assert(status === "waiting" || status === "processing", `Pollo returned unknown generation status ${status}`);
|
|
145
|
+
return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
|
|
146
|
+
}
|
|
147
|
+
const urls = generations.map((generation, index) => {
|
|
148
|
+
assert(typeof generation.url === "string" && /^https?:\/\//u.test(generation.url), `Pollo generation ${index + 1} has no URL`);
|
|
149
|
+
return generation.url;
|
|
150
|
+
});
|
|
151
|
+
return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return failure(error);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
async collect(context) {
|
|
157
|
+
try {
|
|
158
|
+
const handle = object(context.handle, "Pollo handle") as unknown as Handle;
|
|
159
|
+
const route = polloRouteForCapability(context.need.capability);
|
|
160
|
+
assert(route !== undefined && handle.route === route.key && Array.isArray(handle.urls), "Pollo collection route differs");
|
|
161
|
+
await context.reportProgress?.({ phase: "Receiving generated files" });
|
|
162
|
+
const blobs: BlobRef[] = [];
|
|
163
|
+
for (const url of handle.urls) {
|
|
164
|
+
const downloaded = await client.download(url);
|
|
165
|
+
blobs.push(await context.resources.put(downloaded.bytes, downloaded.mediaType));
|
|
166
|
+
}
|
|
167
|
+
return { status: "completed", result: { value: route.packageResult(blobs) }, receipt: { id: handle.taskId } };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return failure(error);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function createPolloProvider(options: CreatePolloProviderOptions = {}) {
|
|
176
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 300_000;
|
|
177
|
+
const operationTimeoutMs = options.operationTimeoutMs ?? 30 * 60_000;
|
|
178
|
+
for (const [name, value] of Object.entries({ requestTimeoutMs, operationTimeoutMs })) {
|
|
179
|
+
assert(Number.isSafeInteger(value) && value > 0, `Pollo ${name} must be a positive integer`);
|
|
180
|
+
}
|
|
181
|
+
const client = new PolloClient(apiBaseUrl(options.baseUrl ?? "https://pollo.ai/api/platform"), requestTimeoutMs, options.fetch ?? globalThis.fetch);
|
|
182
|
+
const asyncEndpoint = endpoint(client, options.pollIntervalMs ?? 10_000, operationTimeoutMs, options.publicAssetUrl);
|
|
183
|
+
return defineEndpointPackage({
|
|
184
|
+
module: polloProviderModuleRef, facet: "gateway", instance: options.instance ?? "pollo.default", pool: options.pool ?? options.instance ?? "pollo.default",
|
|
185
|
+
pricing: { kind: "page", url: "https://api.pollo.ai/pricing" },
|
|
186
|
+
credentials: { apiKey: options.apiKey ?? credentialRef("os", "pollo.api-key") },
|
|
187
|
+
credentialInputs: { apiKey: { label: "Pollo API key" } },
|
|
188
|
+
defaultConcurrency: options.defaultConcurrency ?? 4,
|
|
189
|
+
...(options.actionLimits === undefined ? {} : { actionLimits: options.actionLimits }),
|
|
190
|
+
capabilities: polloRoutes.map((route) => ({
|
|
191
|
+
capability: route.capability, returns: route.returns, lifecycle: "asynchronous" as const, endpoint: asyncEndpoint, capacity: route.capability.name, supports: route.supports,
|
|
192
|
+
})),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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 { polloMappings } from "./mapping.js";
|
|
13
|
+
|
|
14
|
+
export type PolloPreparedRequest = {
|
|
15
|
+
/** Request path under the Pollo platform base URL. */
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly compile: (resolve: GenerationArtifactUrlResolver) => Promise<Record<string, unknown>>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type PolloRoute = GenerationWireMapping & {
|
|
21
|
+
readonly key: string;
|
|
22
|
+
readonly returns: TypeRef;
|
|
23
|
+
readonly supports: (request: EndpointRequest) => EndpointSupport;
|
|
24
|
+
readonly prepare: (constraints: CanonicalValue) => PolloPreparedRequest;
|
|
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
|
+
function strings(value: unknown): readonly string[] {
|
|
36
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.length > 0) : [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const GPT_IMAGE_RATIOS = ["1:1", "3:2", "2:3", "16:9", "9:16", "4:3", "3:4", "21:9", "auto"];
|
|
40
|
+
|
|
41
|
+
/** Pollo's documented input ranges for each mapped model, checked before any reference is resolved. */
|
|
42
|
+
function rejection(mapping: GenerationWireMapping, request: GenerationRequest): string | undefined {
|
|
43
|
+
const { name } = mapping.capability;
|
|
44
|
+
const ratio = scalar(request, "aspectRatio");
|
|
45
|
+
if (name === "grok-imagine-video-1.5-preview") {
|
|
46
|
+
if (count(request, "images") !== 1) return "Pollo Grok Imagine 1.5 animates exactly one image";
|
|
47
|
+
if (ratio !== "auto") return `Pollo Grok Imagine 1.5 takes no aspect ratio; use auto, not ${String(ratio)}`;
|
|
48
|
+
}
|
|
49
|
+
if (name === "gpt-image-2" && !GPT_IMAGE_RATIOS.includes(String(ratio))) {
|
|
50
|
+
return `Pollo GPT Image 2 does not render ${String(ratio)}`;
|
|
51
|
+
}
|
|
52
|
+
if (mapping.capability.module.name === "@hypit/nano-banana" && ratio === "auto") {
|
|
53
|
+
return `Pollo ${name} takes an explicit aspect ratio, not auto`;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ref(type: "image" | "video" | "audio") {
|
|
59
|
+
return (url: string) => ({ url, type });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Fields the model package requires that Pollo names differently or does not take. */
|
|
63
|
+
function normalize(mapping: GenerationWireMapping, input: Record<string, unknown>): Record<string, unknown> {
|
|
64
|
+
const { name } = mapping.capability;
|
|
65
|
+
if (name === "minimax-h3") {
|
|
66
|
+
const refs = [
|
|
67
|
+
...strings(input.refs_image).map(ref("image")),
|
|
68
|
+
...strings(input.refs_video).map(ref("video")),
|
|
69
|
+
...strings(input.refs_audio).map(ref("audio")),
|
|
70
|
+
];
|
|
71
|
+
delete input.refs_image;
|
|
72
|
+
delete input.refs_video;
|
|
73
|
+
delete input.refs_audio;
|
|
74
|
+
if (refs.length > 0) input.refs = refs;
|
|
75
|
+
}
|
|
76
|
+
if (name === "grok-imagine-video-1.5-preview") {
|
|
77
|
+
input.image = strings(input.images)[0];
|
|
78
|
+
delete input.images;
|
|
79
|
+
delete input.aspect_ratio;
|
|
80
|
+
}
|
|
81
|
+
if (mapping.capability.module.name === "@hypit/nano-banana") delete input.output_format;
|
|
82
|
+
return input;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function capabilityKey(capability: CapabilityRef): string {
|
|
86
|
+
return `${capability.module.name}@${capability.module.version}#${capability.name}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const polloRoutes: readonly PolloRoute[] = polloMappings.map((mapping) => ({
|
|
90
|
+
...mapping,
|
|
91
|
+
key: capabilityKey(mapping.capability),
|
|
92
|
+
returns: mapping.result === "image" ? generationTypes.imageSet : generationTypes.videoSet,
|
|
93
|
+
supports: (request) => {
|
|
94
|
+
const reason = rejection(mapping, request.constraints as unknown as GenerationRequest);
|
|
95
|
+
return reason === undefined ? { status: "supported" } : { status: "unsupported", reason };
|
|
96
|
+
},
|
|
97
|
+
prepare: (constraints) => {
|
|
98
|
+
const request = constraints as unknown as GenerationRequest;
|
|
99
|
+
const reason = rejection(mapping, request);
|
|
100
|
+
if (reason !== undefined) throw new Error(reason);
|
|
101
|
+
return {
|
|
102
|
+
path: selectWireModelForRequest(mapping, request),
|
|
103
|
+
compile: async (resolve) => ({
|
|
104
|
+
input: normalize(mapping, (await compileWireRequest(mapping, request, resolve)).input as Record<string, unknown>),
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
packageResult: (artifacts) => ({
|
|
109
|
+
kind: "inline",
|
|
110
|
+
value: canonicalize(mapping.result === "image"
|
|
111
|
+
? sealGeneratedImageSet({ images: artifacts })
|
|
112
|
+
: sealGeneratedVideoSet({ videos: artifacts })),
|
|
113
|
+
}),
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
const byCapability = new Map(polloRoutes.map((route) => [route.key, route]));
|
|
117
|
+
|
|
118
|
+
export function polloRouteForCapability(capability: CapabilityRef): PolloRoute | undefined {
|
|
119
|
+
return byCapability.get(capabilityKey(capability));
|
|
120
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# `@hypit/provider-tokendance`
|
|
2
|
+
|
|
3
|
+
Hypit Runtime Provider for a [TokenDance](https://tokendance.space) account. It submits generation
|
|
4
|
+
requests with a TokenDance API key through the gateway protocols TokenDance documents for each model
|
|
5
|
+
and stores the returned files in the current Build.
|
|
6
|
+
|
|
7
|
+
| Capability | TokenDance model | Protocol |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `@hypit/seedance@1#seedance-2` | `seedance-2.0` | Ark `POST /ark/v3/generations/tasks`, polled at `GET /ark/v3/generations/tasks/{id}` |
|
|
10
|
+
| `@hypit/seedance@1#seedance-2-fast` | `seedance-2.0-fast` | same |
|
|
11
|
+
| `@hypit/seedance@1#seedance-2-mini` | `seedance-2.0-mini` | same |
|
|
12
|
+
| `@hypit/seedance@1#seedance-2.5` | `seedance-2.5` | same |
|
|
13
|
+
| `@hypit/seedream@1#seedream-5-lite` | `seedream-5.0-lite` | Ark `POST /ark/v3/images/generations`, synchronous |
|
|
14
|
+
| `@hypit/minimax-h3@1#minimax-h3` | `minimax-h3` | MiniMax `POST /minimax/v2/video_generation`, polled at `GET /minimax/v2/query/video_generation/{id}` |
|
|
15
|
+
|
|
16
|
+
The TokenDance catalogue at `GET /gateway/v1/models` lists further models; this Provider maps only
|
|
17
|
+
the models the Distribution already describes.
|
|
18
|
+
|
|
19
|
+
Video requests write the prompt and each media input as one item of the protocol's `content`
|
|
20
|
+
array with its `role` (`first_frame`, `last_frame`, `reference_image`, `reference_video`,
|
|
21
|
+
`reference_audio`), then `resolution`, `ratio`, `duration` and, for Seedance, `generate_audio`;
|
|
22
|
+
`web-search="true"` adds `tools: [{ "type": "web_search" }]`. Seedance visual references may carry
|
|
23
|
+
`person-reference`; the Provider accepts the declaration and transmits nothing for it, since the Ark
|
|
24
|
+
protocol has no such field. Seedance 2.0 and 2.5 reject reference images and videos that contain a
|
|
25
|
+
real human face; TokenDance offers no way to register authorized portrait material, so such a request
|
|
26
|
+
fails with the service's moderation error.
|
|
27
|
+
|
|
28
|
+
Seedream requests send the Ark `size` in pixels: the authored `quality` selects the 2K, 3K or 4K
|
|
29
|
+
tier and `aspect-ratio` the entry from the Ark reference table for Seedream 5.0 lite. Output uses
|
|
30
|
+
`response_format: "url"` and `watermark: false`. `nsfw-check` has no Ark field and is not sent.
|
|
31
|
+
|
|
32
|
+
Service limits this Provider reports as unsupported before submitting:
|
|
33
|
+
|
|
34
|
+
- Seedance 2.5 frame mode (`first-frame` present) requires `aspect-ratio="adaptive"`.
|
|
35
|
+
- MiniMax H3 text-to-video requires an explicit `aspect-ratio`; frame and reference modes may omit it.
|
|
36
|
+
|
|
37
|
+
Reference media reach each protocol the way its documentation provides. Ark takes images under
|
|
38
|
+
30 MB and audio up to 15 MB inline as `data:` URLs within a 64 MB request body; the Provider checks
|
|
39
|
+
both before submitting. An Ark reference video takes a URL only; configure `publicAssetUrl` when
|
|
40
|
+
embedding the Provider, otherwise such a request fails before submission. MiniMax inputs are
|
|
41
|
+
uploaded through the gateway's `POST /minimax/v1/files/upload` with `purpose:
|
|
42
|
+
video_generation_input` (images up to 30 MB, videos up to 50 MB, audio up to 15 MB) and referenced
|
|
43
|
+
as `mm_file://{file_id}`; MiniMax keeps such files for seven days.
|
|
44
|
+
|
|
45
|
+
Runtime Profile example:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"endpoints": {
|
|
50
|
+
"tokendance.default": {
|
|
51
|
+
"use": "@hypit/provider-tokendance",
|
|
52
|
+
"pool": "tokendance.default",
|
|
53
|
+
"config": {
|
|
54
|
+
"apiKey": { "store": "platform", "key": "tokendance.api-key" },
|
|
55
|
+
"defaultConcurrency": 3,
|
|
56
|
+
"pollIntervalMs": 10000
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`baseUrl` defaults to `https://tokendance.space/gateway`. Store the API key with
|
|
64
|
+
`hypit auth login tokendance.default --runtime hypit.runtime.json`. Optional `requestTimeoutMs`,
|
|
65
|
+
`operationTimeoutMs` and `actionLimits` bound single HTTP calls, the whole remote task and action
|
|
66
|
+
concurrency. Task and HTTP failures keep TokenDance's `error.code` and message, with any signed URL
|
|
67
|
+
in the message redacted.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hypit/provider-tokendance",
|
|
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/minimax-h3": "workspace:*",
|
|
18
|
+
"@hypit/seedance": "workspace:*",
|
|
19
|
+
"@hypit/seedream": "workspace:*"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -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 { createTokenDanceProvider } from "./provider.js";
|
|
12
|
+
|
|
13
|
+
const adapter = createRuntimeEndpointAdapterFacet({
|
|
14
|
+
use: "@hypit/provider-tokendance",
|
|
15
|
+
activate(context) {
|
|
16
|
+
if (context.pool === undefined) throw new Error("TokenDance Provider Pool is required");
|
|
17
|
+
const config = runtimeConfigObject(context.config, "TokenDance");
|
|
18
|
+
runtimeConfigExact(config, [
|
|
19
|
+
"baseUrl",
|
|
20
|
+
"apiKey",
|
|
21
|
+
"defaultConcurrency",
|
|
22
|
+
"actionLimits",
|
|
23
|
+
"pollIntervalMs",
|
|
24
|
+
"requestTimeoutMs",
|
|
25
|
+
"operationTimeoutMs",
|
|
26
|
+
], "TokenDance");
|
|
27
|
+
const baseUrl = runtimeConfigString(config.baseUrl, "TokenDance 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("TokenDance baseUrl must use HTTPS or loopback");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const apiKey = runtimeConfigCredentialRef(config.apiKey, "TokenDance apiKey");
|
|
35
|
+
if (apiKey === undefined) throw new Error("TokenDance apiKey CredentialRef is required");
|
|
36
|
+
const actionLimits = runtimeConfigActionLimits(config.actionLimits);
|
|
37
|
+
const defaultConcurrency = runtimeConfigPositiveInteger(config.defaultConcurrency, "TokenDance defaultConcurrency");
|
|
38
|
+
const pollIntervalMs = runtimeConfigPositiveInteger(config.pollIntervalMs, "TokenDance pollIntervalMs");
|
|
39
|
+
const requestTimeoutMs = runtimeConfigPositiveInteger(config.requestTimeoutMs, "TokenDance requestTimeoutMs");
|
|
40
|
+
const operationTimeoutMs = runtimeConfigPositiveInteger(config.operationTimeoutMs, "TokenDance operationTimeoutMs");
|
|
41
|
+
return {
|
|
42
|
+
endpoint: createTokenDanceProvider({
|
|
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,47 @@
|
|
|
1
|
+
/** TokenDance relays each protocol's own error body; keep the code, the message and the HTTP facts. */
|
|
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 signed asset URL. Keep the reason, not its access capability.
|
|
11
|
+
export function safeTokenDanceReason(value: string): string {
|
|
12
|
+
return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class TokenDanceServiceError extends Error {
|
|
16
|
+
constructor(readonly code: string, message: string) { super(message); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class TokenDanceHttpError extends TokenDanceServiceError {
|
|
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) ?? text(error?.type) ?? text(body?.code) ?? "TOKENDANCE_HTTP_ERROR";
|
|
26
|
+
const reason = text(error?.message) ?? text(body?.message)
|
|
27
|
+
?? (body === undefined ? text(bodyText.slice(0, 2000)) : undefined);
|
|
28
|
+
const requestId = text(response.headers.get("x-request-id"));
|
|
29
|
+
const facts = [
|
|
30
|
+
`TokenDance HTTP ${status}`, code, `${request.method} ${request.path}`,
|
|
31
|
+
...(request.model === undefined ? [] : [`model=${request.model}`]),
|
|
32
|
+
...(requestId === undefined ? [] : [`request=${requestId}`]),
|
|
33
|
+
];
|
|
34
|
+
super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeTokenDanceReason(reason)}`}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A terminal task body from either protocol; `undefined` when the task did not fail. */
|
|
39
|
+
export function tokenDanceTaskFailure(task: Record<string, unknown>, id: string): TokenDanceServiceError | undefined {
|
|
40
|
+
const status = String(task.status);
|
|
41
|
+
if (!["failed", "cancelled", "canceled", "expired"].includes(status)) return undefined;
|
|
42
|
+
const error = record(task.error);
|
|
43
|
+
const code = text(error?.code) ?? "TOKENDANCE_TASK_FAILED";
|
|
44
|
+
const reason = text(error?.message) ?? text(task.error);
|
|
45
|
+
return new TokenDanceServiceError(code,
|
|
46
|
+
`TokenDance task ${id} ${status}; ${code}${reason === undefined ? "" : `: ${safeTokenDanceReason(reason)}`}`);
|
|
47
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 SEEDREAM: ModuleRef = { name: "@hypit/seedream", version: "1" };
|
|
6
|
+
const MINIMAX: ModuleRef = { name: "@hypit/minimax-h3", version: "1" };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* TokenDance model IDs and the request fields they accept. Media fields name the `role` of an Ark
|
|
10
|
+
* or MiniMax `content` item; routes.ts folds them into that array. `personReference` is accepted
|
|
11
|
+
* on visual references and not transmitted: neither protocol has a field for it.
|
|
12
|
+
*/
|
|
13
|
+
const seedance = (name: string, model: string): GenerationWireMapping => ({
|
|
14
|
+
capability: { module: SEEDANCE, name }, result: "video", routes: [{ model }],
|
|
15
|
+
fields: {
|
|
16
|
+
prompt: { as: "value", field: "text" },
|
|
17
|
+
referenceImage: { as: "urlArray", field: "reference_image", resourceFields: ["personReference"] },
|
|
18
|
+
referenceVideo: { as: "urlArray", field: "reference_video", resourceFields: ["personReference"] },
|
|
19
|
+
referenceAudio: { as: "urlArray", field: "reference_audio" },
|
|
20
|
+
firstFrame: { as: "url", field: "first_frame", resourceFields: ["personReference"] },
|
|
21
|
+
lastFrame: { as: "url", field: "last_frame", resourceFields: ["personReference"] },
|
|
22
|
+
resolution: { as: "value", field: "resolution" },
|
|
23
|
+
aspectRatio: { as: "value", field: "ratio" },
|
|
24
|
+
duration: { as: "value", field: "duration" },
|
|
25
|
+
generateAudio: { as: "value", field: "generate_audio" },
|
|
26
|
+
webSearch: { as: "value", field: "web_search" },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const tokenDanceMappings: readonly GenerationWireMapping[] = [
|
|
31
|
+
seedance("seedance-2", "seedance-2.0"),
|
|
32
|
+
seedance("seedance-2-fast", "seedance-2.0-fast"),
|
|
33
|
+
seedance("seedance-2-mini", "seedance-2.0-mini"),
|
|
34
|
+
seedance("seedance-2.5", "seedance-2.5"),
|
|
35
|
+
{
|
|
36
|
+
capability: { module: SEEDREAM, name: "seedream-5-lite" }, result: "image", routes: [{ model: "seedream-5.0-lite" }],
|
|
37
|
+
fields: {
|
|
38
|
+
prompt: { as: "value", field: "prompt" },
|
|
39
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
40
|
+
quality: { as: "value", field: "quality" },
|
|
41
|
+
outputFormat: { as: "value", field: "output_format" },
|
|
42
|
+
nsfwCheck: { as: "value", field: "nsfw_check" },
|
|
43
|
+
images: { as: "urlArray", field: "image" },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
capability: { module: MINIMAX, name: "minimax-h3" }, result: "video", routes: [{ model: "minimax-h3" }],
|
|
48
|
+
fields: {
|
|
49
|
+
prompt: { as: "value", field: "text" },
|
|
50
|
+
duration: { as: "value", field: "duration" },
|
|
51
|
+
resolution: { as: "value", field: "resolution", whenAbsent: "2K" },
|
|
52
|
+
aspectRatio: { as: "value", field: "ratio" },
|
|
53
|
+
referenceImage: { as: "urlArray", field: "reference_image" },
|
|
54
|
+
referenceVideo: { as: "urlArray", field: "reference_video" },
|
|
55
|
+
referenceAudio: { as: "urlArray", field: "reference_audio" },
|
|
56
|
+
firstFrame: { as: "url", field: "first_frame" },
|
|
57
|
+
lastFrame: { as: "url", field: "last_frame" },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
];
|