@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,245 @@
|
|
|
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 { monidRouteForCapability, monidRoutes } from "./routes.js";
|
|
10
|
+
import { MonidHttpError, MonidServiceError, monidRunFailure, monidTerminalStatuses } from "./errors.js";
|
|
11
|
+
|
|
12
|
+
export const monidProviderModuleRef = { name: "@hypit/provider-monid", version: "1" } as const;
|
|
13
|
+
|
|
14
|
+
export type CreateMonidProviderOptions = {
|
|
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 workspace file system 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.monid-operation@1";
|
|
31
|
+
readonly runId: string;
|
|
32
|
+
readonly route: string;
|
|
33
|
+
readonly startedAt: number;
|
|
34
|
+
readonly url?: 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, "Monid 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, "Monid apiKey credential is unavailable; store a Monid 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 MonidServiceError ? error.code : "MONID_ERROR", message: failureMessage(error) } };
|
|
59
|
+
}
|
|
60
|
+
function runId(run: Record<string, unknown>): string {
|
|
61
|
+
assert(typeof run.runId === "string" && run.runId.length > 0, "Monid response has no runId");
|
|
62
|
+
return run.runId;
|
|
63
|
+
}
|
|
64
|
+
function httpsUrl(value: unknown, subject: string): string {
|
|
65
|
+
assert(typeof value === "string" && /^https?:\/\//u.test(value), `${subject} has no URL`);
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The relayed result. The ModelArk endpoints return the task's `video_url`; MiniMax-H3 returns the
|
|
70
|
+
* video at `content.url`. The link expires, so collection downloads it rather than storing it.
|
|
71
|
+
*/
|
|
72
|
+
function outputVideoUrl(run: Record<string, unknown>): string {
|
|
73
|
+
const output = object(run.output, "Monid run output");
|
|
74
|
+
const content = output.content === undefined ? undefined : object(output.content, "Monid run output content");
|
|
75
|
+
return httpsUrl(output.video_url ?? content?.video_url ?? content?.url, "Monid run output");
|
|
76
|
+
}
|
|
77
|
+
const extensions: Readonly<Record<string, string>> = {
|
|
78
|
+
"image/jpeg": "jpg", "image/png": "png", "image/webp": "webp", "video/mp4": "mp4", "video/quicktime": "mov",
|
|
79
|
+
"audio/wav": "wav", "audio/x-wav": "wav", "audio/mpeg": "mp3",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
class MonidClient {
|
|
83
|
+
constructor(readonly baseUrl: string, readonly timeout: number, readonly pollIntervalMs: number, readonly fetcher: typeof globalThis.fetch) {}
|
|
84
|
+
async json(path: string, key: string, init: RequestInit = {}): Promise<{ readonly status: number; readonly body: Record<string, unknown> }> {
|
|
85
|
+
const deadline = requestDeadline(this.timeout);
|
|
86
|
+
try {
|
|
87
|
+
const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
|
|
88
|
+
...init, signal: deadline.signal, headers: { authorization: `Bearer ${key}`, ...(init.headers ?? {}) },
|
|
89
|
+
}));
|
|
90
|
+
const text = await deadline.wait(response.text());
|
|
91
|
+
let body: unknown;
|
|
92
|
+
try { body = text.length === 0 ? {} : JSON.parse(text); } catch { body = undefined; }
|
|
93
|
+
// A synchronous run mirrors the provider's HTTP status while still returning the run itself.
|
|
94
|
+
const run = body !== null && typeof body === "object" && !Array.isArray(body) && typeof (body as Record<string, unknown>).runId === "string";
|
|
95
|
+
if (!response.ok && !run) throw new MonidHttpError(response.status, response, text, { method: init.method ?? "GET", path });
|
|
96
|
+
assert(body !== undefined, `Monid returned invalid JSON (${response.status})`);
|
|
97
|
+
return { status: response.status, body: object(body, "Monid response") };
|
|
98
|
+
} finally { deadline.finish(); }
|
|
99
|
+
}
|
|
100
|
+
async run(request: { readonly provider: string; readonly endpoint: string; readonly input: Record<string, unknown> }, key: string) {
|
|
101
|
+
return await this.json("/v1/run", key, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(request) });
|
|
102
|
+
}
|
|
103
|
+
async getRun(id: string, key: string): Promise<Record<string, unknown>> {
|
|
104
|
+
return (await this.json(`/v1/runs/${encodeURIComponent(id)}`, key)).body;
|
|
105
|
+
}
|
|
106
|
+
/** Run a short workspace operation to completion and return its provider output. */
|
|
107
|
+
async completeRun(request: { readonly provider: string; readonly endpoint: string; readonly input: Record<string, unknown> }, key: string): Promise<Record<string, unknown>> {
|
|
108
|
+
let run = (await this.run(request, key)).body;
|
|
109
|
+
const id = runId(run);
|
|
110
|
+
const deadline = requestDeadline(this.timeout);
|
|
111
|
+
try {
|
|
112
|
+
while (!monidTerminalStatuses.includes(String(run.status) as typeof monidTerminalStatuses[number])) {
|
|
113
|
+
await deadline.wait(new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs)));
|
|
114
|
+
run = await this.getRun(id, key);
|
|
115
|
+
}
|
|
116
|
+
} finally { deadline.finish(); }
|
|
117
|
+
const rejected = monidRunFailure(run, id);
|
|
118
|
+
if (rejected !== undefined) throw rejected;
|
|
119
|
+
return object(run.output, `Monid ${request.provider}${request.endpoint} output`);
|
|
120
|
+
}
|
|
121
|
+
/** Upload through the workspace file system (`sfs`) and mint a URL third parties can fetch. */
|
|
122
|
+
async publish(artifact: BlobRef, resources: ResourceStore, key: string): Promise<string> {
|
|
123
|
+
const bytes = await resources.get(artifact.resource);
|
|
124
|
+
assert(bytes !== undefined && bytes.byteLength === artifact.size, `Reference Resource ${artifact.resource} is unavailable or has changed`);
|
|
125
|
+
const path = `hypit/${artifact.resource}.${extensions[artifact.mediaType] ?? artifact.mediaType.split("/")[1] ?? "bin"}`;
|
|
126
|
+
const put = await this.completeRun({ provider: "sfs", endpoint: "/put", input: { path, sizeBytes: bytes.byteLength } }, key);
|
|
127
|
+
const uploadUrl = httpsUrl(put.uploadUrl, "Monid sfs /put output");
|
|
128
|
+
const deadline = requestDeadline(this.timeout);
|
|
129
|
+
try {
|
|
130
|
+
const response = await deadline.wait(this.fetcher(uploadUrl, { method: "PUT", body: new Blob([new Uint8Array(bytes)]), signal: deadline.signal }));
|
|
131
|
+
assert(response.ok, `Monid file upload returned HTTP ${response.status}`);
|
|
132
|
+
} finally { deadline.finish(); }
|
|
133
|
+
const cat = await this.completeRun({ provider: "sfs", endpoint: "/cat", input: { path, ttl: "1d" } }, key);
|
|
134
|
+
return httpsUrl(cat.url, "Monid sfs /cat output");
|
|
135
|
+
}
|
|
136
|
+
async download(url: string): Promise<{ readonly bytes: Uint8Array; readonly mediaType: string }> {
|
|
137
|
+
const deadline = requestDeadline(this.timeout);
|
|
138
|
+
try {
|
|
139
|
+
const response = await deadline.wait(this.fetcher(url, { signal: deadline.signal }));
|
|
140
|
+
if (!response.ok) throw new Error(`Monid asset returned HTTP ${response.status}`);
|
|
141
|
+
return { bytes: new Uint8Array(await deadline.wait(response.arrayBuffer())), mediaType: response.headers.get("content-type")?.split(";", 1)[0] ?? "application/octet-stream" };
|
|
142
|
+
} finally { deadline.finish(); }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolverFor(client: MonidClient, context: EndpointInvocationContext, publicAssetUrl: CreateMonidProviderOptions["publicAssetUrl"]): GenerationArtifactUrlResolver {
|
|
147
|
+
const resolved = new Map<string, Promise<string>>();
|
|
148
|
+
return (artifact, fields) => {
|
|
149
|
+
const existing = resolved.get(artifact.resource);
|
|
150
|
+
if (existing !== undefined) return existing;
|
|
151
|
+
const promise = publicAssetUrl === undefined
|
|
152
|
+
? client.publish(artifact, context.resources, apiKey(context.credentials))
|
|
153
|
+
: publicAssetUrl(artifact, context.resources, fields);
|
|
154
|
+
resolved.set(artifact.resource, promise);
|
|
155
|
+
return promise;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function endpoint(client: MonidClient, pollIntervalMs: number, maxOperationMs: number, publicAssetUrl: CreateMonidProviderOptions["publicAssetUrl"]): AsyncEndpoint {
|
|
160
|
+
return {
|
|
161
|
+
async start(context) {
|
|
162
|
+
try {
|
|
163
|
+
const route = monidRouteForCapability(context.need.capability);
|
|
164
|
+
assert(route !== undefined, "Monid does not implement this exact capability");
|
|
165
|
+
const request = route.prepare(context.need.constraints);
|
|
166
|
+
await context.reportProgress?.({ phase: `Preparing Monid request: ${request.service} ${request.endpoint}` });
|
|
167
|
+
let input: Record<string, unknown>;
|
|
168
|
+
try {
|
|
169
|
+
input = await request.compile(resolverFor(client, context, publicAssetUrl));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw new MonidServiceError(error instanceof MonidServiceError ? error.code : "MONID_ERROR",
|
|
172
|
+
`Monid request preparation failed; endpoint=${request.endpoint}; generation not submitted: ${failureMessage(error)}`);
|
|
173
|
+
}
|
|
174
|
+
await context.reportProgress?.({ phase: `Submitting Monid request: ${request.service} ${request.endpoint}` });
|
|
175
|
+
const { body: run } = await client.run({ provider: request.service, endpoint: request.endpoint, input }, apiKey(context.credentials));
|
|
176
|
+
const handle: Handle = { contract: "hypit.monid-operation@1", runId: runId(run), route: route.key, startedAt: Date.now() };
|
|
177
|
+
const receipt = { id: handle.runId };
|
|
178
|
+
const ended = monidTerminalStatuses.includes(String(run.status) as typeof monidTerminalStatuses[number]);
|
|
179
|
+
await context.checkpoint?.({ handle: canonicalize(handle), receipt, ...(ended ? { remoteEnded: true as const } : {}) });
|
|
180
|
+
if (!ended) return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: String(run.status) }), receipt };
|
|
181
|
+
const rejected = monidRunFailure(run, handle.runId);
|
|
182
|
+
if (rejected !== undefined) return { ...failure(rejected), receipt };
|
|
183
|
+
return { status: "ready", handle: canonicalize({ ...handle, url: outputVideoUrl(run) }), receipt };
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return failure(error);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
async poll(context) {
|
|
189
|
+
try {
|
|
190
|
+
const handle = object(context.handle, "Monid handle") as unknown as Handle;
|
|
191
|
+
const route = monidRouteForCapability(context.need.capability);
|
|
192
|
+
assert(route !== undefined && handle.contract === "hypit.monid-operation@1" && handle.route === route.key, "Monid handle is invalid");
|
|
193
|
+
const receipt = { id: handle.runId };
|
|
194
|
+
if (Date.now() - handle.startedAt > maxOperationMs) {
|
|
195
|
+
return { status: "failed", receipt, failure: { code: "MONID_OPERATION_TIMEOUT", message: `Monid run ${handle.runId} exceeded this Provider's operationTimeoutMs (${maxOperationMs}); remote outcome is unknown` } };
|
|
196
|
+
}
|
|
197
|
+
const run = await client.getRun(handle.runId, apiKey(context.credentials));
|
|
198
|
+
const status = String(run.status);
|
|
199
|
+
if (!monidTerminalStatuses.includes(status as typeof monidTerminalStatuses[number])) {
|
|
200
|
+
return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
|
|
201
|
+
}
|
|
202
|
+
const rejected = monidRunFailure(run, handle.runId);
|
|
203
|
+
if (rejected !== undefined) return { ...failure(rejected), receipt };
|
|
204
|
+
return { status: "ready", handle: canonicalize({ ...handle, url: outputVideoUrl(run) }), receipt };
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return failure(error);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
async collect(context) {
|
|
210
|
+
try {
|
|
211
|
+
const handle = object(context.handle, "Monid handle") as unknown as Handle;
|
|
212
|
+
const route = monidRouteForCapability(context.need.capability);
|
|
213
|
+
assert(route !== undefined && handle.route === route.key, "Monid collection route differs");
|
|
214
|
+
await context.reportProgress?.({ phase: "Receiving generated video" });
|
|
215
|
+
const downloaded = await client.download(httpsUrl(handle.url, "Monid handle"));
|
|
216
|
+
const blob = await context.resources.put(downloaded.bytes, downloaded.mediaType);
|
|
217
|
+
return { status: "completed", result: { value: route.packageResult([blob]) }, receipt: { id: handle.runId } };
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return failure(error);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function createMonidProvider(options: CreateMonidProviderOptions = {}) {
|
|
226
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 300_000;
|
|
227
|
+
const operationTimeoutMs = options.operationTimeoutMs ?? 30 * 60_000;
|
|
228
|
+
const pollIntervalMs = options.pollIntervalMs ?? 10_000;
|
|
229
|
+
for (const [name, value] of Object.entries({ requestTimeoutMs, operationTimeoutMs, pollIntervalMs })) {
|
|
230
|
+
assert(Number.isSafeInteger(value) && value > 0, `Monid ${name} must be a positive integer`);
|
|
231
|
+
}
|
|
232
|
+
const client = new MonidClient(apiBaseUrl(options.baseUrl ?? "https://api.monid.ai"), requestTimeoutMs, Math.min(pollIntervalMs, 5_000), options.fetch ?? globalThis.fetch);
|
|
233
|
+
const asyncEndpoint = endpoint(client, pollIntervalMs, operationTimeoutMs, options.publicAssetUrl);
|
|
234
|
+
return defineEndpointPackage({
|
|
235
|
+
module: monidProviderModuleRef, facet: "gateway", instance: options.instance ?? "monid.default", pool: options.pool ?? options.instance ?? "monid.default",
|
|
236
|
+
pricing: { kind: "page", url: "https://monid.ai/tools" },
|
|
237
|
+
credentials: { apiKey: options.apiKey ?? credentialRef("os", "monid.api-key") },
|
|
238
|
+
credentialInputs: { apiKey: { label: "Monid API key" } },
|
|
239
|
+
defaultConcurrency: options.defaultConcurrency ?? 4,
|
|
240
|
+
...(options.actionLimits === undefined ? {} : { actionLimits: options.actionLimits }),
|
|
241
|
+
capabilities: monidRoutes.map((route) => ({
|
|
242
|
+
capability: route.capability, returns: route.returns, lifecycle: "asynchronous" as const, endpoint: asyncEndpoint, capacity: route.capability.name, supports: route.supports,
|
|
243
|
+
})),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compileWireRequest,
|
|
3
|
+
selectWireModelForRequest,
|
|
4
|
+
generationTypes,
|
|
5
|
+
sealGeneratedVideoSet,
|
|
6
|
+
} from "@hypit/generation";
|
|
7
|
+
import type { GenerationArtifactUrlResolver, GenerationRequest } from "@hypit/generation";
|
|
8
|
+
import { canonicalize } from "@hypit/protocol";
|
|
9
|
+
import type { BlobRef, CapabilityRef, CanonicalValue, StoredValue, TypeRef } from "@hypit/protocol";
|
|
10
|
+
import type { EndpointRequest, EndpointSupport } from "@hypit/endpoint-kit";
|
|
11
|
+
import { monidMappings } from "./mapping.js";
|
|
12
|
+
import type { MonidMapping } from "./mapping.js";
|
|
13
|
+
|
|
14
|
+
export type MonidPreparedRequest = {
|
|
15
|
+
/** Monid provider relaying the endpoint. */
|
|
16
|
+
readonly service: string;
|
|
17
|
+
/** Monid endpoint path under that provider. */
|
|
18
|
+
readonly endpoint: string;
|
|
19
|
+
readonly compile: (resolve: GenerationArtifactUrlResolver) => Promise<Record<string, unknown>>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type MonidRoute = MonidMapping & {
|
|
23
|
+
readonly key: string;
|
|
24
|
+
readonly returns: TypeRef;
|
|
25
|
+
readonly supports: (request: EndpointRequest) => EndpointSupport;
|
|
26
|
+
readonly prepare: (constraints: CanonicalValue) => MonidPreparedRequest;
|
|
27
|
+
readonly packageResult: (artifacts: readonly BlobRef[]) => StoredValue;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function scalar(request: GenerationRequest, port: string): string | number | boolean | undefined {
|
|
31
|
+
const value = request.ports[port]?.[0];
|
|
32
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
function present(request: GenerationRequest, port: string): boolean {
|
|
35
|
+
return (request.ports[port]?.length ?? 0) > 0;
|
|
36
|
+
}
|
|
37
|
+
function strings(value: unknown): readonly string[] {
|
|
38
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.length > 0) : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function referenceToVideo(request: GenerationRequest): boolean {
|
|
42
|
+
return present(request, "referenceImage") || present(request, "referenceVideo") || present(request, "referenceAudio");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rejection(mapping: MonidMapping, request: GenerationRequest): string | undefined {
|
|
46
|
+
if (mapping.capability.name === "minimax-h3") {
|
|
47
|
+
// MiniMax-H3 resolves the framing from the uploaded image in frame mode and defaults
|
|
48
|
+
// reference-to-video to adaptive. Text-to-video carries no such source, and the endpoint
|
|
49
|
+
// takes no adaptive ratio there, so the aspect ratio has to be stated.
|
|
50
|
+
if (!present(request, "firstFrame") && !present(request, "lastFrame") && !referenceToVideo(request)
|
|
51
|
+
&& scalar(request, "aspectRatio") === undefined) {
|
|
52
|
+
return "MiniMax H3 text-to-video on Monid requires an aspect ratio";
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (scalar(request, "webSearch") === true) {
|
|
57
|
+
return "Monid's Seedance endpoints document no web search field";
|
|
58
|
+
}
|
|
59
|
+
if (mapping.capability.name === "seedance-2.5" && present(request, "firstFrame") && scalar(request, "aspectRatio") !== "adaptive") {
|
|
60
|
+
return `Seedance 2.5 frame mode takes only aspect-ratio adaptive, not ${String(scalar(request, "aspectRatio"))}`;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function contentItem(type: "image_url" | "video_url" | "audio_url", url: string, role: string) {
|
|
66
|
+
return { type, [type]: { url }, role };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The ModelArk request body Monid relays: one `content` array of typed, role-tagged items plus the output settings. */
|
|
70
|
+
function arkInput(input: Record<string, unknown>): Record<string, unknown> {
|
|
71
|
+
const first = input.first_frame;
|
|
72
|
+
const last = input.last_frame;
|
|
73
|
+
return {
|
|
74
|
+
...(typeof input.model === "string" ? { model: input.model } : {}),
|
|
75
|
+
content: [
|
|
76
|
+
{ type: "text", text: input.text },
|
|
77
|
+
...(typeof first === "string" ? [contentItem("image_url", first, "first_frame")] : []),
|
|
78
|
+
...(typeof last === "string" ? [contentItem("image_url", last, "last_frame")] : []),
|
|
79
|
+
...strings(input.reference_image).map((url) => contentItem("image_url", url, "reference_image")),
|
|
80
|
+
...strings(input.reference_video).map((url) => contentItem("video_url", url, "reference_video")),
|
|
81
|
+
...strings(input.reference_audio).map((url) => contentItem("audio_url", url, "reference_audio")),
|
|
82
|
+
],
|
|
83
|
+
resolution: input.resolution,
|
|
84
|
+
ratio: input.ratio,
|
|
85
|
+
duration: input.duration,
|
|
86
|
+
generate_audio: input.generate_audio,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* MiniMax-H3 takes a required ratio: frame mode and reference-to-video both resolve to adaptive,
|
|
92
|
+
* and rejection() has already required a stated ratio for text-to-video.
|
|
93
|
+
*/
|
|
94
|
+
function minimaxH3Input(input: Record<string, unknown>): Record<string, unknown> {
|
|
95
|
+
const body = arkInput(input);
|
|
96
|
+
return { ...body, ratio: body.ratio ?? "adaptive" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function capabilityKey(capability: CapabilityRef): string {
|
|
100
|
+
return `${capability.module.name}@${capability.module.version}#${capability.name}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const monidRoutes: readonly MonidRoute[] = monidMappings.map((mapping) => ({
|
|
104
|
+
...mapping,
|
|
105
|
+
key: capabilityKey(mapping.capability),
|
|
106
|
+
returns: generationTypes.videoSet,
|
|
107
|
+
supports: (request) => {
|
|
108
|
+
const reason = rejection(mapping, request.constraints as unknown as GenerationRequest);
|
|
109
|
+
return reason === undefined ? { status: "supported" } : { status: "unsupported", reason };
|
|
110
|
+
},
|
|
111
|
+
prepare: (constraints) => {
|
|
112
|
+
const request = constraints as unknown as GenerationRequest;
|
|
113
|
+
const reason = rejection(mapping, request);
|
|
114
|
+
if (reason !== undefined) throw new Error(reason);
|
|
115
|
+
const body = mapping.capability.name === "minimax-h3" ? minimaxH3Input : arkInput;
|
|
116
|
+
return {
|
|
117
|
+
service: mapping.service,
|
|
118
|
+
endpoint: selectWireModelForRequest(mapping, request),
|
|
119
|
+
compile: async (resolve) => body((await compileWireRequest(mapping, request, resolve)).input as Record<string, unknown>),
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
packageResult: (artifacts) => ({
|
|
123
|
+
kind: "inline",
|
|
124
|
+
value: canonicalize(sealGeneratedVideoSet({ videos: artifacts })),
|
|
125
|
+
}),
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
const byCapability = new Map(monidRoutes.map((route) => [route.key, route]));
|
|
129
|
+
|
|
130
|
+
export function monidRouteForCapability(capability: CapabilityRef): MonidRoute | undefined {
|
|
131
|
+
return byCapability.get(capabilityKey(capability));
|
|
132
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# `@hypit/provider-pollo`
|
|
2
|
+
|
|
3
|
+
Hypit Runtime Provider for a [Pollo AI](https://docs.pollo.ai) API platform account. Each capability
|
|
4
|
+
posts `{ "input": … }` to the model's generation path with the `x-api-key` header, polls
|
|
5
|
+
`GET /v1/generation/{taskId}/status` until every generation reaches `succeed`, downloads each
|
|
6
|
+
generation's `url` and stores the files in the current Build.
|
|
7
|
+
|
|
8
|
+
| Capability | Pollo generation path |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| `@hypit/minimax-h3@1#minimax-h3` | `/v1/generation/minimax/minimax-h3/video` |
|
|
11
|
+
| `@hypit/grok-imagine@1#grok-imagine-video-1.5-preview` | `/v1/generation/xai/grok-imagine-video-1-5/video` |
|
|
12
|
+
| `@hypit/gpt-image@1#gpt-image-2` | `/v1/generation/openai/gpt-image-2/image` |
|
|
13
|
+
| `@hypit/nano-banana@1#nano-banana-2` | `/v1/generation/google/nano-banana-2/image` |
|
|
14
|
+
| `@hypit/nano-banana@1#nano-banana-pro` | `/v1/generation/google/nano-banana-pro/image` |
|
|
15
|
+
|
|
16
|
+
Pollo documents further models; this Provider maps only the models the Distribution already
|
|
17
|
+
describes. Seedance and Seedream are not offered by Pollo.
|
|
18
|
+
|
|
19
|
+
MiniMax H3 requests send `prompt`, `duration`, `resolution` and `aspectRatio`; a first frame is
|
|
20
|
+
`image`, a last frame `imageTail`, and reference images, videos and audio become typed entries of
|
|
21
|
+
`refs`. Pollo renders 480p when `resolution` is omitted.
|
|
22
|
+
|
|
23
|
+
Service limits this Provider reports as unsupported before submitting:
|
|
24
|
+
|
|
25
|
+
- Grok Imagine 1.5 animates exactly one image and has no aspect-ratio field; author
|
|
26
|
+
`aspect-ratio="auto"`.
|
|
27
|
+
- GPT Image 2 renders `1:1`, `3:2`, `2:3`, `16:9`, `9:16`, `4:3`, `3:4`, `21:9` and `auto`;
|
|
28
|
+
other ratios are unsupported. Pollo's `quality` field is left at its default.
|
|
29
|
+
- Nano Banana takes an explicit aspect ratio, not `auto`. `output-format` has no Pollo field and is
|
|
30
|
+
not sent.
|
|
31
|
+
|
|
32
|
+
Pollo accepts reference media only by HTTP(S) URL. Configure `publicAssetUrl` when embedding the
|
|
33
|
+
Provider; without it, a request with reference media fails before submission, while text-only
|
|
34
|
+
requests proceed.
|
|
35
|
+
|
|
36
|
+
Runtime Profile example:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"endpoints": {
|
|
41
|
+
"pollo.default": {
|
|
42
|
+
"use": "@hypit/provider-pollo",
|
|
43
|
+
"pool": "pollo.default",
|
|
44
|
+
"config": {
|
|
45
|
+
"apiKey": { "store": "platform", "key": "pollo.api-key" },
|
|
46
|
+
"defaultConcurrency": 3,
|
|
47
|
+
"pollIntervalMs": 10000
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`baseUrl` defaults to `https://pollo.ai/api/platform`. Store the API key with
|
|
55
|
+
`hypit auth login pollo.default --runtime hypit.runtime.json`. Optional `requestTimeoutMs`,
|
|
56
|
+
`operationTimeoutMs` and `actionLimits` bound single HTTP calls, the whole remote task and action
|
|
57
|
+
concurrency. HTTP failures keep Pollo's `errorCode`, message and `requestId`; a failed generation
|
|
58
|
+
keeps its `failMsg`, with any signed URL in the message redacted. Pollo stores generated files for
|
|
59
|
+
14 days; the Provider downloads them when the task completes.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hypit/provider-pollo",
|
|
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
|
+
}
|
|
22
|
+
}
|
|
@@ -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 { createPolloProvider } from "./provider.js";
|
|
12
|
+
|
|
13
|
+
const adapter = createRuntimeEndpointAdapterFacet({
|
|
14
|
+
use: "@hypit/provider-pollo",
|
|
15
|
+
activate(context) {
|
|
16
|
+
if (context.pool === undefined) throw new Error("Pollo Provider Pool is required");
|
|
17
|
+
const config = runtimeConfigObject(context.config, "Pollo");
|
|
18
|
+
runtimeConfigExact(config, [
|
|
19
|
+
"baseUrl",
|
|
20
|
+
"apiKey",
|
|
21
|
+
"defaultConcurrency",
|
|
22
|
+
"actionLimits",
|
|
23
|
+
"pollIntervalMs",
|
|
24
|
+
"requestTimeoutMs",
|
|
25
|
+
"operationTimeoutMs",
|
|
26
|
+
], "Pollo");
|
|
27
|
+
const baseUrl = runtimeConfigString(config.baseUrl, "Pollo 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("Pollo baseUrl must use HTTPS or loopback");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const apiKey = runtimeConfigCredentialRef(config.apiKey, "Pollo apiKey");
|
|
35
|
+
if (apiKey === undefined) throw new Error("Pollo apiKey CredentialRef is required");
|
|
36
|
+
const actionLimits = runtimeConfigActionLimits(config.actionLimits);
|
|
37
|
+
const defaultConcurrency = runtimeConfigPositiveInteger(config.defaultConcurrency, "Pollo defaultConcurrency");
|
|
38
|
+
const pollIntervalMs = runtimeConfigPositiveInteger(config.pollIntervalMs, "Pollo pollIntervalMs");
|
|
39
|
+
const requestTimeoutMs = runtimeConfigPositiveInteger(config.requestTimeoutMs, "Pollo requestTimeoutMs");
|
|
40
|
+
const operationTimeoutMs = runtimeConfigPositiveInteger(config.operationTimeoutMs, "Pollo operationTimeoutMs");
|
|
41
|
+
return {
|
|
42
|
+
endpoint: createPolloProvider({
|
|
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,41 @@
|
|
|
1
|
+
/** Pollo's `{ errorCode, message, code, requestId }` envelope and generation `failMsg`, kept at the service boundary. */
|
|
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 safePolloReason(value: string): string {
|
|
12
|
+
return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class PolloServiceError extends Error {
|
|
16
|
+
constructor(readonly code: string, message: string) { super(message); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class PolloHttpError extends PolloServiceError {
|
|
20
|
+
constructor(readonly status: number, bodyText: string, request: { readonly method: string; readonly path: string }) {
|
|
21
|
+
let body: Record<string, unknown> | undefined;
|
|
22
|
+
try { body = record(JSON.parse(bodyText)); } catch { /* Non-JSON gateway failures still have HTTP evidence. */ }
|
|
23
|
+
const code = text(body?.errorCode) ?? "POLLO_HTTP_ERROR";
|
|
24
|
+
const reason = text(body?.message) ?? (body === undefined ? text(bodyText.slice(0, 2000)) : undefined);
|
|
25
|
+
const requestId = text(body?.requestId);
|
|
26
|
+
const facts = [
|
|
27
|
+
`Pollo HTTP ${status}`, code, `${request.method} ${request.path}`,
|
|
28
|
+
...(requestId === undefined ? [] : [`request=${requestId}`]),
|
|
29
|
+
];
|
|
30
|
+
super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safePolloReason(reason)}`}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The first failed generation of a task; `undefined` when none failed. */
|
|
35
|
+
export function polloTaskFailure(generations: readonly Record<string, unknown>[], id: string): PolloServiceError | undefined {
|
|
36
|
+
const failed = generations.find((generation) => generation.status === "failed");
|
|
37
|
+
if (failed === undefined) return undefined;
|
|
38
|
+
const reason = text(failed.failMsg);
|
|
39
|
+
return new PolloServiceError("POLLO_TASK_FAILED",
|
|
40
|
+
`Pollo task ${id} failed${reason === undefined ? "" : `: ${safePolloReason(reason)}`}`);
|
|
41
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ModuleRef } from "@hypit/protocol";
|
|
2
|
+
import type { GenerationWireMapping } from "@hypit/generation";
|
|
3
|
+
|
|
4
|
+
const MINIMAX: ModuleRef = { name: "@hypit/minimax-h3", version: "1" };
|
|
5
|
+
const GROK: ModuleRef = { name: "@hypit/grok-imagine", version: "1" };
|
|
6
|
+
const GPT_IMAGE: ModuleRef = { name: "@hypit/gpt-image", version: "1" };
|
|
7
|
+
const NANO_BANANA: ModuleRef = { name: "@hypit/nano-banana", version: "1" };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pollo generation paths and the `input` fields each documents. The route model is the request
|
|
11
|
+
* path under the Pollo platform base URL. Reference fields prefixed `refs_` are folded into MiniMax's
|
|
12
|
+
* typed `refs` array by routes.ts.
|
|
13
|
+
*/
|
|
14
|
+
export const polloMappings: readonly GenerationWireMapping[] = [
|
|
15
|
+
{
|
|
16
|
+
capability: { module: MINIMAX, name: "minimax-h3" }, result: "video", routes: [{ model: "/v1/generation/minimax/minimax-h3/video" }],
|
|
17
|
+
fields: {
|
|
18
|
+
prompt: { as: "value", field: "prompt" },
|
|
19
|
+
duration: { as: "value", field: "duration" },
|
|
20
|
+
resolution: { as: "value", field: "resolution" },
|
|
21
|
+
aspectRatio: { as: "value", field: "aspectRatio" },
|
|
22
|
+
firstFrame: { as: "url", field: "image" },
|
|
23
|
+
lastFrame: { as: "url", field: "imageTail" },
|
|
24
|
+
referenceImage: { as: "urlArray", field: "refs_image" },
|
|
25
|
+
referenceVideo: { as: "urlArray", field: "refs_video" },
|
|
26
|
+
referenceAudio: { as: "urlArray", field: "refs_audio" },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
capability: { module: GROK, name: "grok-imagine-video-1.5-preview" }, result: "video",
|
|
31
|
+
routes: [{ model: "/v1/generation/xai/grok-imagine-video-1-5/video" }],
|
|
32
|
+
fields: {
|
|
33
|
+
prompt: { as: "value", field: "prompt" },
|
|
34
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
35
|
+
resolution: { as: "value", field: "resolution" },
|
|
36
|
+
duration: { as: "value", field: "duration" },
|
|
37
|
+
images: { as: "urlArray", field: "images" },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
capability: { module: GPT_IMAGE, name: "gpt-image-2" }, result: "image", routes: [{ model: "/v1/generation/openai/gpt-image-2/image" }],
|
|
42
|
+
fields: {
|
|
43
|
+
prompt: { as: "value", field: "prompt" },
|
|
44
|
+
aspectRatio: { as: "value", field: "aspectRatio" },
|
|
45
|
+
resolution: { as: "value", field: "resolution" },
|
|
46
|
+
background: { as: "value", field: "background" },
|
|
47
|
+
images: { as: "urlArray", field: "images" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
...(["nano-banana-2", "nano-banana-pro"] as const).map((name): GenerationWireMapping => ({
|
|
51
|
+
capability: { module: NANO_BANANA, name }, result: "image", routes: [{ model: `/v1/generation/google/${name}/image` }],
|
|
52
|
+
fields: {
|
|
53
|
+
prompt: { as: "value", field: "prompt" },
|
|
54
|
+
images: { as: "urlArray", field: "images" },
|
|
55
|
+
aspectRatio: { as: "value", field: "aspectRatio" },
|
|
56
|
+
resolution: { as: "value", field: "resolution" },
|
|
57
|
+
outputFormat: { as: "value", field: "output_format" },
|
|
58
|
+
},
|
|
59
|
+
})),
|
|
60
|
+
];
|