@hypit/provider-sume 0.0.0-stage → 0.1.0
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/LICENSE +26 -0
- package/README.md +105 -2
- package/dist/activation.d.ts +5 -0
- package/dist/activation.js +52 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +50 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/mapping.d.ts +2 -0
- package/dist/mapping.js +120 -0
- package/dist/provider.d.ts +21 -0
- package/dist/provider.js +252 -0
- package/dist/routes.d.ts +17 -0
- package/dist/routes.js +149 -0
- package/package.json +26 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Open Source License
|
|
2
|
+
|
|
3
|
+
Hypit is licensed under a modified version of the Apache License 2.0, with the following additional conditions:
|
|
4
|
+
|
|
5
|
+
1. Hypit may be utilized commercially for your own organization's purposes, including as a rendering or generation backend for applications your organization operates for itself, and as internal tooling within an enterprise. Should any of the conditions below be met, a commercial license must be obtained from the producer:
|
|
6
|
+
|
|
7
|
+
a. Multi-tenant service: Unless explicitly authorized by Hypit.AI in writing, you may not use the Hypit source code, or any derivative work of it, to operate a multi-tenant environment, or to offer Hypit's functionality to third parties as a hosted, managed, or software-as-a-service offering.
|
|
8
|
+
- Tenant Definition: Within the context of Hypit, one tenant corresponds to one workspace — the isolated set of projects, artifacts, credentials, and configuration belonging to a single party. Operating an environment in which two or more parties outside your own organization hold separate workspaces constitutes a multi-tenant service, whether or not a fee is charged.
|
|
9
|
+
|
|
10
|
+
b. Commercial redistribution: Unless explicitly authorized by Hypit.AI in writing, you may not sell, license for a fee, or otherwise supply for commercial gain Hypit or any derivative work of it, whether standalone or as a component of, or bundled with, any product, application, package, or device provided to third parties.
|
|
11
|
+
- Forking, modifying, and publishing the source code of Hypit or a derivative work is permitted, provided the result is made available under the terms of this same license and is not supplied for commercial gain.
|
|
12
|
+
|
|
13
|
+
- Permitted without a commercial license under clauses (a) and (b): obtaining Hypit and running it yourself on your own infrastructure; using it for your own organization's work, including commercial work and work performed for your clients; and single-tenant deployments operated by and for one organization.
|
|
14
|
+
|
|
15
|
+
c. LOGO and copyright information: In the process of using Hypit, you may not remove or modify the name, LOGO, or copyright information presented in the Hypit command-line interface, its generated run reports and manifests, or any user-facing surface derived from them. This restriction is inapplicable to uses of Hypit that do not surface these components to a user.
|
|
16
|
+
|
|
17
|
+
2. As a contributor, you should agree that:
|
|
18
|
+
|
|
19
|
+
a. The producer can adjust the open-source agreement to be more strict or relaxed as deemed necessary.
|
|
20
|
+
b. Your contributed code may be used for commercial purposes, including but not limited to its cloud business operations.
|
|
21
|
+
|
|
22
|
+
3. Ownership of output: The producer claims no rights in the content you create with Hypit. Videos, audio, images, manifests, and any other artifacts produced through your use of Hypit belong to you, and this license imposes no conditions on their use — including commercial use. Note that content generated by way of third-party model or service providers may additionally be subject to the terms of those providers.
|
|
23
|
+
|
|
24
|
+
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0.
|
|
25
|
+
|
|
26
|
+
© 2026 Hypit.AI
|
package/README.md
CHANGED
|
@@ -1,3 +1,106 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@hypit/provider-sume`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Optional Provider, not installed with Hypit. Install in the video project:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @hypit/provider-sume
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Hypit Runtime Provider for a [Sume](https://docs.sume.com/public-api) API key. Each capability
|
|
10
|
+
submits one asynchronous job to `https://api.sume.com` with a `Bearer` API key, polls
|
|
11
|
+
`GET /v1/jobs/{id}/status` until the job is terminal, reads the generated files from
|
|
12
|
+
`GET /v1/jobs/{id}/result` and stores them in the current Build.
|
|
13
|
+
|
|
14
|
+
| Capability | Sume model | Submit path |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `@hypit/seedance@1#seedance-2` | `seedance-2` | `/v1/video-router/generate` |
|
|
17
|
+
| `@hypit/seedance@1#seedance-2-fast` | `seedance-2-fast` | `/v1/video-router/generate` |
|
|
18
|
+
| `@hypit/seedance@1#seedance-2-mini` | `seedance-2-mini` | `/v1/video-router/generate` |
|
|
19
|
+
| `@hypit/seedance@1#seedance-2.5` | `seedance-2.5` | `/v1/video-router/generate` |
|
|
20
|
+
| `@hypit/minimax-h3@1#minimax-h3` | `minimax-h3` | `/v1/video-router/generate` |
|
|
21
|
+
| `@hypit/grok-imagine@1#grok-imagine-video-1.5-preview` | `grok-imagine-video-1.5` | `/v1/video-router/generate` |
|
|
22
|
+
| `@hypit/pixverse@1#pixverse-v6` | `pixverse-v6` | `/v1/video-router/generate` |
|
|
23
|
+
| `@hypit/pixverse@1#pixverse-c1` | `pixverse-c1` | `/v1/video-router/generate` |
|
|
24
|
+
| `@hypit/gpt-image@1#gpt-image-2` | `openai/gpt-image-2` | `/v1/images` |
|
|
25
|
+
| `@hypit/nano-banana@1#nano-banana-2` | `google/nano-banana-2.1` | `/v1/images` |
|
|
26
|
+
| `@hypit/nano-banana@1#nano-banana-pro` | `google/nano-banana-pro` | `/v1/images` |
|
|
27
|
+
| `@hypit/seedream@1#seedream-5-lite` | `bytedance-seed/seedream-5-lite` | `/v1/images` |
|
|
28
|
+
|
|
29
|
+
Sume's [video](https://docs.sume.com/models/videos.md) and [image](https://docs.sume.com/models/images.md)
|
|
30
|
+
catalogs list further models; this Provider maps only the models the Distribution already describes.
|
|
31
|
+
Sume serves Nano Banana 2 as its successor, Nano Banana 2.1, so a `nano-banana-2` request renders on
|
|
32
|
+
`google/nano-banana-2.1`.
|
|
33
|
+
|
|
34
|
+
Video jobs carry the opening and closing frames as `image_url` and `end_image_url`, and references as
|
|
35
|
+
`reference_image_urls`, `reference_video_urls` and `reference_audio_urls`. Image jobs carry their
|
|
36
|
+
references as `input_references`. Every submission uses `mode: "async"`. `duration`, `aspect_ratio`,
|
|
37
|
+
`resolution` and `generate_audio` keep their authored values, except that MiniMax H3's `768P` is sent
|
|
38
|
+
as `768p`, PixVerse's `quality` travels as `resolution`, and Nano Banana's `jpg` output format is sent
|
|
39
|
+
as `jpeg`. A Grok Imagine 1.5 image travels as the first frame, and the first frame then sets the
|
|
40
|
+
shape, so the authored aspect ratio is not sent with it.
|
|
41
|
+
|
|
42
|
+
Service limits this Provider reports as unsupported before uploading any reference:
|
|
43
|
+
|
|
44
|
+
- Seedance has no `web_search` field. An authored `false` is dropped; an authored `true` is refused.
|
|
45
|
+
- Seedance 2.5 takes 4 to 30 seconds, so `duration="-1"` for an automatic length is refused.
|
|
46
|
+
- MiniMax H3 takes 5 to 15 seconds.
|
|
47
|
+
- Grok Imagine 1.5 takes a prompt alone or one first-frame image, does not render `2:3` or `3:2`, and
|
|
48
|
+
needs an explicit aspect ratio without an image.
|
|
49
|
+
- PixVerse V6 and C1 take a prompt or reference images, without frames, reference videos, a seed or
|
|
50
|
+
multi-clip, for 2 to 15 seconds, and do not render `2:3`, `3:2` or `auto`.
|
|
51
|
+
- GPT Image 2 has no `background` field, accepts up to ten reference images and does not render
|
|
52
|
+
`3:2`, `2:3`, `2:1`, `1:2`, `3:1`, `1:3`, `21:9` or `9:21`.
|
|
53
|
+
- Nano Banana 2.1 accepts up to ten reference images.
|
|
54
|
+
- Seedream 5.0 lite renders at 2K, which is `quality="basic"`; accepts up to ten reference images;
|
|
55
|
+
does not render `21:9`; and has no switch to turn its content check off, so `nsfw-check="false"` is
|
|
56
|
+
refused.
|
|
57
|
+
|
|
58
|
+
Seedance visual references carry `person-reference`; the Provider accepts the declaration and
|
|
59
|
+
transmits nothing for it, since Sume's request body has no field for it.
|
|
60
|
+
|
|
61
|
+
Referenced Resources are uploaded through `POST /v1/files/generate-upload-url`: the Provider PUTs the
|
|
62
|
+
bytes to the returned presigned URL and sends the returned public `media.sume.com` URL in the job. One
|
|
63
|
+
Resource is uploaded once per Runtime operation. Embedded callers may replace that transport with
|
|
64
|
+
`publicAssetUrl(artifact, resources, fields)`, which must return a public HTTPS URL Sume can fetch.
|
|
65
|
+
|
|
66
|
+
Runtime Profile example:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"format": "hypit.runtime-local@1",
|
|
71
|
+
"dataRoot": ".hypit/runtimes/local",
|
|
72
|
+
"credentials": {
|
|
73
|
+
"local": {
|
|
74
|
+
"use": "@hypit/credential-store-local"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"endpoints": {
|
|
78
|
+
"sume.default": {
|
|
79
|
+
"use": "@hypit/provider-sume",
|
|
80
|
+
"pool": "sume.default",
|
|
81
|
+
"config": {
|
|
82
|
+
"apiKey": { "store": "local", "key": "sume.api-key" },
|
|
83
|
+
"defaultConcurrency": 2,
|
|
84
|
+
"pollIntervalMs": 5000
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"bindings": {}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`baseUrl` defaults to `https://api.sume.com`. Store the API key with
|
|
93
|
+
`hypit auth login sume.default --runtime hypit.runtime.json`. `defaultConcurrency` defaults to 2,
|
|
94
|
+
which is the number of jobs Sume's free plan runs at once; raise it to the workspace's plan limit.
|
|
95
|
+
Optional `requestTimeoutMs`, `operationTimeoutMs` and `actionLimits` bound single HTTP calls, the
|
|
96
|
+
whole remote job and action concurrency. Each submission sends the Runtime operation as its
|
|
97
|
+
`Idempotency-Key`, so a retried submission of the same body resolves to the same job and is charged
|
|
98
|
+
once.
|
|
99
|
+
|
|
100
|
+
HTTP failures keep Sume's `error.code`, message, `details`, `request_id` and any
|
|
101
|
+
`retry_after_seconds`; a failed or canceled job keeps its error code, message and the upstream
|
|
102
|
+
provider's message. Any URL inside a surfaced reason is redacted, since an upload or result URL is an
|
|
103
|
+
access capability rather than diagnostic content. Polling transport failures and HTTP 429/5xx
|
|
104
|
+
preserve the same job. Sume's `retry_after_seconds` takes precedence over the standard `Retry-After`
|
|
105
|
+
header; either wait is capped at the original operation deadline, where the next poll concludes
|
|
106
|
+
timeout without querying early.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createRuntimeEndpointAdapterFacet, runtimeConfigCredentialRef, runtimeConfigActionLimits, runtimeConfigExact, runtimeConfigObject, runtimeConfigPositiveInteger, runtimeConfigString, } from "@hypit/runtime-local/extension";
|
|
2
|
+
import { createSumeProvider } from "./provider.js";
|
|
3
|
+
const adapter = createRuntimeEndpointAdapterFacet({
|
|
4
|
+
use: "@hypit/provider-sume",
|
|
5
|
+
activate(context) {
|
|
6
|
+
if (context.pool === undefined)
|
|
7
|
+
throw new Error("Sume Provider Pool is required");
|
|
8
|
+
const config = runtimeConfigObject(context.config, "Sume");
|
|
9
|
+
runtimeConfigExact(config, [
|
|
10
|
+
"baseUrl",
|
|
11
|
+
"apiKey",
|
|
12
|
+
"defaultConcurrency",
|
|
13
|
+
"actionLimits",
|
|
14
|
+
"pollIntervalMs",
|
|
15
|
+
"requestTimeoutMs",
|
|
16
|
+
"operationTimeoutMs",
|
|
17
|
+
], "Sume");
|
|
18
|
+
const baseUrl = runtimeConfigString(config.baseUrl, "Sume baseUrl");
|
|
19
|
+
if (baseUrl !== undefined) {
|
|
20
|
+
const url = new URL(baseUrl);
|
|
21
|
+
if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
|
|
22
|
+
throw new Error("Sume baseUrl must use HTTPS or loopback");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const apiKey = runtimeConfigCredentialRef(config.apiKey, "Sume apiKey");
|
|
26
|
+
if (apiKey === undefined)
|
|
27
|
+
throw new Error("Sume apiKey CredentialRef is required");
|
|
28
|
+
const actionLimits = runtimeConfigActionLimits(config.actionLimits);
|
|
29
|
+
const defaultConcurrency = runtimeConfigPositiveInteger(config.defaultConcurrency, "Sume defaultConcurrency");
|
|
30
|
+
const pollIntervalMs = runtimeConfigPositiveInteger(config.pollIntervalMs, "Sume pollIntervalMs");
|
|
31
|
+
const requestTimeoutMs = runtimeConfigPositiveInteger(config.requestTimeoutMs, "Sume requestTimeoutMs");
|
|
32
|
+
const operationTimeoutMs = runtimeConfigPositiveInteger(config.operationTimeoutMs, "Sume operationTimeoutMs");
|
|
33
|
+
return {
|
|
34
|
+
endpoint: createSumeProvider({
|
|
35
|
+
instance: context.instance,
|
|
36
|
+
pool: context.pool,
|
|
37
|
+
...(baseUrl === undefined ? {} : { baseUrl }),
|
|
38
|
+
apiKey,
|
|
39
|
+
...(defaultConcurrency === undefined ? {} : { defaultConcurrency }),
|
|
40
|
+
...(actionLimits === undefined ? {} : { actionLimits }),
|
|
41
|
+
...(pollIntervalMs === undefined ? {} : { pollIntervalMs }),
|
|
42
|
+
...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
|
|
43
|
+
...(operationTimeoutMs === undefined ? {} : { operationTimeoutMs }),
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
export const hypitPackage = {
|
|
49
|
+
format: "hypit.package@1",
|
|
50
|
+
facets: [adapter],
|
|
51
|
+
};
|
|
52
|
+
export default hypitPackage;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EndpointServiceError } from "@hypit/hypit/endpoint";
|
|
2
|
+
import { EndpointHttpError } from "@hypit/hypit/endpoint/http";
|
|
3
|
+
export declare function safeSumeReason(value: string): string;
|
|
4
|
+
export declare class SumeServiceError extends EndpointServiceError {
|
|
5
|
+
}
|
|
6
|
+
export declare class SumeHttpError extends EndpointHttpError {
|
|
7
|
+
constructor(status: number, response: {
|
|
8
|
+
readonly headers: Headers;
|
|
9
|
+
}, bodyText: string, request: {
|
|
10
|
+
readonly method: string;
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly model?: string;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** A terminal `failed` or `canceled` job; `undefined` otherwise. */
|
|
16
|
+
export declare function sumeJobFailure(status: string, job: Record<string, unknown> | undefined, id: string): SumeServiceError | undefined;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { EndpointServiceError } from "@hypit/hypit/endpoint";
|
|
2
|
+
import { EndpointHttpError, retryAfterMs } from "@hypit/hypit/endpoint/http";
|
|
3
|
+
/** Sume's `{ error: { code, message, request_id, retry_after_seconds, details } }` envelope and failed jobs. */
|
|
4
|
+
function record(value) {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
6
|
+
? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
function text(value) {
|
|
9
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
10
|
+
}
|
|
11
|
+
// Responses may carry a signed upload or result URL. Keep the reason, not its access capability.
|
|
12
|
+
export function safeSumeReason(value) {
|
|
13
|
+
return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
|
|
14
|
+
}
|
|
15
|
+
export class SumeServiceError extends EndpointServiceError {
|
|
16
|
+
}
|
|
17
|
+
export class SumeHttpError extends EndpointHttpError {
|
|
18
|
+
constructor(status, response, bodyText, request) {
|
|
19
|
+
let body;
|
|
20
|
+
try {
|
|
21
|
+
body = record(JSON.parse(bodyText));
|
|
22
|
+
}
|
|
23
|
+
catch { /* Non-JSON gateway failures still have HTTP evidence. */ }
|
|
24
|
+
const error = record(body?.error);
|
|
25
|
+
const code = text(error?.code) ?? "SUME_HTTP_ERROR";
|
|
26
|
+
const reason = text(error?.message) ?? (error === undefined ? text(bodyText.slice(0, 2000)) : undefined);
|
|
27
|
+
const details = error?.details === undefined ? undefined : JSON.stringify(error.details).slice(0, 1000);
|
|
28
|
+
const requestId = text(error?.request_id) ?? text(response.headers.get("x-sume-request-id"));
|
|
29
|
+
const serviceRetryAfter = typeof error?.retry_after_seconds === "number" ? Math.round(error.retry_after_seconds * 1000) : undefined;
|
|
30
|
+
const retryAfter = serviceRetryAfter ?? retryAfterMs(response.headers);
|
|
31
|
+
const facts = [
|
|
32
|
+
`Sume HTTP ${status}`, code, `${request.method} ${request.path}`,
|
|
33
|
+
...(request.model === undefined ? [] : [`model=${request.model}`]),
|
|
34
|
+
...(requestId === undefined ? [] : [`request=${requestId}`]),
|
|
35
|
+
...(retryAfter === undefined ? [] : [`retry-after=${retryAfter}ms`]),
|
|
36
|
+
];
|
|
37
|
+
const message = [reason, details].filter((item) => item !== undefined).join(" ");
|
|
38
|
+
super(code, `${facts.join("; ")}${message.length === 0 ? "" : `: ${safeSumeReason(message)}`}`, status, retryAfter);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** A terminal `failed` or `canceled` job; `undefined` otherwise. */
|
|
42
|
+
export function sumeJobFailure(status, job, id) {
|
|
43
|
+
if (status !== "failed" && status !== "canceled")
|
|
44
|
+
return undefined;
|
|
45
|
+
const error = record(job?.error);
|
|
46
|
+
const code = text(error?.code) ?? text(error?.public_reason) ?? (status === "canceled" ? "SUME_JOB_CANCELED" : "SUME_JOB_FAILED");
|
|
47
|
+
const upstream = text(record(error?.details)?.provider_error_message);
|
|
48
|
+
const reason = [text(error?.message) ?? text(job?.error), upstream].filter((item) => item !== undefined).join(" ") || undefined;
|
|
49
|
+
return new SumeServiceError(code, `Sume job ${id} ${status}; ${code}${reason === undefined ? "" : `: ${safeSumeReason(reason)}`}`);
|
|
50
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createSumeProvider, sumeProviderModuleRef } from "./provider.js";
|
package/dist/mapping.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const SEEDANCE = { name: "@hypit/seedance", version: "1" };
|
|
2
|
+
const MINIMAX = { name: "@hypit/minimax-h3", version: "1" };
|
|
3
|
+
const GROK = { name: "@hypit/grok-imagine", version: "1" };
|
|
4
|
+
const PIXVERSE = { name: "@hypit/pixverse", version: "1" };
|
|
5
|
+
const GPT_IMAGE = { name: "@hypit/gpt-image", version: "1" };
|
|
6
|
+
const NANO_BANANA = { name: "@hypit/nano-banana", version: "1" };
|
|
7
|
+
const SEEDREAM = { name: "@hypit/seedream", version: "1" };
|
|
8
|
+
/**
|
|
9
|
+
* Sume catalog ids and the body fields each one documents.
|
|
10
|
+
*
|
|
11
|
+
* Video models post to `POST /v1/video-router/generate`, whose body carries every reference as a
|
|
12
|
+
* flat URL field. Image models post to `POST /v1/images`; its `input_references` array of
|
|
13
|
+
* `{ type, image_url: { url } }` objects is built in routes.ts from the `images` URLs written here.
|
|
14
|
+
* Ports the service has no field for are written under their own names so routes.ts can drop an
|
|
15
|
+
* authored "off" value and refuse an authored "on" value before anything is uploaded.
|
|
16
|
+
*/
|
|
17
|
+
const seedanceFields = {
|
|
18
|
+
prompt: { as: "value", field: "prompt" },
|
|
19
|
+
firstFrame: { as: "url", field: "image_url", resourceFields: ["personReference"] },
|
|
20
|
+
lastFrame: { as: "url", field: "end_image_url", resourceFields: ["personReference"] },
|
|
21
|
+
referenceImage: { as: "urlArray", field: "reference_image_urls", resourceFields: ["personReference"] },
|
|
22
|
+
referenceVideo: { as: "urlArray", field: "reference_video_urls", resourceFields: ["personReference"] },
|
|
23
|
+
referenceAudio: { as: "urlArray", field: "reference_audio_urls" },
|
|
24
|
+
duration: { as: "value", field: "duration" },
|
|
25
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
26
|
+
resolution: { as: "value", field: "resolution" },
|
|
27
|
+
generateAudio: { as: "value", field: "generate_audio" },
|
|
28
|
+
webSearch: { as: "value", field: "web_search" },
|
|
29
|
+
};
|
|
30
|
+
const seedance = (name) => ({
|
|
31
|
+
capability: { module: SEEDANCE, name }, result: "video", routes: [{ model: name }], fields: seedanceFields,
|
|
32
|
+
});
|
|
33
|
+
const pixverse = (name) => ({
|
|
34
|
+
capability: { module: PIXVERSE, name }, result: "video", routes: [{ model: name }],
|
|
35
|
+
fields: {
|
|
36
|
+
prompt: { as: "value", field: "prompt" },
|
|
37
|
+
firstFrame: { as: "url", field: "image_url" },
|
|
38
|
+
lastFrame: { as: "url", field: "end_image_url" },
|
|
39
|
+
referenceImage: { as: "urlArray", field: "reference_image_urls" },
|
|
40
|
+
...(name === "pixverse-v6" ? {
|
|
41
|
+
referenceVideo: { as: "urlArray", field: "reference_video_urls" },
|
|
42
|
+
multiClip: { as: "value", field: "multi_clip" },
|
|
43
|
+
seed: { as: "value", field: "seed" },
|
|
44
|
+
} : {}),
|
|
45
|
+
duration: { as: "value", field: "duration" },
|
|
46
|
+
// PixVerse's quality band is Sume's resolution.
|
|
47
|
+
quality: { as: "value", field: "resolution" },
|
|
48
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
49
|
+
generateAudio: { as: "value", field: "generate_audio" },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
export const sumeMappings = [
|
|
53
|
+
seedance("seedance-2"),
|
|
54
|
+
seedance("seedance-2-fast"),
|
|
55
|
+
seedance("seedance-2-mini"),
|
|
56
|
+
seedance("seedance-2.5"),
|
|
57
|
+
{
|
|
58
|
+
capability: { module: MINIMAX, name: "minimax-h3" }, result: "video",
|
|
59
|
+
routes: [{ model: "minimax-h3" }],
|
|
60
|
+
fields: {
|
|
61
|
+
prompt: { as: "value", field: "prompt" },
|
|
62
|
+
firstFrame: { as: "url", field: "image_url" },
|
|
63
|
+
lastFrame: { as: "url", field: "end_image_url" },
|
|
64
|
+
referenceImage: { as: "urlArray", field: "reference_image_urls" },
|
|
65
|
+
referenceVideo: { as: "urlArray", field: "reference_video_urls" },
|
|
66
|
+
referenceAudio: { as: "urlArray", field: "reference_audio_urls" },
|
|
67
|
+
duration: { as: "value", field: "duration" },
|
|
68
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
69
|
+
resolution: { as: "value", field: "resolution" },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
capability: { module: GROK, name: "grok-imagine-video-1.5-preview" }, result: "video",
|
|
74
|
+
routes: [{ model: "grok-imagine-video-1.5" }],
|
|
75
|
+
fields: {
|
|
76
|
+
prompt: { as: "value", field: "prompt" },
|
|
77
|
+
images: { as: "urlArray", field: "images" },
|
|
78
|
+
duration: { as: "value", field: "duration" },
|
|
79
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
80
|
+
resolution: { as: "value", field: "resolution" },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
pixverse("pixverse-v6"),
|
|
84
|
+
pixverse("pixverse-c1"),
|
|
85
|
+
{
|
|
86
|
+
capability: { module: GPT_IMAGE, name: "gpt-image-2" }, result: "image",
|
|
87
|
+
routes: [{ model: "openai/gpt-image-2" }],
|
|
88
|
+
fields: {
|
|
89
|
+
prompt: { as: "value", field: "prompt" },
|
|
90
|
+
images: { as: "urlArray", field: "images" },
|
|
91
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
92
|
+
resolution: { as: "value", field: "resolution" },
|
|
93
|
+
background: { as: "value", field: "background" },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
// Sume serves Nano Banana 2 as its successor, Nano Banana 2.1.
|
|
97
|
+
...[["nano-banana-2", "google/nano-banana-2.1"], ["nano-banana-pro", "google/nano-banana-pro"]].map(([name, model]) => ({
|
|
98
|
+
capability: { module: NANO_BANANA, name }, result: "image",
|
|
99
|
+
routes: [{ model }],
|
|
100
|
+
fields: {
|
|
101
|
+
prompt: { as: "value", field: "prompt" },
|
|
102
|
+
images: { as: "urlArray", field: "images" },
|
|
103
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
104
|
+
resolution: { as: "value", field: "resolution" },
|
|
105
|
+
outputFormat: { as: "value", field: "output_format" },
|
|
106
|
+
},
|
|
107
|
+
})),
|
|
108
|
+
{
|
|
109
|
+
capability: { module: SEEDREAM, name: "seedream-5-lite" }, result: "image",
|
|
110
|
+
routes: [{ model: "bytedance-seed/seedream-5-lite" }],
|
|
111
|
+
fields: {
|
|
112
|
+
prompt: { as: "value", field: "prompt" },
|
|
113
|
+
images: { as: "urlArray", field: "images" },
|
|
114
|
+
aspectRatio: { as: "value", field: "aspect_ratio" },
|
|
115
|
+
quality: { as: "value", field: "quality" },
|
|
116
|
+
outputFormat: { as: "value", field: "output_format" },
|
|
117
|
+
nsfwCheck: { as: "value", field: "nsfw_check" },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CredentialRef, EndpointActionLimits, ResourceStore } from "@hypit/hypit/endpoint";
|
|
2
|
+
import type { BlobRef } from "@hypit/hypit/protocol";
|
|
3
|
+
export declare const sumeProviderModuleRef: {
|
|
4
|
+
readonly name: "@hypit/provider-sume";
|
|
5
|
+
readonly version: "1";
|
|
6
|
+
};
|
|
7
|
+
export type CreateSumeProviderOptions = {
|
|
8
|
+
readonly instance?: string;
|
|
9
|
+
readonly pool?: string;
|
|
10
|
+
readonly baseUrl?: string;
|
|
11
|
+
readonly apiKey?: CredentialRef;
|
|
12
|
+
readonly defaultConcurrency?: number;
|
|
13
|
+
readonly actionLimits?: EndpointActionLimits;
|
|
14
|
+
readonly pollIntervalMs?: number;
|
|
15
|
+
readonly requestTimeoutMs?: number;
|
|
16
|
+
readonly operationTimeoutMs?: number;
|
|
17
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
18
|
+
/** Publish a referenced Resource at a URL the service can fetch; replaces Sume's upload URL. */
|
|
19
|
+
readonly publicAssetUrl?: (artifact: BlobRef, artifacts: ResourceStore, fields?: Readonly<Record<string, string | number | boolean>>) => Promise<string>;
|
|
20
|
+
};
|
|
21
|
+
export declare function createSumeProvider(options?: CreateSumeProviderOptions): import("@hypit/hypit/endpoint").EndpointInstance;
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { EndpointServiceError, credentialRef, defineEndpoint, wakeAfter } from "@hypit/hypit/endpoint";
|
|
2
|
+
import { EndpointHttpError, EndpointResponseError, EndpointTransportError, withRequestDeadline } from "@hypit/hypit/endpoint/http";
|
|
3
|
+
import { canonicalize } from "@hypit/hypit/protocol";
|
|
4
|
+
import { sumeRouteForCapability, sumeRoutes } from "./routes.js";
|
|
5
|
+
import { SumeHttpError, SumeServiceError, sumeJobFailure } from "./errors.js";
|
|
6
|
+
export const sumeProviderModuleRef = { name: "@hypit/provider-sume", version: "1" };
|
|
7
|
+
const extensions = {
|
|
8
|
+
"image/png": "png", "image/jpeg": "jpg", "image/webp": "webp",
|
|
9
|
+
"audio/mpeg": "mp3", "audio/wav": "wav", "audio/x-wav": "wav", "audio/aac": "aac", "audio/mp4": "m4a",
|
|
10
|
+
"video/mp4": "mp4", "video/quicktime": "mov", "video/webm": "webm",
|
|
11
|
+
};
|
|
12
|
+
function assert(condition, message) { if (!condition)
|
|
13
|
+
throw new Error(message); }
|
|
14
|
+
function object(value, subject) {
|
|
15
|
+
assert(value !== null && typeof value === "object" && !Array.isArray(value), `${subject} must be an object`);
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function apiBaseUrl(value) {
|
|
19
|
+
let trimmed = value.trim();
|
|
20
|
+
while (trimmed.endsWith("/"))
|
|
21
|
+
trimmed = trimmed.slice(0, -1);
|
|
22
|
+
assert(trimmed.length > 0, "Sume base URL is empty");
|
|
23
|
+
return trimmed;
|
|
24
|
+
}
|
|
25
|
+
function apiKey(credentials) {
|
|
26
|
+
const value = credentials.apiKey?.secret;
|
|
27
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
28
|
+
throw new SumeServiceError("SUME_CREDENTIAL_UNAVAILABLE", "Sume apiKey credential is unavailable; store a Sume API key for this Endpoint");
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function httpsUrl(value, subject) {
|
|
33
|
+
assert(typeof value === "string" && /^https:\/\//u.test(value), `${subject} has no HTTPS URL`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function failure(error) {
|
|
37
|
+
if (!(error instanceof EndpointServiceError || error instanceof EndpointHttpError
|
|
38
|
+
|| error instanceof EndpointTransportError || error instanceof EndpointResponseError))
|
|
39
|
+
throw error;
|
|
40
|
+
const code = error instanceof EndpointServiceError || error instanceof EndpointHttpError ? error.code : "SUME_ERROR";
|
|
41
|
+
return { status: "failed", failure: { code, message: error.message } };
|
|
42
|
+
}
|
|
43
|
+
function retrying(error, handle, pollIntervalMs, deadlineAt) {
|
|
44
|
+
const delay = error instanceof EndpointTransportError ? pollIntervalMs
|
|
45
|
+
: error instanceof EndpointHttpError && (error.status === 429 || error.status >= 500)
|
|
46
|
+
? error.retryAfterMs ?? pollIntervalMs : undefined;
|
|
47
|
+
if (delay === undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
return { status: "pending", handle, wakeAt: Math.min(Date.now() + delay, deadlineAt ?? Number.MAX_SAFE_INTEGER), progress: { phase: "retrying" } };
|
|
50
|
+
}
|
|
51
|
+
function unknownSubmission(error) {
|
|
52
|
+
return error instanceof EndpointTransportError ? { status: "failed", failure: {
|
|
53
|
+
code: "SUME_SUBMISSION_UNKNOWN",
|
|
54
|
+
message: `Sume submission transport failed; remote outcome is unknown: ${error.message}`,
|
|
55
|
+
} } : undefined;
|
|
56
|
+
}
|
|
57
|
+
class SumeClient {
|
|
58
|
+
baseUrl;
|
|
59
|
+
timeout;
|
|
60
|
+
fetcher;
|
|
61
|
+
constructor(baseUrl, timeout, fetcher) {
|
|
62
|
+
this.baseUrl = baseUrl;
|
|
63
|
+
this.timeout = timeout;
|
|
64
|
+
this.fetcher = fetcher;
|
|
65
|
+
}
|
|
66
|
+
/** One authenticated call; returns the `data` member of Sume's envelope. */
|
|
67
|
+
async json(path, key, init = {}) {
|
|
68
|
+
return await withRequestDeadline(this.timeout, async ({ signal, wait }) => {
|
|
69
|
+
const response = await wait(this.fetcher(`${this.baseUrl}${path}`, {
|
|
70
|
+
...init, signal, headers: { authorization: `Bearer ${key}`, ...(init.headers ?? {}) },
|
|
71
|
+
}));
|
|
72
|
+
const text = await wait(response.text());
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const input = typeof init.body === "string" ? JSON.parse(init.body) : undefined;
|
|
75
|
+
throw new SumeHttpError(response.status, response, text, {
|
|
76
|
+
method: init.method ?? "GET", path, ...(typeof input?.model === "string" ? { model: input.model } : {}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
let body;
|
|
80
|
+
try {
|
|
81
|
+
body = text.length === 0 ? {} : JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new EndpointResponseError(`Sume returned invalid JSON (${response.status})`);
|
|
85
|
+
}
|
|
86
|
+
return object(object(body, "Sume response").data, "Sume response data");
|
|
87
|
+
}, () => new EndpointTransportError("Sume request timed out", { timeout: true }));
|
|
88
|
+
}
|
|
89
|
+
/** Ask for an upload URL, PUT the Resource there and return the public HTTPS URL a job reads. */
|
|
90
|
+
async upload(artifact, resources, key) {
|
|
91
|
+
const kind = artifact.mediaType.split("/", 1)[0] ?? "";
|
|
92
|
+
assert(kind === "image" || kind === "video" || kind === "audio", `Sume accepts image, audio and video references, not ${artifact.mediaType}`);
|
|
93
|
+
const bytes = await resources.get(artifact.resource);
|
|
94
|
+
assert(bytes !== undefined && bytes.byteLength === artifact.size, `Reference Resource ${artifact.resource} is unavailable or has changed`);
|
|
95
|
+
const slot = await this.json("/v1/files/generate-upload-url", key, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "content-type": "application/json" },
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
content_type: artifact.mediaType,
|
|
100
|
+
size_bytes: artifact.size,
|
|
101
|
+
filename: `${artifact.resource}.${extensions[artifact.mediaType] ?? "bin"}`,
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
const uploadUrl = httpsUrl(slot.upload_url, "Sume upload slot");
|
|
105
|
+
const headers = slot.upload_headers === undefined ? {} : object(slot.upload_headers, "Sume upload headers");
|
|
106
|
+
await withRequestDeadline(this.timeout, async ({ signal, wait }) => {
|
|
107
|
+
const response = await wait(this.fetcher(uploadUrl, {
|
|
108
|
+
method: "PUT", signal, headers: { "content-type": artifact.mediaType, ...headers }, body: new Uint8Array(bytes),
|
|
109
|
+
}));
|
|
110
|
+
// The upload URL is presigned; its path is a credential and stays out of the error.
|
|
111
|
+
if (!response.ok)
|
|
112
|
+
throw new SumeHttpError(response.status, response, "", { method: "PUT", path: "[upload-url]" });
|
|
113
|
+
await wait(response.arrayBuffer());
|
|
114
|
+
}, () => new EndpointTransportError("Sume upload timed out", { timeout: true }));
|
|
115
|
+
return httpsUrl(slot.public_url, "Sume upload slot");
|
|
116
|
+
}
|
|
117
|
+
async download(url) {
|
|
118
|
+
return await withRequestDeadline(this.timeout, async ({ signal, wait }) => {
|
|
119
|
+
const response = await wait(this.fetcher(url, { signal }));
|
|
120
|
+
if (!response.ok)
|
|
121
|
+
throw new SumeHttpError(response.status, response, "", { method: "GET", path: "[result-url]" });
|
|
122
|
+
return { bytes: new Uint8Array(await wait(response.arrayBuffer())), mediaType: response.headers.get("content-type")?.split(";", 1)[0] ?? "application/octet-stream" };
|
|
123
|
+
}, () => new EndpointTransportError("Sume download timed out", { timeout: true }));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function resolverFor(client, context, publicAssetUrl) {
|
|
127
|
+
const resolved = new Map();
|
|
128
|
+
return (artifact, fields) => {
|
|
129
|
+
const existing = resolved.get(artifact.resource);
|
|
130
|
+
if (existing !== undefined)
|
|
131
|
+
return existing;
|
|
132
|
+
const promise = publicAssetUrl === undefined
|
|
133
|
+
? client.upload(artifact, context.resources, apiKey(context.credentials))
|
|
134
|
+
: publicAssetUrl(artifact, context.resources, fields);
|
|
135
|
+
resolved.set(artifact.resource, promise);
|
|
136
|
+
return promise;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Job statuses before a terminal one. */
|
|
140
|
+
const pendingStatuses = ["queued", "processing"];
|
|
141
|
+
function endpoint(client, pollIntervalMs, maxOperationMs, publicAssetUrl) {
|
|
142
|
+
return {
|
|
143
|
+
async start(context) {
|
|
144
|
+
try {
|
|
145
|
+
const route = sumeRouteForCapability(context.need.capability);
|
|
146
|
+
assert(route !== undefined, "Sume does not implement this exact capability");
|
|
147
|
+
const request = route.prepare(context.need.constraints);
|
|
148
|
+
await context.reportProgress?.({ phase: `Preparing Sume request: ${request.model}` });
|
|
149
|
+
const body = await request.compile(resolverFor(client, context, publicAssetUrl));
|
|
150
|
+
await context.reportProgress?.({ phase: `Submitting Sume request: ${request.model}` });
|
|
151
|
+
let response;
|
|
152
|
+
try {
|
|
153
|
+
response = await client.json(request.media === "image" ? "/v1/images" : "/v1/video-router/generate", apiKey(context.credentials), {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "content-type": "application/json", "idempotency-key": context.operation },
|
|
156
|
+
body: JSON.stringify(body),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const unknown = unknownSubmission(error);
|
|
161
|
+
if (unknown !== undefined)
|
|
162
|
+
return unknown;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
const job = response.job === undefined ? undefined : object(response.job, "Sume job");
|
|
166
|
+
const jobId = job?.id ?? response.request_id;
|
|
167
|
+
if (typeof jobId !== "string" || jobId.length === 0)
|
|
168
|
+
throw new EndpointResponseError("Sume response has no job id");
|
|
169
|
+
const handle = { contract: "hypit.sume-operation@1", jobId, route: route.key, startedAt: Date.now() };
|
|
170
|
+
const receipt = { id: jobId };
|
|
171
|
+
await context.checkpoint?.({ handle: canonicalize(handle), receipt });
|
|
172
|
+
return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: "submitted" }), receipt };
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return failure(error);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
async poll(context) {
|
|
179
|
+
let operationDeadlineAt;
|
|
180
|
+
try {
|
|
181
|
+
const handle = object(context.handle, "Sume handle");
|
|
182
|
+
const route = sumeRouteForCapability(context.need.capability);
|
|
183
|
+
assert(route !== undefined && handle.contract === "hypit.sume-operation@1" && handle.route === route.key, "Sume handle is invalid");
|
|
184
|
+
const receipt = { id: handle.jobId };
|
|
185
|
+
operationDeadlineAt = handle.startedAt + maxOperationMs;
|
|
186
|
+
if (Date.now() >= operationDeadlineAt) {
|
|
187
|
+
return { status: "failed", receipt, failure: { code: "SUME_OPERATION_TIMEOUT", message: `Sume job ${handle.jobId} exceeded this Provider's operationTimeoutMs (${maxOperationMs}); remote outcome is unknown` } };
|
|
188
|
+
}
|
|
189
|
+
const key = apiKey(context.credentials);
|
|
190
|
+
const id = encodeURIComponent(handle.jobId);
|
|
191
|
+
const state = await client.json(`/v1/jobs/${id}/status`, key);
|
|
192
|
+
const status = String(state.sume_status);
|
|
193
|
+
if (pendingStatuses.includes(status)) {
|
|
194
|
+
return { ...wakeAfter(canonicalize(handle), pollIntervalMs, Date.now(), { phase: status }), receipt };
|
|
195
|
+
}
|
|
196
|
+
const job = state.job === undefined || state.job === null ? undefined : object(state.job, "Sume job");
|
|
197
|
+
const rejected = sumeJobFailure(status, job, handle.jobId);
|
|
198
|
+
if (rejected !== undefined)
|
|
199
|
+
return { ...failure(rejected), receipt };
|
|
200
|
+
if (status !== "completed")
|
|
201
|
+
throw new EndpointResponseError(`Sume returned unknown job status ${status}`);
|
|
202
|
+
const result = object((await client.json(`/v1/jobs/${id}/result`, key)).result, "Sume job result");
|
|
203
|
+
const artifacts = Array.isArray(result.artifacts) ? result.artifacts.map((item, index) => object(item, `Sume artifact ${index + 1}`)) : [];
|
|
204
|
+
const media = artifacts.filter((item) => item.type === route.result);
|
|
205
|
+
if (media.length === 0)
|
|
206
|
+
throw new EndpointResponseError(`Sume job completed without ${route.result} artifacts`);
|
|
207
|
+
const urls = media.map((item, index) => httpsUrl(item.url, `Sume ${route.result} artifact ${index + 1}`));
|
|
208
|
+
return { status: "ready", handle: canonicalize({ ...handle, urls }), receipt };
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return retrying(error, context.handle, pollIntervalMs, operationDeadlineAt) ?? failure(error);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
async collect(context) {
|
|
215
|
+
try {
|
|
216
|
+
const handle = object(context.handle, "Sume handle");
|
|
217
|
+
const route = sumeRouteForCapability(context.need.capability);
|
|
218
|
+
assert(route !== undefined && handle.route === route.key && Array.isArray(handle.urls), "Sume collection route differs");
|
|
219
|
+
await context.reportProgress?.({ phase: "Receiving generated files" });
|
|
220
|
+
const blobs = [];
|
|
221
|
+
for (const url of handle.urls) {
|
|
222
|
+
const downloaded = await client.download(url);
|
|
223
|
+
blobs.push(await context.resources.put(downloaded.bytes, downloaded.mediaType));
|
|
224
|
+
}
|
|
225
|
+
return { status: "completed", result: { value: route.packageResult(blobs) }, receipt: { id: handle.jobId } };
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
return retrying(error, context.handle, pollIntervalMs) ?? failure(error);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
export function createSumeProvider(options = {}) {
|
|
234
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 300_000;
|
|
235
|
+
const operationTimeoutMs = options.operationTimeoutMs ?? 30 * 60_000;
|
|
236
|
+
for (const [name, value] of Object.entries({ requestTimeoutMs, operationTimeoutMs })) {
|
|
237
|
+
assert(Number.isSafeInteger(value) && value > 0, `Sume ${name} must be a positive integer`);
|
|
238
|
+
}
|
|
239
|
+
const client = new SumeClient(apiBaseUrl(options.baseUrl ?? "https://api.sume.com"), requestTimeoutMs, options.fetch ?? globalThis.fetch);
|
|
240
|
+
const asyncEndpoint = endpoint(client, options.pollIntervalMs ?? 5_000, operationTimeoutMs, options.publicAssetUrl);
|
|
241
|
+
return defineEndpoint({
|
|
242
|
+
instance: options.instance ?? "sume.default", pool: options.pool ?? options.instance ?? "sume.default",
|
|
243
|
+
pricing: { kind: "page", url: "https://www.sume.com/pricing/api" },
|
|
244
|
+
credentials: { apiKey: options.apiKey ?? credentialRef("local", "sume.api-key") },
|
|
245
|
+
credentialInputs: { apiKey: { label: "Sume API key" } },
|
|
246
|
+
defaultConcurrency: options.defaultConcurrency ?? 2,
|
|
247
|
+
...(options.actionLimits === undefined ? {} : { actionLimits: options.actionLimits }),
|
|
248
|
+
capabilities: sumeRoutes.map((route) => ({
|
|
249
|
+
capability: route.capability, returns: route.returns, lifecycle: "asynchronous", endpoint: asyncEndpoint, capacity: route.capability.name, supports: route.supports,
|
|
250
|
+
})),
|
|
251
|
+
});
|
|
252
|
+
}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { GenerationArtifactUrlResolver, GenerationWireMapping } from "@hypit/generation";
|
|
2
|
+
import type { BlobRef, CanonicalValue, CapabilityRef, StoredValue, TypeRef } from "@hypit/hypit/protocol";
|
|
3
|
+
import type { EndpointRequest, EndpointSupport } from "@hypit/hypit/endpoint";
|
|
4
|
+
export type SumePreparedRequest = {
|
|
5
|
+
readonly model: string;
|
|
6
|
+
readonly media: "image" | "video";
|
|
7
|
+
readonly compile: (resolve: GenerationArtifactUrlResolver) => Promise<Record<string, unknown>>;
|
|
8
|
+
};
|
|
9
|
+
export type SumeRoute = GenerationWireMapping & {
|
|
10
|
+
readonly key: string;
|
|
11
|
+
readonly returns: TypeRef;
|
|
12
|
+
readonly supports: (request: EndpointRequest) => EndpointSupport;
|
|
13
|
+
readonly prepare: (constraints: CanonicalValue) => SumePreparedRequest;
|
|
14
|
+
readonly packageResult: (artifacts: readonly BlobRef[]) => StoredValue;
|
|
15
|
+
};
|
|
16
|
+
export declare const sumeRoutes: readonly SumeRoute[];
|
|
17
|
+
export declare function sumeRouteForCapability(capability: CapabilityRef): SumeRoute | undefined;
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { compileWireRequest, generationTypes, sealGeneratedImageSet, sealGeneratedVideoSet, selectWireModelForRequest, } from "@hypit/generation";
|
|
2
|
+
import { canonicalize } from "@hypit/hypit/protocol";
|
|
3
|
+
import { sumeMappings } from "./mapping.js";
|
|
4
|
+
function scalar(request, port) {
|
|
5
|
+
const value = request.ports[port]?.[0];
|
|
6
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
function count(request, port) {
|
|
9
|
+
return request.ports[port]?.length ?? 0;
|
|
10
|
+
}
|
|
11
|
+
/** Ratios the model packages offer that the Sume catalog row for the model does not list. */
|
|
12
|
+
const UNLISTED_RATIOS = {
|
|
13
|
+
"grok-imagine-video-1.5-preview": ["2:3", "3:2"],
|
|
14
|
+
"pixverse-v6": ["2:3", "3:2", "auto"],
|
|
15
|
+
"pixverse-c1": ["2:3", "3:2"],
|
|
16
|
+
"gpt-image-2": ["3:2", "2:3", "2:1", "1:2", "3:1", "1:3", "21:9", "9:21"],
|
|
17
|
+
"seedream-5-lite": ["21:9"],
|
|
18
|
+
};
|
|
19
|
+
/** Sume's per-model input ranges, checked before any reference is uploaded. */
|
|
20
|
+
function rejection(mapping, request) {
|
|
21
|
+
const { name } = mapping.capability;
|
|
22
|
+
const model = selectWireModelForRequest(mapping, request);
|
|
23
|
+
const ratio = scalar(request, "aspectRatio");
|
|
24
|
+
if (ratio !== undefined && UNLISTED_RATIOS[name]?.includes(String(ratio)) === true) {
|
|
25
|
+
return `Sume ${model} does not render aspect ratio ${String(ratio)}`;
|
|
26
|
+
}
|
|
27
|
+
if (mapping.capability.module.name === "@hypit/seedance") {
|
|
28
|
+
if (scalar(request, "webSearch") === true)
|
|
29
|
+
return `Sume ${model} has no web_search field`;
|
|
30
|
+
if (name === "seedance-2.5" && scalar(request, "duration") === -1) {
|
|
31
|
+
return "Sume seedance-2.5 takes a duration of 4 to 30 seconds, not automatic";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (name === "minimax-h3" && scalar(request, "duration") === 4) {
|
|
35
|
+
return "Sume minimax-h3 takes a duration of 5 to 15 seconds";
|
|
36
|
+
}
|
|
37
|
+
if (name === "grok-imagine-video-1.5-preview") {
|
|
38
|
+
if (count(request, "images") > 1)
|
|
39
|
+
return "Sume grok-imagine-video-1.5 takes a prompt alone or one first-frame image";
|
|
40
|
+
if (ratio === "auto" && count(request, "images") === 0) {
|
|
41
|
+
return "Sume grok-imagine-video-1.5 needs an explicit aspect ratio without a first frame";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (name === "pixverse-v6" || name === "pixverse-c1") {
|
|
45
|
+
if (count(request, "firstFrame") > 0 || count(request, "lastFrame") > 0) {
|
|
46
|
+
return `Sume ${model} takes a prompt or reference images, not frames`;
|
|
47
|
+
}
|
|
48
|
+
if (count(request, "referenceVideo") > 0)
|
|
49
|
+
return `Sume ${model} takes no reference videos`;
|
|
50
|
+
if (scalar(request, "multiClip") === true)
|
|
51
|
+
return `Sume ${model} has no multi-clip field`;
|
|
52
|
+
if (scalar(request, "seed") !== undefined)
|
|
53
|
+
return `Sume ${model} takes no seed`;
|
|
54
|
+
if (scalar(request, "duration") === 1)
|
|
55
|
+
return `Sume ${model} takes a duration of 2 to 15 seconds`;
|
|
56
|
+
}
|
|
57
|
+
if (name === "gpt-image-2") {
|
|
58
|
+
if (scalar(request, "background") !== undefined)
|
|
59
|
+
return "Sume openai/gpt-image-2 has no background field";
|
|
60
|
+
if (count(request, "images") > 10)
|
|
61
|
+
return "Sume openai/gpt-image-2 accepts up to ten reference images";
|
|
62
|
+
}
|
|
63
|
+
if (name === "nano-banana-2" && count(request, "images") > 10) {
|
|
64
|
+
return "Sume google/nano-banana-2.1 accepts up to ten reference images";
|
|
65
|
+
}
|
|
66
|
+
if (name === "seedream-5-lite") {
|
|
67
|
+
if (scalar(request, "quality") !== "basic") {
|
|
68
|
+
return "Sume bytedance-seed/seedream-5-lite renders at 2K only, which is quality basic";
|
|
69
|
+
}
|
|
70
|
+
if (scalar(request, "nsfwCheck") === false) {
|
|
71
|
+
return "Sume bytedance-seed/seedream-5-lite has no switch to turn its content check off";
|
|
72
|
+
}
|
|
73
|
+
if (count(request, "images") > 10)
|
|
74
|
+
return "Sume bytedance-seed/seedream-5-lite accepts up to ten reference images";
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/** Fields the model package states that Sume names differently, spells differently or omits. */
|
|
79
|
+
function normalize(mapping, input) {
|
|
80
|
+
const { name } = mapping.capability;
|
|
81
|
+
if (input.web_search === false)
|
|
82
|
+
delete input.web_search;
|
|
83
|
+
// MiniMax spells its native tier 768P; Sume's catalog lists 768p.
|
|
84
|
+
if (name === "minimax-h3" && input.resolution === "768P")
|
|
85
|
+
input.resolution = "768p";
|
|
86
|
+
if (name === "grok-imagine-video-1.5-preview") {
|
|
87
|
+
const images = input.images;
|
|
88
|
+
delete input.images;
|
|
89
|
+
// A first frame keeps its own shape, and Sume applies aspect_ratio to text-to-video only.
|
|
90
|
+
if (images !== undefined && images.length > 0) {
|
|
91
|
+
input.image_url = images[0];
|
|
92
|
+
delete input.aspect_ratio;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (name === "pixverse-v6" && input.multi_clip === false)
|
|
96
|
+
delete input.multi_clip;
|
|
97
|
+
if (mapping.result === "image") {
|
|
98
|
+
const images = input.images;
|
|
99
|
+
delete input.images;
|
|
100
|
+
if (images !== undefined && images.length > 0) {
|
|
101
|
+
input.input_references = images.map((url) => ({ type: "image_url", image_url: { url } }));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (mapping.capability.module.name === "@hypit/nano-banana" && input.output_format === "jpg")
|
|
105
|
+
input.output_format = "jpeg";
|
|
106
|
+
if (name === "seedream-5-lite") {
|
|
107
|
+
delete input.quality;
|
|
108
|
+
delete input.nsfw_check;
|
|
109
|
+
}
|
|
110
|
+
return input;
|
|
111
|
+
}
|
|
112
|
+
function capabilityKey(capability) {
|
|
113
|
+
return `${capability.module.name}@${capability.module.version}#${capability.name}`;
|
|
114
|
+
}
|
|
115
|
+
export const sumeRoutes = sumeMappings.map((mapping) => ({
|
|
116
|
+
...mapping,
|
|
117
|
+
key: capabilityKey(mapping.capability),
|
|
118
|
+
returns: mapping.result === "image" ? generationTypes.imageSet : generationTypes.videoSet,
|
|
119
|
+
supports: (request) => {
|
|
120
|
+
const reason = rejection(mapping, request.constraints);
|
|
121
|
+
return reason === undefined ? { status: "supported" } : { status: "unsupported", reason };
|
|
122
|
+
},
|
|
123
|
+
prepare: (constraints) => {
|
|
124
|
+
const request = constraints;
|
|
125
|
+
const reason = rejection(mapping, request);
|
|
126
|
+
if (reason !== undefined)
|
|
127
|
+
throw new Error(reason);
|
|
128
|
+
const model = selectWireModelForRequest(mapping, request);
|
|
129
|
+
return {
|
|
130
|
+
model,
|
|
131
|
+
media: mapping.result === "image" ? "image" : "video",
|
|
132
|
+
compile: async (resolve) => ({
|
|
133
|
+
model,
|
|
134
|
+
...normalize(mapping, (await compileWireRequest(mapping, request, resolve)).input),
|
|
135
|
+
mode: "async",
|
|
136
|
+
}),
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
packageResult: (artifacts) => ({
|
|
140
|
+
kind: "inline",
|
|
141
|
+
value: canonicalize(mapping.result === "image"
|
|
142
|
+
? sealGeneratedImageSet({ images: artifacts })
|
|
143
|
+
: sealGeneratedVideoSet({ videos: artifacts })),
|
|
144
|
+
}),
|
|
145
|
+
}));
|
|
146
|
+
const byCapability = new Map(sumeRoutes.map((route) => [route.key, route]));
|
|
147
|
+
export function sumeRouteForCapability(capability) {
|
|
148
|
+
return byCapability.get(capabilityKey(capability));
|
|
149
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypit/provider-sume",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"hypit": {
|
|
15
|
+
"activation": "./dist/activation.js"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@hypit/hypit": "^0.3.1",
|
|
19
|
+
"@hypit/runtime-local": "^0.1.1"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@hypit/generation": "^0.1.0"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/hypit-ai/hypit.git"
|
|
27
|
+
}
|
|
28
|
+
}
|