@hypit/hypit 0.2.2 → 0.2.4

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 (68) hide show
  1. package/README.md +18 -6
  2. package/bin/hypit.mjs +12 -9
  3. package/dist/public/generation.d.ts +4 -5
  4. package/examples/provider-package/README.md +92 -16
  5. package/examples/provider-package/hypit.runtime.json +8 -1
  6. package/examples/provider-package/packages/provider-videos/package.json +11 -0
  7. package/examples/provider-package/packages/provider-videos/src/activation.ts +24 -0
  8. package/examples/provider-package/packages/provider-videos/src/provider.ts +181 -0
  9. package/examples/provider-package/packages/provider-videos/tsconfig.json +9 -0
  10. package/package.json +5 -1
  11. package/packages/build-result/package.json +1 -1
  12. package/packages/build-result/src/store.ts +1 -1
  13. package/packages/credential-store-file/README.md +9 -4
  14. package/packages/credential-store-file/package.json +2 -1
  15. package/packages/credential-store-file/src/activation.ts +2 -2
  16. package/packages/credential-store-file/src/index.ts +0 -1
  17. package/packages/credential-store-file/src/store.ts +13 -40
  18. package/packages/credential-store-os/runtime/windows-credential.ps1 +1 -2
  19. package/packages/credential-store-os/src/store.ts +2 -68
  20. package/packages/credential-store-os/src/windows.ts +47 -0
  21. package/packages/credential-store-platform/README.md +2 -1
  22. package/packages/credential-store-platform/src/activation.ts +2 -2
  23. package/packages/file-io-node/README.md +20 -0
  24. package/packages/file-io-node/package.json +13 -0
  25. package/packages/file-io-node/src/index.ts +1 -0
  26. package/packages/{build-result → file-io-node}/src/replace-file-windows.ts +3 -3
  27. package/packages/generation/README.md +6 -0
  28. package/packages/generation/src/mapping.ts +30 -10
  29. package/packages/media-execution/src/execute.ts +3 -2
  30. package/packages/media-execution/src/toolchain.ts +10 -36
  31. package/packages/provider-hiapi/README.md +75 -0
  32. package/packages/provider-hiapi/package.json +24 -0
  33. package/packages/provider-hiapi/src/activation.ts +62 -0
  34. package/packages/provider-hiapi/src/errors.ts +44 -0
  35. package/packages/provider-hiapi/src/index.ts +2 -0
  36. package/packages/provider-hiapi/src/mapping.ts +129 -0
  37. package/packages/provider-hiapi/src/provider.ts +215 -0
  38. package/packages/provider-hiapi/src/routes.ts +154 -0
  39. package/packages/provider-hyperframes-local/src/program.ts +2 -1
  40. package/packages/provider-media-local/README.md +2 -1
  41. package/packages/provider-monid/README.md +60 -0
  42. package/packages/provider-monid/package.json +19 -0
  43. package/packages/provider-monid/src/activation.ts +62 -0
  44. package/packages/provider-monid/src/errors.ts +55 -0
  45. package/packages/provider-monid/src/index.ts +2 -0
  46. package/packages/provider-monid/src/mapping.ts +33 -0
  47. package/packages/provider-monid/src/provider.ts +242 -0
  48. package/packages/provider-monid/src/routes.ts +103 -0
  49. package/packages/provider-pollo/README.md +59 -0
  50. package/packages/provider-pollo/package.json +22 -0
  51. package/packages/provider-pollo/src/activation.ts +62 -0
  52. package/packages/provider-pollo/src/errors.ts +41 -0
  53. package/packages/provider-pollo/src/index.ts +2 -0
  54. package/packages/provider-pollo/src/mapping.ts +60 -0
  55. package/packages/provider-pollo/src/provider.ts +194 -0
  56. package/packages/provider-pollo/src/routes.ts +120 -0
  57. package/packages/provider-tokendance/README.md +67 -0
  58. package/packages/provider-tokendance/package.json +21 -0
  59. package/packages/provider-tokendance/src/activation.ts +62 -0
  60. package/packages/provider-tokendance/src/errors.ts +47 -0
  61. package/packages/provider-tokendance/src/index.ts +2 -0
  62. package/packages/provider-tokendance/src/mapping.ts +60 -0
  63. package/packages/provider-tokendance/src/provider.ts +268 -0
  64. package/packages/provider-tokendance/src/routes.ts +192 -0
  65. package/packages/runtime-local/src/programs.ts +2 -0
  66. package/packages/video-cli/package.json +4 -0
  67. package/packages/credential-store-file/src/paths.ts +0 -16
  68. /package/packages/{build-result → file-io-node}/src/replace-file.ts +0 -0
@@ -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 { createHiApiProvider } from "./provider.js";
12
+
13
+ const adapter = createRuntimeEndpointAdapterFacet({
14
+ use: "@hypit/provider-hiapi",
15
+ activate(context) {
16
+ if (context.pool === undefined) throw new Error("HiAPI Provider Pool is required");
17
+ const config = runtimeConfigObject(context.config, "HiAPI");
18
+ runtimeConfigExact(config, [
19
+ "baseUrl",
20
+ "apiKey",
21
+ "defaultConcurrency",
22
+ "actionLimits",
23
+ "pollIntervalMs",
24
+ "requestTimeoutMs",
25
+ "operationTimeoutMs",
26
+ ], "HiAPI");
27
+ const baseUrl = runtimeConfigString(config.baseUrl, "HiAPI 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("HiAPI baseUrl must use HTTPS or loopback");
32
+ }
33
+ }
34
+ const apiKey = runtimeConfigCredentialRef(config.apiKey, "HiAPI apiKey");
35
+ if (apiKey === undefined) throw new Error("HiAPI apiKey CredentialRef is required");
36
+ const actionLimits = runtimeConfigActionLimits(config.actionLimits);
37
+ const defaultConcurrency = runtimeConfigPositiveInteger(config.defaultConcurrency, "HiAPI defaultConcurrency");
38
+ const pollIntervalMs = runtimeConfigPositiveInteger(config.pollIntervalMs, "HiAPI pollIntervalMs");
39
+ const requestTimeoutMs = runtimeConfigPositiveInteger(config.requestTimeoutMs, "HiAPI requestTimeoutMs");
40
+ const operationTimeoutMs = runtimeConfigPositiveInteger(config.operationTimeoutMs, "HiAPI operationTimeoutMs");
41
+ return {
42
+ endpoint: createHiApiProvider({
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,44 @@
1
+ /** HiAPI's `{ code, message, error_code }` envelope and task `error` object, 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 safeHiApiReason(value: string): string {
12
+ return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
13
+ }
14
+
15
+ export class HiApiServiceError extends Error {
16
+ constructor(readonly code: string, message: string) { super(message); }
17
+ }
18
+
19
+ export class HiApiHttpError extends HiApiServiceError {
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 code = text(body?.error_code) ?? "HIAPI_HTTP_ERROR";
25
+ const reason = text(body?.message) ?? (body === undefined ? text(bodyText.slice(0, 2000)) : undefined);
26
+ const requestId = text(response.headers.get("x-request-id"));
27
+ const facts = [
28
+ `HiAPI HTTP ${status}`, code, `${request.method} ${request.path}`,
29
+ ...(request.model === undefined ? [] : [`model=${request.model}`]),
30
+ ...(requestId === undefined ? [] : [`request=${requestId}`]),
31
+ ];
32
+ super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeHiApiReason(reason)}`}`);
33
+ }
34
+ }
35
+
36
+ /** A terminal `fail` task; `undefined` otherwise. */
37
+ export function hiApiTaskFailure(task: Record<string, unknown>, id: string): HiApiServiceError | undefined {
38
+ if (task.status !== "fail") return undefined;
39
+ const error = record(task.error);
40
+ const code = text(error?.code) ?? "HIAPI_TASK_FAILED";
41
+ const reason = text(error?.message);
42
+ return new HiApiServiceError(code,
43
+ `HiAPI task ${id} failed; ${code}${reason === undefined ? "" : `: ${safeHiApiReason(reason)}`}`);
44
+ }
@@ -0,0 +1,2 @@
1
+ export { createHiApiProvider, hiApiProviderModuleRef } from "./provider.js";
2
+ export type { CreateHiApiProviderOptions } from "./provider.js";
@@ -0,0 +1,129 @@
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
+ const GPT_IMAGE: ModuleRef = { name: "@hypit/gpt-image", version: "1" };
8
+ const NANO_BANANA: ModuleRef = { name: "@hypit/nano-banana", version: "1" };
9
+ const GROK: ModuleRef = { name: "@hypit/grok-imagine", version: "1" };
10
+
11
+ /**
12
+ * HiAPI model IDs and the `input` fields each documents. A model whose HiAPI ID depends on the
13
+ * request shape (text, frames or references) lists one route per shape; the first route whose
14
+ * ports are present wins. `personReference` is accepted on Seedance visual references and not
15
+ * transmitted: HiAPI has no field for it.
16
+ */
17
+ const seedanceFields = {
18
+ prompt: { as: "value", field: "prompt" },
19
+ aspectRatio: { as: "value", field: "aspect_ratio" },
20
+ duration: { as: "value", field: "duration" },
21
+ resolution: { as: "value", field: "resolution" },
22
+ firstFrame: { as: "url", field: "first_frame_url", resourceFields: ["personReference"] },
23
+ lastFrame: { as: "url", field: "last_frame_url", resourceFields: ["personReference"] },
24
+ referenceImage: { as: "urlArray", field: "reference_image_urls", resourceFields: ["personReference"] },
25
+ referenceVideo: { as: "urlArray", field: "reference_video_urls", resourceFields: ["personReference"] },
26
+ referenceAudio: { as: "urlArray", field: "reference_audio_urls" },
27
+ generateAudio: { as: "value", field: "generate_audio" },
28
+ webSearch: { as: "value", field: "web_search" },
29
+ } as const satisfies GenerationWireMapping["fields"];
30
+
31
+ const seedance2 = (name: string, model: string): GenerationWireMapping => ({
32
+ capability: { module: SEEDANCE, name }, result: "video", routes: [{ model }], fields: seedanceFields,
33
+ });
34
+
35
+ export const hiApiMappings: readonly GenerationWireMapping[] = [
36
+ seedance2("seedance-2", "seedance-2.0"),
37
+ seedance2("seedance-2-fast", "seedance-2.0-fast"),
38
+ seedance2("seedance-2-mini", "seedance-2.0-mini"),
39
+ {
40
+ capability: { module: SEEDANCE, name: "seedance-2.5" }, result: "video",
41
+ routes: [
42
+ { model: "seedance-2.5/reference-to-video", whenPresent: ["referenceVideo"] },
43
+ { model: "seedance-2.5/image-to-video", whenPresent: ["firstFrame"] },
44
+ { model: "seedance-2.5/image-to-video", whenPresent: ["referenceImage"] },
45
+ { model: "seedance-2.5/image-to-video", whenPresent: ["referenceAudio"] },
46
+ { model: "seedance-2.5/text-to-video" },
47
+ ],
48
+ fields: seedanceFields,
49
+ },
50
+ {
51
+ capability: { module: SEEDREAM, name: "seedream-5-lite" }, result: "image",
52
+ routes: [
53
+ { model: "seedream-5.0-lite/image-to-image", whenPresent: ["images"] },
54
+ { model: "seedream-5.0-lite/text-to-image" },
55
+ ],
56
+ fields: {
57
+ prompt: { as: "value", field: "prompt" },
58
+ aspectRatio: { as: "value", field: "aspect_ratio" },
59
+ quality: { as: "value", field: "quality" },
60
+ outputFormat: { as: "value", field: "output_format" },
61
+ nsfwCheck: { as: "value", field: "nsfw_check" },
62
+ images: { as: "urlArray", field: "image_urls" },
63
+ },
64
+ },
65
+ {
66
+ capability: { module: MINIMAX, name: "minimax-h3" }, result: "video", routes: [{ model: "minimax-h3" }],
67
+ constants: { watermark: false },
68
+ fields: {
69
+ prompt: { as: "value", field: "prompt" },
70
+ duration: { as: "value", field: "duration" },
71
+ resolution: { as: "value", field: "resolution" },
72
+ aspectRatio: { as: "value", field: "aspect_ratio" },
73
+ firstFrame: { as: "url", field: "first_frame_image" },
74
+ lastFrame: { as: "url", field: "last_frame_image" },
75
+ referenceImage: { as: "urlArray", field: "image_urls" },
76
+ referenceVideo: { as: "urlArray", field: "video_urls" },
77
+ referenceAudio: { as: "urlArray", field: "audio_urls" },
78
+ },
79
+ },
80
+ {
81
+ capability: { module: GPT_IMAGE, name: "gpt-image-2" }, result: "image",
82
+ routes: [
83
+ { model: "gpt-image-2/image-to-image", whenPresent: ["images"] },
84
+ { model: "gpt-image-2/text-to-image" },
85
+ ],
86
+ fields: {
87
+ prompt: { as: "value", field: "prompt" },
88
+ aspectRatio: { as: "value", field: "aspect_ratio" },
89
+ resolution: { as: "value", field: "resolution" },
90
+ background: { as: "value", field: "background" },
91
+ images: { as: "urlArray", field: "input_urls" },
92
+ },
93
+ },
94
+ ...([["nano-banana-2", "Nano-Banana-2"], ["nano-banana-pro", "Nano-Banana-Pro"]] as const).map(([name, model]): GenerationWireMapping => ({
95
+ capability: { module: NANO_BANANA, name }, result: "image", routes: [{ model }],
96
+ fields: {
97
+ prompt: { as: "value", field: "prompt" },
98
+ images: { as: "urlArray", field: "image_input" },
99
+ aspectRatio: { as: "value", field: "aspect_ratio" },
100
+ resolution: { as: "value", field: "resolution" },
101
+ outputFormat: { as: "value", field: "output_format" },
102
+ },
103
+ })),
104
+ {
105
+ capability: { module: GROK, name: "grok-imagine-video" }, result: "video",
106
+ routes: [
107
+ { model: "grok-imagine/image-to-video", whenPresent: ["images"] },
108
+ { model: "grok-imagine/text-to-video" },
109
+ ],
110
+ fields: {
111
+ prompt: { as: "value", field: "prompt" },
112
+ aspectRatio: { as: "value", field: "aspect_ratio" },
113
+ resolution: { as: "value", field: "resolution" },
114
+ duration: { as: "value", field: "duration" },
115
+ images: { as: "urlArray", field: "image_urls" },
116
+ },
117
+ },
118
+ {
119
+ capability: { module: GROK, name: "grok-imagine-video-1.5-preview" }, result: "video",
120
+ routes: [{ model: "grok-imagine-1.5/image-to-video" }],
121
+ fields: {
122
+ prompt: { as: "value", field: "prompt" },
123
+ aspectRatio: { as: "value", field: "aspect_ratio" },
124
+ resolution: { as: "value", field: "resolution" },
125
+ duration: { as: "value", field: "duration" },
126
+ images: { as: "urlArray", field: "image_urls" },
127
+ },
128
+ },
129
+ ];
@@ -0,0 +1,215 @@
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 { hiApiRouteForCapability, hiApiRoutes } from "./routes.js";
10
+ import type { HiApiMediaLimits } from "./routes.js";
11
+ import { HiApiHttpError, HiApiServiceError, hiApiTaskFailure } from "./errors.js";
12
+
13
+ export const hiApiProviderModuleRef = { name: "@hypit/provider-hiapi", version: "1" } as const;
14
+
15
+ export type CreateHiApiProviderOptions = {
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.hiapi-operation@1";
32
+ readonly taskId: string;
33
+ readonly route: string;
34
+ readonly startedAt: number;
35
+ readonly urls?: readonly 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, "HiAPI 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, "HiAPI apiKey credential is unavailable; store a HiAPI 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 HiApiServiceError ? error.code : "HIAPI_ERROR", message: failureMessage(error) } };
60
+ }
61
+
62
+ class HiApiClient {
63
+ constructor(readonly baseUrl: string, readonly timeout: number, readonly fetcher: typeof globalThis.fetch) {}
64
+ async json(path: string, key: string, init: RequestInit = {}): Promise<Record<string, unknown>> {
65
+ const deadline = requestDeadline(this.timeout);
66
+ try {
67
+ const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, {
68
+ ...init, signal: deadline.signal, headers: { authorization: `Bearer ${key}`, ...(init.headers ?? {}) },
69
+ }));
70
+ const text = await deadline.wait(response.text());
71
+ if (!response.ok) {
72
+ const input = typeof init.body === "string" ? JSON.parse(init.body) as Record<string, unknown> : undefined;
73
+ throw new HiApiHttpError(response.status, response, text, {
74
+ method: init.method ?? "GET", path, ...(typeof input?.model === "string" ? { model: input.model } : {}),
75
+ });
76
+ }
77
+ let body: unknown;
78
+ try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new Error(`HiAPI returned invalid JSON (${response.status})`); }
79
+ return object(object(body, "HiAPI response").data, "HiAPI response data");
80
+ } finally { deadline.finish(); }
81
+ }
82
+ async download(url: string): Promise<{ readonly bytes: Uint8Array; readonly mediaType: string }> {
83
+ const deadline = requestDeadline(this.timeout);
84
+ try {
85
+ const response = await deadline.wait(this.fetcher(url, { signal: deadline.signal }));
86
+ if (!response.ok) throw new Error(`HiAPI asset returned HTTP ${response.status}`);
87
+ return { bytes: new Uint8Array(await deadline.wait(response.arrayBuffer())), mediaType: response.headers.get("content-type")?.split(";", 1)[0] ?? "application/octet-stream" };
88
+ } finally { deadline.finish(); }
89
+ }
90
+ }
91
+
92
+ function resolverFor(limits: HiApiMediaLimits, context: EndpointInvocationContext, publicAssetUrl: CreateHiApiProviderOptions["publicAssetUrl"]): GenerationArtifactUrlResolver {
93
+ const resolved = new Map<string, Promise<string>>();
94
+ let imageBytes = 0;
95
+ return (artifact, fields) => {
96
+ const existing = resolved.get(artifact.resource);
97
+ if (existing !== undefined) return existing;
98
+ const promise = (async () => {
99
+ if (publicAssetUrl !== undefined) return await publicAssetUrl(artifact, context.resources, fields);
100
+ // HiAPI documents data URLs for image and audio inputs; reference videos must be public HTTPS URLs.
101
+ const kind = artifact.mediaType.split("/", 1)[0];
102
+ assert(kind === "image" || kind === "audio",
103
+ `HiAPI accepts ${artifact.mediaType} references only by public URL; configure publicAssetUrl for this Endpoint`);
104
+ const limit = limits[kind];
105
+ assert(limit === undefined || artifact.size <= limit,
106
+ `HiAPI accepts ${kind} references up to ${(limit ?? 0) / 1_000_000} MB for this model; ${artifact.resource} is ${artifact.size} bytes`);
107
+ if (kind === "image") {
108
+ imageBytes += artifact.size;
109
+ assert(limits.imagesTotal === undefined || imageBytes <= limits.imagesTotal,
110
+ `HiAPI accepts reference images up to ${(limits.imagesTotal ?? 0) / 1_000_000} MB combined for this model`);
111
+ }
112
+ const bytes = await context.resources.get(artifact.resource);
113
+ assert(bytes !== undefined && bytes.byteLength === artifact.size, `Reference Resource ${artifact.resource} is unavailable or has changed`);
114
+ return `data:${artifact.mediaType};base64,${Buffer.from(bytes).toString("base64")}`;
115
+ })();
116
+ resolved.set(artifact.resource, promise);
117
+ return promise;
118
+ };
119
+ }
120
+
121
+ function endpoint(client: HiApiClient, pollIntervalMs: number, maxOperationMs: number, publicAssetUrl: CreateHiApiProviderOptions["publicAssetUrl"]): AsyncEndpoint {
122
+ return {
123
+ async start(context) {
124
+ try {
125
+ const route = hiApiRouteForCapability(context.need.capability);
126
+ assert(route !== undefined, "HiAPI does not implement this exact capability");
127
+ const request = route.prepare(context.need.constraints);
128
+ await context.reportProgress?.({ phase: `Preparing HiAPI request: ${request.model}` });
129
+ let body: Record<string, unknown>;
130
+ try {
131
+ body = await request.compile(resolverFor(request.mediaLimits, context, publicAssetUrl));
132
+ } catch (error) {
133
+ throw new HiApiServiceError(error instanceof HiApiServiceError ? error.code : "HIAPI_ERROR",
134
+ `HiAPI request preparation failed; model=${request.model}; generation not submitted: ${failureMessage(error)}`);
135
+ }
136
+ await context.reportProgress?.({ phase: `Submitting HiAPI request: ${request.model}` });
137
+ const response = await client.json("/v1/tasks", apiKey(context.credentials), {
138
+ method: "POST", headers: { "content-type": "application/json", "idempotency-key": context.operation }, body: JSON.stringify(body),
139
+ });
140
+ assert(typeof response.taskId === "string" && response.taskId.length > 0, "HiAPI response has no taskId");
141
+ const handle: Handle = { contract: "hypit.hiapi-operation@1", taskId: response.taskId, route: route.key, startedAt: Date.now() };
142
+ const receipt = { id: handle.taskId };
143
+ await context.checkpoint?.({ handle: canonicalize(handle), receipt });
144
+ return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "submitted" }), receipt };
145
+ } catch (error) {
146
+ return failure(error);
147
+ }
148
+ },
149
+ async poll(context) {
150
+ try {
151
+ const handle = object(context.handle, "HiAPI handle") as unknown as Handle;
152
+ const route = hiApiRouteForCapability(context.need.capability);
153
+ assert(route !== undefined && handle.contract === "hypit.hiapi-operation@1" && handle.route === route.key, "HiAPI handle is invalid");
154
+ const receipt = { id: handle.taskId };
155
+ if (Date.now() - handle.startedAt > maxOperationMs) {
156
+ return { status: "failed", receipt, failure: { code: "HIAPI_OPERATION_TIMEOUT", message: `HiAPI task ${handle.taskId} exceeded this Provider's operationTimeoutMs (${maxOperationMs}); remote outcome is unknown` } };
157
+ }
158
+ const task = await client.json(`/v1/tasks/${encodeURIComponent(handle.taskId)}`, apiKey(context.credentials));
159
+ const status = String(task.status);
160
+ if (status === "queued" || status === "handling" || status === "archiving") {
161
+ return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
162
+ }
163
+ const rejected = hiApiTaskFailure(task, handle.taskId);
164
+ if (rejected !== undefined) return { ...failure(rejected), receipt };
165
+ assert(status === "success", `HiAPI returned unknown task status ${status}`);
166
+ assert(Array.isArray(task.output) && task.output.length > 0, "HiAPI task succeeded without output");
167
+ const urls = task.output.map((item, index) => {
168
+ const url = object(item, `HiAPI output ${index + 1}`).url;
169
+ assert(typeof url === "string" && /^https?:\/\//u.test(url), `HiAPI output ${index + 1} has no URL`);
170
+ return url;
171
+ });
172
+ return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
173
+ } catch (error) {
174
+ return failure(error);
175
+ }
176
+ },
177
+ async collect(context) {
178
+ try {
179
+ const handle = object(context.handle, "HiAPI handle") as unknown as Handle;
180
+ const route = hiApiRouteForCapability(context.need.capability);
181
+ assert(route !== undefined && handle.route === route.key && Array.isArray(handle.urls), "HiAPI collection route differs");
182
+ await context.reportProgress?.({ phase: "Receiving generated files" });
183
+ const blobs: BlobRef[] = [];
184
+ for (const url of handle.urls) {
185
+ const downloaded = await client.download(url);
186
+ blobs.push(await context.resources.put(downloaded.bytes, downloaded.mediaType));
187
+ }
188
+ return { status: "completed", result: { value: route.packageResult(blobs) }, receipt: { id: handle.taskId } };
189
+ } catch (error) {
190
+ return failure(error);
191
+ }
192
+ },
193
+ };
194
+ }
195
+
196
+ export function createHiApiProvider(options: CreateHiApiProviderOptions = {}) {
197
+ const requestTimeoutMs = options.requestTimeoutMs ?? 300_000;
198
+ const operationTimeoutMs = options.operationTimeoutMs ?? 30 * 60_000;
199
+ for (const [name, value] of Object.entries({ requestTimeoutMs, operationTimeoutMs })) {
200
+ assert(Number.isSafeInteger(value) && value > 0, `HiAPI ${name} must be a positive integer`);
201
+ }
202
+ const client = new HiApiClient(apiBaseUrl(options.baseUrl ?? "https://api.hiapi.ai"), requestTimeoutMs, options.fetch ?? globalThis.fetch);
203
+ const asyncEndpoint = endpoint(client, options.pollIntervalMs ?? 10_000, operationTimeoutMs, options.publicAssetUrl);
204
+ return defineEndpointPackage({
205
+ module: hiApiProviderModuleRef, facet: "gateway", instance: options.instance ?? "hiapi.default", pool: options.pool ?? options.instance ?? "hiapi.default",
206
+ pricing: { kind: "page", url: "https://www.hiapi.ai/en/pricing" },
207
+ credentials: { apiKey: options.apiKey ?? credentialRef("os", "hiapi.api-key") },
208
+ credentialInputs: { apiKey: { label: "HiAPI API key" } },
209
+ defaultConcurrency: options.defaultConcurrency ?? 4,
210
+ ...(options.actionLimits === undefined ? {} : { actionLimits: options.actionLimits }),
211
+ capabilities: hiApiRoutes.map((route) => ({
212
+ capability: route.capability, returns: route.returns, lifecycle: "asynchronous" as const, endpoint: asyncEndpoint, capacity: route.capability.name, supports: route.supports,
213
+ })),
214
+ });
215
+ }
@@ -0,0 +1,154 @@
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 { hiApiMappings } from "./mapping.js";
13
+
14
+ /** Documented byte limits for inline media on one HiAPI model; an absent kind carries no documented cap. */
15
+ export type HiApiMediaLimits = {
16
+ readonly image?: number;
17
+ readonly audio?: number;
18
+ /** Documented cap on the combined size of all reference images. */
19
+ readonly imagesTotal?: number;
20
+ };
21
+
22
+ const MB = 1_000_000;
23
+
24
+ /** Per-file sizes HiAPI's model pages state for images and audio; videos travel by public URL. */
25
+ const hiApiMediaLimits: Readonly<Record<string, HiApiMediaLimits>> = {
26
+ "seedance-2.0-mini": { image: 30 * MB, audio: 15 * MB },
27
+ "seedance-2.5/image-to-video": { image: 30 * MB, audio: 15 * MB, imagesTotal: 120 * MB },
28
+ "seedance-2.5/reference-to-video": { image: 30 * MB, audio: 15 * MB, imagesTotal: 120 * MB },
29
+ "seedream-5.0-lite/image-to-image": { image: 10 * MB },
30
+ "Nano-Banana-Pro": { image: 30 * MB },
31
+ "grok-imagine/image-to-video": { image: 10 * MB },
32
+ "grok-imagine-1.5/image-to-video": { image: 20 * MB },
33
+ };
34
+
35
+ export type HiApiPreparedRequest = {
36
+ readonly model: string;
37
+ readonly mediaLimits: HiApiMediaLimits;
38
+ readonly compile: (resolve: GenerationArtifactUrlResolver) => Promise<Record<string, unknown>>;
39
+ };
40
+
41
+ export type HiApiRoute = GenerationWireMapping & {
42
+ readonly key: string;
43
+ readonly returns: TypeRef;
44
+ readonly supports: (request: EndpointRequest) => EndpointSupport;
45
+ readonly prepare: (constraints: CanonicalValue) => HiApiPreparedRequest;
46
+ readonly packageResult: (artifacts: readonly BlobRef[]) => StoredValue;
47
+ };
48
+
49
+ function scalar(request: GenerationRequest, port: string): string | number | boolean | undefined {
50
+ const value = request.ports[port]?.[0];
51
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : undefined;
52
+ }
53
+ function count(request: GenerationRequest, port: string): number {
54
+ return request.ports[port]?.length ?? 0;
55
+ }
56
+
57
+ const SEEDREAM_RESOLUTION: Readonly<Record<string, string>> = { basic: "2K", ultra: "4K" };
58
+ const GPT_IMAGE_UNAVAILABLE: Readonly<Record<string, readonly string[]>> = {
59
+ "2K": ["5:4", "4:5", "3:1", "1:3", "9:21"],
60
+ "4K": ["1:1", "3:1", "1:3", "9:21"],
61
+ };
62
+
63
+ /** HiAPI's documented input ranges for each mapped model, checked before any reference is resolved. */
64
+ function rejection(mapping: GenerationWireMapping, request: GenerationRequest): string | undefined {
65
+ const { name } = mapping.capability;
66
+ const ratio = scalar(request, "aspectRatio");
67
+ const resolution = scalar(request, "resolution");
68
+ let model: string;
69
+ try { model = selectWireModelForRequest(mapping, request); } catch (error) { return error instanceof Error ? error.message : String(error); }
70
+ if (name === "seedance-2-mini" && scalar(request, "webSearch") === true) {
71
+ return "HiAPI seedance-2.0-mini has no web_search field";
72
+ }
73
+ if (name === "seedance-2.5") {
74
+ if (model === "seedance-2.5/image-to-video" && ratio !== "adaptive") {
75
+ return `HiAPI ${model} takes only aspect-ratio adaptive, not ${String(ratio)}`;
76
+ }
77
+ if (model !== "seedance-2.5/reference-to-video" && resolution === "480p") {
78
+ return `HiAPI ${model} renders 720p or 1080p, not 480p`;
79
+ }
80
+ }
81
+ if (name === "seedream-5-lite" && SEEDREAM_RESOLUTION[String(scalar(request, "quality"))] === undefined) {
82
+ return `HiAPI Seedream 5.0 lite renders 2K (basic) or 4K (ultra), not quality ${String(scalar(request, "quality"))}`;
83
+ }
84
+ if (name === "minimax-h3") {
85
+ if (resolution !== undefined && resolution !== "2K") return `HiAPI minimax-h3 renders 2K only, not ${String(resolution)}`;
86
+ if (count(request, "referenceImage") > 5) return "HiAPI minimax-h3 accepts up to five reference images";
87
+ }
88
+ if (name === "gpt-image-2") {
89
+ if (count(request, "images") > 6) return "HiAPI gpt-image-2/image-to-image accepts up to six reference images";
90
+ if (scalar(request, "background") !== undefined && resolution !== "1K") return `HiAPI GPT Image 2 accepts background only at 1K; omit it at ${String(resolution)}`;
91
+ if (ratio === "auto" && resolution !== "1K") return "HiAPI GPT Image 2 renders aspect-ratio auto only at 1K";
92
+ if (GPT_IMAGE_UNAVAILABLE[String(resolution)]?.includes(String(ratio))) return `HiAPI GPT Image 2 does not render ${String(ratio)} at ${String(resolution)}`;
93
+ }
94
+ if (mapping.capability.module.name === "@hypit/grok-imagine") {
95
+ if (resolution === "1080p") return `HiAPI ${model} renders 480p or 720p, not 1080p`;
96
+ if (name === "grok-imagine-video-1.5-preview" && count(request, "images") !== 1) {
97
+ return "HiAPI grok-imagine-1.5/image-to-video animates exactly one image";
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ /** Fields the model package requires that HiAPI names differently or does not take. */
104
+ function normalize(mapping: GenerationWireMapping, input: Record<string, unknown>): Record<string, unknown> {
105
+ const { name } = mapping.capability;
106
+ if (name === "seedance-2-mini" && input.web_search === false) delete input.web_search;
107
+ if (name === "seedream-5-lite") {
108
+ input.resolution = SEEDREAM_RESOLUTION[String(input.quality)];
109
+ delete input.quality;
110
+ delete input.output_format;
111
+ delete input.nsfw_check;
112
+ }
113
+ return input;
114
+ }
115
+
116
+ function capabilityKey(capability: CapabilityRef): string {
117
+ return `${capability.module.name}@${capability.module.version}#${capability.name}`;
118
+ }
119
+
120
+ export const hiApiRoutes: readonly HiApiRoute[] = hiApiMappings.map((mapping) => ({
121
+ ...mapping,
122
+ key: capabilityKey(mapping.capability),
123
+ returns: mapping.result === "image" ? generationTypes.imageSet : generationTypes.videoSet,
124
+ supports: (request) => {
125
+ const reason = rejection(mapping, request.constraints as unknown as GenerationRequest);
126
+ return reason === undefined ? { status: "supported" } : { status: "unsupported", reason };
127
+ },
128
+ prepare: (constraints) => {
129
+ const request = constraints as unknown as GenerationRequest;
130
+ const reason = rejection(mapping, request);
131
+ if (reason !== undefined) throw new Error(reason);
132
+ const model = selectWireModelForRequest(mapping, request);
133
+ return {
134
+ model,
135
+ mediaLimits: hiApiMediaLimits[model] ?? {},
136
+ compile: async (resolve) => ({
137
+ model,
138
+ input: normalize(mapping, (await compileWireRequest(mapping, request, resolve)).input as Record<string, unknown>),
139
+ }),
140
+ };
141
+ },
142
+ packageResult: (artifacts) => ({
143
+ kind: "inline",
144
+ value: canonicalize(mapping.result === "image"
145
+ ? sealGeneratedImageSet({ images: artifacts })
146
+ : sealGeneratedVideoSet({ videos: artifacts })),
147
+ }),
148
+ }));
149
+
150
+ const byCapability = new Map(hiApiRoutes.map((route) => [route.key, route]));
151
+
152
+ export function hiApiRouteForCapability(capability: CapabilityRef): HiApiRoute | undefined {
153
+ return byCapability.get(capabilityKey(capability));
154
+ }
@@ -5,6 +5,7 @@ import { probeMediaToolchain } from "@hypit/media-execution";
5
5
  import type { ManagedProgram, ManagedProgramState } from "@hypit/runtime-kit";
6
6
  import { browserCacheDirectory, browserDownloadBaseUrl, browserDownloadUrl, browserExecutablePath, configuredBrowserPath, requireBrowserExecutable, selectedBrowserVersion } from "./browser.js";
7
7
  import type { BrowserOptions } from "./browser.js";
8
+ import { processEnvironment } from "./process.js";
8
9
 
9
10
  /**
10
11
  * This Provider owns browser selection and preparation. Probes never install;
@@ -44,7 +45,7 @@ export function localHyperframesBrowserProgram(
44
45
  async probe(): Promise<ManagedProgramState> {
45
46
  const browser = await probeBrowser();
46
47
  if (browser.state !== "ready") return browser;
47
- const media = await probeMediaToolchain({ ffprobePath: input.ffprobePath, ...(input.ffmpegPath === undefined ? {} : { ffmpegPath: input.ffmpegPath }) });
48
+ const media = await probeMediaToolchain({ environment: processEnvironment(), ffprobePath: input.ffprobePath, ...(input.ffmpegPath === undefined ? {} : { ffmpegPath: input.ffmpegPath }) });
48
49
  return media.state === "ready"
49
50
  ? { state: "ready" }
50
51
  : { state: media.state, detail: media.detail };
@@ -43,7 +43,8 @@ Source. Local HyperFrames extracts alpha-preserving PNGs and composites them aga
43
43
  Canvas and lower visual layers before encoding the final MP4.
44
44
 
45
45
  The Runtime Adapter declares the selected `ffmpeg`/`ffprobe` pair as an external, non-daemon Program.
46
- Its shared probe checks the encoders and filters used by the execution body. Compatible custom paths
46
+ Its probe checks that the selected executables start in the media execution environment. It does not
47
+ guarantee every codec or filter for every task; an unsupported operation reports FFmpeg’s actual error. Custom paths
47
48
  remain valid; the package neither pins a semantic Capability to one FFmpeg version nor mutates a
48
49
  system package manager.
49
50