focalapi-cli 0.2.2 → 0.3.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/CHANGELOG.md +11 -0
- package/README.md +5 -2
- package/dist/cli.js +205 -60
- package/package.json +1 -1
- package/skills/focalapi/SKILL.md +2 -1
- package/skills/focalapi-gen/SKILL.md +56 -50
- package/skills/focalapi-models/SKILL.md +55 -55
- package/skills/focalapi-task/SKILL.md +7 -3
- package/skills/focalapi-usage/SKILL.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 - 2026-08-18
|
|
4
|
+
|
|
5
|
+
- Added `task cancel` for queued tasks through `DELETE /v1/video/generations/{task_id}`, including the 409 `task_already_running` / `task_already_finished` and 502 `task_cancel_failed` contract with actionable hints.
|
|
6
|
+
- Added `gen video --first-frame <url>` for image-to-video and made `--image` the reference-media channel; Grok 1.5 reference-to-video is capped at 720p and 7 images locally, the legacy Grok video model rejects references outright, and the two flags are mutually exclusive.
|
|
7
|
+
- Accepted the new Seedance 2.5 1080p tier and wired per-model reference-image caps (Seedance, Omni, Kling, Vidu, LTX, FLUX).
|
|
8
|
+
- Aligned Gemini image validation: `gemini-2.5-flash-image` accepts at most 1 `inlineData` reference, `gemini-3.1-flash-image` gains the 15-value ratio surface and `thinkingLevel`/`temperature`/`topP`, and seeds are capped at `Number.MAX_SAFE_INTEGER`.
|
|
9
|
+
- Corrected `seedream-4-0-250828` default size to `2k`, lowered the Vidu seed ceiling to 2147483647, and capped FLUX 3 `safety_tolerance` at 2 whenever images are attached.
|
|
10
|
+
- Normalized task statuses `cancelled`, `expired`, and the `queued_*` family, with refund-aware messaging for expired tasks.
|
|
11
|
+
- Mapped the platform's 503 `capacity_exhausted` signal to a stable retryable error code with a retry hint instead of a generic server error.
|
|
12
|
+
- Refreshed the bundled Agent Skills and README with the cancel workflow, capacity retry guidance, and the Grok video mode contract.
|
|
13
|
+
|
|
3
14
|
## 0.2.2 - 2026-08-14
|
|
4
15
|
|
|
5
16
|
- Removed the retired Veo 3.1 preview models from automatic video selection and local validation.
|
package/README.md
CHANGED
|
@@ -50,6 +50,9 @@ focalapi gen video "Ocean waves hitting rocks, cinematic" --no-wait -o ./out --j
|
|
|
50
50
|
# Continue from next_command in the generation response without resubmitting the task.
|
|
51
51
|
focalapi task status <task-id> --json
|
|
52
52
|
focalapi task download <task-id> -o ./out --json
|
|
53
|
+
|
|
54
|
+
# Stop a task that is still queued; cancelled tasks are refunded automatically.
|
|
55
|
+
focalapi task cancel <task-id> --json
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
When the user specifies a model, read its authoritative contract first:
|
|
@@ -68,7 +71,7 @@ focalapi models resolve video --json
|
|
|
68
71
|
|
|
69
72
|
`resolve` returns the exact `model.id`, verified `endpoint_type`, complete `supported_params`, candidate models, and a `next_command`. When a list summary and detailed contract disagree, the detailed contract is authoritative.
|
|
70
73
|
|
|
71
|
-
The maintained creative defaults are aligned with the current catalog: Seedream 5.0, GPT Image 2, Gemini 3.1 Image, Grok Imagine Image 2.0, Kling Image 3.0, Qwen Image 3.0, and Krea 2 for images; Seedance 2.5, Kling 3.0, Vidu Q3, Gemini Omni Flash, Grok Imagine Video 1.5, LTX 2.5, FLUX 3
|
|
74
|
+
The maintained creative defaults are aligned with the current catalog: Seedream 5.0, GPT Image 2, Gemini 3.1 Image, Grok Imagine Image 2.0, Kling Image 3.0, Qwen Image 3.0, and Krea 2 for images; Seedance 2.5 (480p/720p/1080p), Kling 3.0, Vidu Q3, Gemini Omni Flash, Grok Imagine Video 1.5, LTX 2.5, and FLUX 3 for video. Availability still depends on the current key, so runtime model details always take precedence over this overview.
|
|
72
75
|
|
|
73
76
|
## Agent integration
|
|
74
77
|
|
|
@@ -112,7 +115,7 @@ The fully validated automatic generation paths currently cover images and video,
|
|
|
112
115
|
| Generate images with automatic or explicit model selection | `focalapi gen image` |
|
|
113
116
|
| Generate video with automatic or explicit model selection | `focalapi gen video` |
|
|
114
117
|
| Resolve models and inspect live contracts | `focalapi models resolve/get/search/list` |
|
|
115
|
-
| Check and download asynchronous tasks | `focalapi task status/download` |
|
|
118
|
+
| Check, cancel (queued), and download asynchronous tasks | `focalapi task status/cancel/download` |
|
|
116
119
|
| Sign in and inspect key status | `focalapi auth login/status/logout` |
|
|
117
120
|
| Inspect quota, usage, and diagnostics | `focalapi usage`, `focalapi doctor` |
|
|
118
121
|
| Connect Agent Skills | `focalapi connect` |
|
package/dist/cli.js
CHANGED
|
@@ -37,6 +37,20 @@ var ApiError = class extends Error {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
|
+
var ERROR_HINTS = {
|
|
41
|
+
missing_api_key: "\u672A\u914D\u7F6E API Key\u3002\u8FD0\u884C focalapi auth login --key <sk-...>\uFF0C\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF FOCALAPI_API_KEY\u3002",
|
|
42
|
+
invalid_api_key: "API Key \u65E0\u6548\u3001\u5DF2\u8FC7\u671F\u6216\u88AB\u5220\u9664\u3002\u524D\u5F80 https://focalapi.com/console/token \u68C0\u67E5\u6216\u65B0\u5EFA Key\uFF0C\u7136\u540E\u91CD\u65B0 focalapi auth login\u3002",
|
|
43
|
+
insufficient_quota: "\u8D26\u6237\u989D\u5EA6\u4E0D\u8DB3\u3002\u524D\u5F80 https://focalapi.com/console/topup \u5145\u503C\uFF1B\u53EF\u8FD0\u884C focalapi usage \u67E5\u770B\u5F53\u524D\u989D\u5EA6\u3002",
|
|
44
|
+
rate_limited: "\u8BF7\u6C42\u89E6\u53D1\u9650\u6D41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
|
|
45
|
+
capacity_exhausted: "\u5E73\u53F0\u521B\u4F5C\u5BB9\u91CF\u5DF2\u6EE1\uFF08\u6392\u961F\u51C6\u5165\u8FBE\u5230\u4E0A\u9650\uFF0CHTTP 503\uFF09\u3002\u670D\u52A1\u7AEF\u5EFA\u8BAE\u7EA6 10 \u79D2\u540E\u91CD\u8BD5\uFF1A\u76F4\u63A5\u91CD\u8DD1\u540C\u4E00\u6761\u547D\u4EE4\u5373\u53EF\uFF0C\u65E0\u9700\u4FEE\u6539\u53C2\u6570\u6216\u66F4\u6362\u6A21\u578B\u3002",
|
|
46
|
+
model_not_found: "\u6A21\u578B\u4E0D\u5B58\u5728\u6216\u672A\u5BF9\u4F60\u7684 Key \u5F00\u653E\u3002\u8FD0\u884C focalapi models list \u67E5\u770B\u53EF\u7528\u6A21\u578B\u3002",
|
|
47
|
+
network_error: "\u65E0\u6CD5\u8FDE\u63A5 focalapi \u670D\u52A1\u3002\u68C0\u67E5\u7F51\u7EDC\u4EE3\u7406\u4E0E FOCALAPI_BASE_URL \u914D\u7F6E\uFF1B\u53EF\u8FD0\u884C focalapi doctor \u505A\u94FE\u8DEF\u8BCA\u65AD\u3002",
|
|
48
|
+
timeout: "\u8BF7\u6C42\u8D85\u65F6\u3002\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u8FD0\u884C focalapi doctor \u68C0\u67E5\u94FE\u8DEF\u8D28\u91CF\u3002",
|
|
49
|
+
server_error: "focalapi \u670D\u52A1\u7AEF\u9519\u8BEF\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002\u82E5\u6301\u7EED\u51FA\u73B0\uFF0C\u8BF7\u643A\u5E26\u9519\u8BEF\u4FE1\u606F\u53CD\u9988\u7ED9\u670D\u52A1\u65B9\u3002",
|
|
50
|
+
invalid_request: "\u8BF7\u6C42\u53C2\u6570\u6709\u8BEF\uFF0C\u8BF7\u68C0\u67E5\u547D\u4EE4\u53C2\u6570\u3002",
|
|
51
|
+
authentication_failed: "\u9274\u6743\u5931\u8D25\u3002\u5148\u8FD0\u884C focalapi auth status \u9A8C\u8BC1\u672C\u7AD9 Key\uFF1B\u82E5\u672C\u7AD9 Key \u6709\u6548\uFF0C\u5219\u8BF7\u5C06\u8BF7\u6C42 ID \u63D0\u4F9B\u7ED9\u670D\u52A1\u65B9\u6392\u67E5\u6E20\u9053\u6743\u9650\u3002",
|
|
52
|
+
upstream_auth_failed: "\u4E0A\u6E38\u6E20\u9053\u9274\u6743\u5931\u8D25\uFF0C\u5E76\u4E0D\u8868\u793A\u4F60\u7684 FocalAPI Key \u65E0\u6548\u3002\u5148\u8FD0\u884C focalapi auth status\uFF1B\u82E5\u901A\u8FC7\uFF0C\u8BF7\u5C06\u8BF7\u6C42 ID \u63D0\u4F9B\u7ED9\u670D\u52A1\u65B9\u6392\u67E5\u6E20\u9053\u914D\u7F6E\u3002"
|
|
53
|
+
};
|
|
40
54
|
function refineErrorCode(status, message, opts) {
|
|
41
55
|
const m = message.toLowerCase();
|
|
42
56
|
if (m.includes("quota") || m.includes("\u989D\u5EA6") || m.includes("insufficient")) {
|
|
@@ -104,8 +118,15 @@ function printJson(data) {
|
|
|
104
118
|
function info(message) {
|
|
105
119
|
process.stderr.write(message + "\n");
|
|
106
120
|
}
|
|
107
|
-
function printError(
|
|
108
|
-
if (
|
|
121
|
+
function printError(errInput, opts) {
|
|
122
|
+
if (errInput instanceof ApiError) {
|
|
123
|
+
const err = !errInput.hint && ERROR_HINTS[errInput.code] ? new ApiError(errInput.code, errInput.message, {
|
|
124
|
+
status: errInput.status,
|
|
125
|
+
hint: ERROR_HINTS[errInput.code],
|
|
126
|
+
body: errInput.body,
|
|
127
|
+
upstreamCode: errInput.upstreamCode,
|
|
128
|
+
requestId: errInput.requestId
|
|
129
|
+
}) : errInput;
|
|
109
130
|
if (opts?.json) {
|
|
110
131
|
printJson(err.toJSON());
|
|
111
132
|
} else {
|
|
@@ -127,7 +148,7 @@ function printError(err, opts) {
|
|
|
127
148
|
}
|
|
128
149
|
return;
|
|
129
150
|
}
|
|
130
|
-
const message =
|
|
151
|
+
const message = errInput instanceof Error ? errInput.message : String(errInput);
|
|
131
152
|
if (opts?.json) {
|
|
132
153
|
printJson({ error: { code: "internal_error", message } });
|
|
133
154
|
} else {
|
|
@@ -155,7 +176,7 @@ function displayWidth(s) {
|
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
// src/lib/version.ts
|
|
158
|
-
var VERSION = true ? "0.
|
|
179
|
+
var VERSION = true ? "0.3.0" : "0.0.0-dev";
|
|
159
180
|
|
|
160
181
|
// src/commands/auth.ts
|
|
161
182
|
import { createInterface } from "readline/promises";
|
|
@@ -319,7 +340,10 @@ async function rawRequest(opts) {
|
|
|
319
340
|
if (!res.ok) {
|
|
320
341
|
const text = await res.text().catch(() => "");
|
|
321
342
|
const { message, body, upstreamCode } = extractErrorMessage(text);
|
|
322
|
-
|
|
343
|
+
let code = refineErrorCode(res.status, message, { authFailureIsInvalidApiKey: opts.authFailureIsInvalidApiKey });
|
|
344
|
+
if (upstreamCode === "capacity_exhausted" || res.status === 503 && /capacity/i.test(message)) {
|
|
345
|
+
code = "capacity_exhausted";
|
|
346
|
+
}
|
|
323
347
|
const requestId = res.headers.get("x-request-id") ?? res.headers.get("request-id") ?? res.headers.get("x-requestid") ?? void 0;
|
|
324
348
|
throw new ApiError(code, message, { status: res.status, body, upstreamCode, requestId });
|
|
325
349
|
}
|
|
@@ -485,7 +509,6 @@ var RECOMMENDED_MODELS = {
|
|
|
485
509
|
"grok-imagine-video-1.5",
|
|
486
510
|
"ltx-2-5-fast",
|
|
487
511
|
"flux-3",
|
|
488
|
-
"MiniMax-H3",
|
|
489
512
|
"dreamina-seedance-2-0-260128"
|
|
490
513
|
]
|
|
491
514
|
};
|
|
@@ -895,6 +918,7 @@ import { pipeline as pipeline2 } from "stream/promises";
|
|
|
895
918
|
import { Readable as Readable2 } from "stream";
|
|
896
919
|
|
|
897
920
|
// src/lib/model-capabilities.ts
|
|
921
|
+
var GEMINI_IMAGE_MAX_SEED = 9007199254740991;
|
|
898
922
|
var SEEDREAM_OUTPUT_FORMATS = ["png", "jpeg"];
|
|
899
923
|
var SEEDREAM_OPTIMIZE_PROMPT_MODES = ["auto", "enabled", "disabled"];
|
|
900
924
|
var GROK_IMAGE_ASPECT_RATIOS = [
|
|
@@ -946,7 +970,7 @@ var IMAGE_CONSTRAINTS = {
|
|
|
946
970
|
backgrounds: ["auto", "opaque"]
|
|
947
971
|
},
|
|
948
972
|
"seedream-4-0-250828": {
|
|
949
|
-
defaultSize: "
|
|
973
|
+
defaultSize: "2k",
|
|
950
974
|
sizeTiers: ["1k", "2k", "4k"],
|
|
951
975
|
maxN: 10,
|
|
952
976
|
maxReferenceImages: 10,
|
|
@@ -1087,7 +1111,6 @@ var SEEDANCE_RATIOS = ["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16", "21:9"];
|
|
|
1087
1111
|
var GROK_VIDEO_ASPECT_RATIOS = ["auto", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16"];
|
|
1088
1112
|
var KLING_VIDEO_ASPECT_RATIOS = ["16:9", "9:16", "1:1"];
|
|
1089
1113
|
var VIDU_VIDEO_ASPECT_RATIOS = ["16:9", "9:16", "3:4", "4:3", "1:1"];
|
|
1090
|
-
var MINIMAX_VIDEO_RATIOS = ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"];
|
|
1091
1114
|
var FLUX_VIDEO_RATIOS = ["auto", "21:9", "2:1", "16:9", "4:3", "1:1", "3:4", "9:16"];
|
|
1092
1115
|
var VIDEO_CONSTRAINTS = {
|
|
1093
1116
|
"dreamina-seedance-2-0-260128": {
|
|
@@ -1095,55 +1118,65 @@ var VIDEO_CONSTRAINTS = {
|
|
|
1095
1118
|
ratios: SEEDANCE_RATIOS,
|
|
1096
1119
|
minSeconds: 4,
|
|
1097
1120
|
maxSeconds: 15,
|
|
1098
|
-
|
|
1121
|
+
maxReferenceImages: 9
|
|
1099
1122
|
},
|
|
1100
1123
|
"dreamina-seedance-2-0-fast-260128": {
|
|
1101
1124
|
resolutions: ["480p", "720p"],
|
|
1102
1125
|
ratios: SEEDANCE_RATIOS,
|
|
1103
1126
|
minSeconds: 4,
|
|
1104
1127
|
maxSeconds: 15,
|
|
1105
|
-
|
|
1128
|
+
maxReferenceImages: 9
|
|
1106
1129
|
},
|
|
1107
|
-
"
|
|
1130
|
+
"seed-2-0-mini-260428": {
|
|
1108
1131
|
resolutions: ["480p", "720p"],
|
|
1109
1132
|
ratios: SEEDANCE_RATIOS,
|
|
1110
1133
|
minSeconds: 4,
|
|
1111
1134
|
maxSeconds: 15,
|
|
1112
|
-
|
|
1135
|
+
maxReferenceImages: 9
|
|
1113
1136
|
},
|
|
1114
1137
|
"dreamina-seedance-2-5-260628": {
|
|
1115
|
-
resolutions: ["480p", "720p"],
|
|
1138
|
+
resolutions: ["480p", "720p", "1080p"],
|
|
1116
1139
|
ratios: SEEDANCE_RATIOS,
|
|
1117
1140
|
minSeconds: 4,
|
|
1118
|
-
maxSeconds: 30
|
|
1141
|
+
maxSeconds: 30,
|
|
1142
|
+
maxReferenceImages: 30
|
|
1119
1143
|
},
|
|
1120
1144
|
"gemini-omni-flash-preview": {
|
|
1121
1145
|
resolutions: [],
|
|
1122
1146
|
ratios: ["16:9", "9:16"],
|
|
1123
1147
|
minSeconds: 3,
|
|
1124
|
-
maxSeconds: 10
|
|
1148
|
+
maxSeconds: 10,
|
|
1149
|
+
maxReferenceImages: 14,
|
|
1150
|
+
supportsGenerateAudio: false
|
|
1125
1151
|
},
|
|
1126
1152
|
"grok-imagine-video": {
|
|
1127
|
-
resolutions: ["480p", "720p"
|
|
1153
|
+
resolutions: ["480p", "720p"],
|
|
1128
1154
|
aspectRatios: GROK_VIDEO_ASPECT_RATIOS,
|
|
1129
1155
|
minSeconds: 1,
|
|
1130
1156
|
maxSeconds: 15,
|
|
1131
|
-
|
|
1132
|
-
|
|
1157
|
+
disallowReferences: true,
|
|
1158
|
+
maxFirstFrameImages: 1,
|
|
1159
|
+
supportsGenerateAudio: false,
|
|
1160
|
+
supportsWatermark: false
|
|
1133
1161
|
},
|
|
1134
1162
|
"grok-imagine-video-1.5": {
|
|
1135
1163
|
resolutions: ["480p", "720p", "1080p"],
|
|
1136
1164
|
aspectRatios: GROK_VIDEO_ASPECT_RATIOS,
|
|
1137
1165
|
minSeconds: 1,
|
|
1138
1166
|
maxSeconds: 15,
|
|
1139
|
-
|
|
1140
|
-
|
|
1167
|
+
maxReferenceImages: 7,
|
|
1168
|
+
referenceResolutions: ["480p", "720p"],
|
|
1169
|
+
maxFirstFrameImages: 1,
|
|
1170
|
+
supportsGenerateAudio: false,
|
|
1171
|
+
supportsWatermark: false
|
|
1141
1172
|
},
|
|
1142
1173
|
"kling-3.0": {
|
|
1143
1174
|
resolutions: ["720p", "1080p", "4k"],
|
|
1144
1175
|
aspectRatios: KLING_VIDEO_ASPECT_RATIOS,
|
|
1145
1176
|
minSeconds: 3,
|
|
1146
|
-
maxSeconds: 15
|
|
1177
|
+
maxSeconds: 15,
|
|
1178
|
+
maxReferenceImages: 2,
|
|
1179
|
+
supportsGenerateAudio: true
|
|
1147
1180
|
},
|
|
1148
1181
|
"viduq3-pro": {
|
|
1149
1182
|
resolutions: ["720p", "1080p"],
|
|
@@ -1151,7 +1184,8 @@ var VIDEO_CONSTRAINTS = {
|
|
|
1151
1184
|
minSeconds: 1,
|
|
1152
1185
|
maxSeconds: 16,
|
|
1153
1186
|
supportsSeed: true,
|
|
1154
|
-
maxSeed: 2147483647
|
|
1187
|
+
maxSeed: 2147483647,
|
|
1188
|
+
maxReferenceImages: 2
|
|
1155
1189
|
},
|
|
1156
1190
|
"viduq3-turbo": {
|
|
1157
1191
|
resolutions: ["720p", "1080p"],
|
|
@@ -1159,49 +1193,67 @@ var VIDEO_CONSTRAINTS = {
|
|
|
1159
1193
|
minSeconds: 1,
|
|
1160
1194
|
maxSeconds: 16,
|
|
1161
1195
|
supportsSeed: true,
|
|
1162
|
-
maxSeed: 2147483647
|
|
1163
|
-
|
|
1164
|
-
"MiniMax-H3": {
|
|
1165
|
-
resolutions: ["768p", "2k"],
|
|
1166
|
-
ratios: MINIMAX_VIDEO_RATIOS,
|
|
1167
|
-
minSeconds: 5,
|
|
1168
|
-
maxSeconds: 15
|
|
1196
|
+
maxSeed: 2147483647,
|
|
1197
|
+
maxReferenceImages: 2
|
|
1169
1198
|
},
|
|
1170
1199
|
"ltx-2-5-fast": {
|
|
1171
1200
|
resolutions: ["1280x720", "720x1280", "1920x1080", "1080x1920", "2560x1440", "1440x2560", "3840x2160", "2160x3840"],
|
|
1172
|
-
minSeconds:
|
|
1201
|
+
minSeconds: 6,
|
|
1173
1202
|
maxSeconds: 20,
|
|
1174
|
-
allowedSeconds: [
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1203
|
+
allowedSeconds: [6, 8, 10, 12, 14, 16, 18, 20],
|
|
1204
|
+
allowedFps: [24, 25, 48, 50],
|
|
1205
|
+
maxReferenceImages: 2,
|
|
1206
|
+
supportsGenerateAudio: true
|
|
1178
1207
|
},
|
|
1179
1208
|
"ltx-2-5-pro": {
|
|
1180
1209
|
resolutions: ["1280x720", "720x1280", "1920x1080", "1080x1920"],
|
|
1181
|
-
minSeconds:
|
|
1210
|
+
minSeconds: 6,
|
|
1182
1211
|
maxSeconds: 10,
|
|
1183
|
-
allowedSeconds: [
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1212
|
+
allowedSeconds: [6, 8, 10],
|
|
1213
|
+
allowedFps: [24, 25, 50],
|
|
1214
|
+
maxReferenceImages: 2,
|
|
1215
|
+
supportsGenerateAudio: true
|
|
1187
1216
|
},
|
|
1188
1217
|
"flux-3": {
|
|
1189
1218
|
resolutions: ["hd", "fhd"],
|
|
1190
1219
|
ratios: FLUX_VIDEO_RATIOS,
|
|
1191
1220
|
minSeconds: 5,
|
|
1192
1221
|
maxSeconds: 20,
|
|
1193
|
-
safetyTolerance: { minimum: 0, maximum: 4 }
|
|
1222
|
+
safetyTolerance: { minimum: 0, maximum: 4 },
|
|
1223
|
+
safetyToleranceMaxWithImages: 2,
|
|
1224
|
+
maxReferenceImages: 10
|
|
1194
1225
|
}
|
|
1195
1226
|
};
|
|
1196
1227
|
var COMMON_GEMINI_RATIOS = ["auto", "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
|
|
1228
|
+
var EXTENDED_GEMINI_RATIOS = [...COMMON_GEMINI_RATIOS, "1:4", "4:1", "1:8", "8:1"];
|
|
1197
1229
|
var GEMINI_IMAGE_CONSTRAINTS = {
|
|
1198
|
-
"gemini-2.5-flash-image": {
|
|
1199
|
-
|
|
1200
|
-
|
|
1230
|
+
"gemini-2.5-flash-image": {
|
|
1231
|
+
aspectRatios: COMMON_GEMINI_RATIOS,
|
|
1232
|
+
supportsSampling: false,
|
|
1233
|
+
maxReferenceImages: 1,
|
|
1234
|
+
referenceImagesInlineDataOnly: true,
|
|
1235
|
+
maxSeed: GEMINI_IMAGE_MAX_SEED
|
|
1236
|
+
},
|
|
1237
|
+
"gemini-3-pro-image": {
|
|
1238
|
+
aspectRatios: COMMON_GEMINI_RATIOS,
|
|
1239
|
+
imageSizes: ["1K", "2K", "4K"],
|
|
1240
|
+
supportsSampling: false,
|
|
1241
|
+
maxReferenceImages: 14,
|
|
1242
|
+
maxSeed: GEMINI_IMAGE_MAX_SEED
|
|
1243
|
+
},
|
|
1244
|
+
"gemini-3.1-flash-image": {
|
|
1245
|
+
aspectRatios: EXTENDED_GEMINI_RATIOS,
|
|
1246
|
+
imageSizes: ["1K", "2K", "4K"],
|
|
1247
|
+
supportsSampling: true,
|
|
1248
|
+
maxReferenceImages: 14,
|
|
1249
|
+
maxSeed: GEMINI_IMAGE_MAX_SEED
|
|
1250
|
+
},
|
|
1201
1251
|
"gemini-3.1-flash-lite-image": {
|
|
1202
|
-
aspectRatios:
|
|
1252
|
+
aspectRatios: EXTENDED_GEMINI_RATIOS,
|
|
1203
1253
|
imageSizes: ["1K"],
|
|
1204
|
-
supportsSampling: true
|
|
1254
|
+
supportsSampling: true,
|
|
1255
|
+
maxReferenceImages: 14,
|
|
1256
|
+
maxSeed: GEMINI_IMAGE_MAX_SEED
|
|
1205
1257
|
}
|
|
1206
1258
|
};
|
|
1207
1259
|
function parseSize(size, model) {
|
|
@@ -1316,11 +1368,18 @@ function validateGeminiImageGeneration(model, input) {
|
|
|
1316
1368
|
const supported = constraint.imageSizes?.join(", ") ?? "none";
|
|
1317
1369
|
throw new ApiError("invalid_request", `${model} imageSize must be one of ${supported} (received: ${input.imageSize})`);
|
|
1318
1370
|
}
|
|
1319
|
-
if (input.seed !== void 0 && (!Number.isInteger(input.seed) || input.seed < 0)) {
|
|
1320
|
-
|
|
1371
|
+
if (input.seed !== void 0 && (!Number.isInteger(input.seed) || input.seed < 0 || constraint.maxSeed !== void 0 && input.seed > constraint.maxSeed)) {
|
|
1372
|
+
const maximum = constraint.maxSeed === void 0 ? "" : ` no greater than ${constraint.maxSeed}`;
|
|
1373
|
+
throw new ApiError("invalid_request", `seed must be a non-negative integer${maximum}`);
|
|
1374
|
+
}
|
|
1375
|
+
if (input.referenceImageCount !== void 0 && constraint.maxReferenceImages !== void 0 && input.referenceImageCount > constraint.maxReferenceImages) {
|
|
1376
|
+
throw new ApiError("invalid_request", `${model} supports at most ${constraint.maxReferenceImages} reference image${constraint.maxReferenceImages === 1 ? "" : "s"}`);
|
|
1377
|
+
}
|
|
1378
|
+
if (constraint.referenceImagesInlineDataOnly && input.nonDataUriReferenceCount) {
|
|
1379
|
+
throw new ApiError("invalid_request", `${model} reference images must be base64 data URIs (inlineData); fileUri inputs are rejected by this model`);
|
|
1321
1380
|
}
|
|
1322
1381
|
if (!constraint.supportsSampling && (input.thinkingLevel || input.temperature !== void 0 || input.topP !== void 0)) {
|
|
1323
|
-
throw new ApiError("invalid_request", `${model} supports thinkingLevel, temperature, and topP only on gemini-3.1-flash-lite-image`);
|
|
1382
|
+
throw new ApiError("invalid_request", `${model} supports thinkingLevel, temperature, and topP only on gemini-3.1-flash-image and gemini-3.1-flash-lite-image`);
|
|
1324
1383
|
}
|
|
1325
1384
|
if (input.thinkingLevel && !["MINIMAL", "HIGH"].includes(input.thinkingLevel.toUpperCase())) {
|
|
1326
1385
|
throw new ApiError("invalid_request", "thinkingLevel must be MINIMAL or HIGH");
|
|
@@ -1335,6 +1394,24 @@ function validateGeminiImageGeneration(model, input) {
|
|
|
1335
1394
|
function validateVideoGeneration(model, input) {
|
|
1336
1395
|
const constraint = VIDEO_CONSTRAINTS[model.trim()];
|
|
1337
1396
|
if (!constraint) return;
|
|
1397
|
+
if ((input.imageCount ?? 0) > 0 && (input.firstFrameCount ?? 0) > 0) {
|
|
1398
|
+
throw new ApiError("invalid_request", `${model}: --image and --first-frame are mutually exclusive (reference-to-video vs image-to-video)`);
|
|
1399
|
+
}
|
|
1400
|
+
if (constraint.disallowReferences && (input.imageCount ?? 0) > 0) {
|
|
1401
|
+
throw new ApiError("invalid_request", `${model} supports image-to-video only (single --first-frame image); reference images require grok-imagine-video-1.5`);
|
|
1402
|
+
}
|
|
1403
|
+
if (input.imageCount !== void 0 && constraint.maxReferenceImages !== void 0 && input.imageCount > constraint.maxReferenceImages) {
|
|
1404
|
+
throw new ApiError("invalid_request", `${model} supports at most ${constraint.maxReferenceImages} reference image${constraint.maxReferenceImages === 1 ? "" : "s"}`);
|
|
1405
|
+
}
|
|
1406
|
+
if (input.firstFrameCount !== void 0 && constraint.maxFirstFrameImages !== void 0 && input.firstFrameCount > constraint.maxFirstFrameImages) {
|
|
1407
|
+
throw new ApiError("invalid_request", `${model} image-to-video supports exactly ${constraint.maxFirstFrameImages} starting image${constraint.maxFirstFrameImages === 1 ? "" : "s"} (--first-frame)`);
|
|
1408
|
+
}
|
|
1409
|
+
if (input.generateAudio !== void 0 && constraint.supportsGenerateAudio === false) {
|
|
1410
|
+
throw new ApiError("invalid_request", `${model} does not support generate_audio`);
|
|
1411
|
+
}
|
|
1412
|
+
if (input.watermark !== void 0 && constraint.supportsWatermark === false) {
|
|
1413
|
+
throw new ApiError("invalid_request", `${model} does not support watermark`);
|
|
1414
|
+
}
|
|
1338
1415
|
if (input.seconds !== void 0 && (input.seconds < constraint.minSeconds || input.seconds > constraint.maxSeconds)) {
|
|
1339
1416
|
throw new ApiError("invalid_request", `${model} seconds must be ${constraint.minSeconds}-${constraint.maxSeconds} (received: ${input.seconds})`);
|
|
1340
1417
|
}
|
|
@@ -1344,6 +1421,9 @@ function validateVideoGeneration(model, input) {
|
|
|
1344
1421
|
if (input.resolution && !constraint.resolutions.includes(input.resolution.toLowerCase())) {
|
|
1345
1422
|
throw new ApiError("invalid_request", `${model} resolution must be one of ${constraint.resolutions.join(", ")} (received: ${input.resolution})`);
|
|
1346
1423
|
}
|
|
1424
|
+
if (input.resolution && (input.imageCount ?? 0) > 0 && constraint.referenceResolutions && !constraint.referenceResolutions.includes(input.resolution.toLowerCase())) {
|
|
1425
|
+
throw new ApiError("invalid_request", `${model} reference-to-video mode supports only ${constraint.referenceResolutions.join(" and ")} (received: ${input.resolution}); ${input.resolution} requires text-to-video or a single --first-frame image`);
|
|
1426
|
+
}
|
|
1347
1427
|
if (input.seconds !== void 0 && input.resolution) {
|
|
1348
1428
|
const required = constraint.requiredSecondsByResolution?.[input.resolution.toLowerCase()];
|
|
1349
1429
|
if (required !== void 0 && input.seconds !== required) {
|
|
@@ -1378,9 +1458,12 @@ function validateVideoGeneration(model, input) {
|
|
|
1378
1458
|
if (input.fps !== void 0 && (!constraint.allowedFps || !constraint.allowedFps.includes(input.fps))) {
|
|
1379
1459
|
throw new ApiError("invalid_request", `${model} fps must be one of ${constraint.allowedFps?.join(", ") ?? "none"} (received: ${input.fps})`);
|
|
1380
1460
|
}
|
|
1381
|
-
if (input.safetyTolerance !== void 0 &&
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1461
|
+
if (input.safetyTolerance !== void 0 && constraint.safetyTolerance) {
|
|
1462
|
+
const ceiling = (input.imageCount ?? 0) > 0 && constraint.safetyToleranceMaxWithImages !== void 0 ? Math.min(constraint.safetyTolerance.maximum, constraint.safetyToleranceMaxWithImages) : constraint.safetyTolerance.maximum;
|
|
1463
|
+
if (!Number.isInteger(input.safetyTolerance) || input.safetyTolerance < constraint.safetyTolerance.minimum || input.safetyTolerance > ceiling) {
|
|
1464
|
+
const withImagesNote = constraint.safetyToleranceMaxWithImages !== void 0 && ceiling !== constraint.safetyTolerance.maximum ? ` while images are attached (text-only allows up to ${constraint.safetyTolerance.maximum})` : "";
|
|
1465
|
+
throw new ApiError("invalid_request", `${model} safety_tolerance must be ${constraint.safetyTolerance.minimum}-${ceiling}${withImagesNote} (received: ${input.safetyTolerance})`);
|
|
1466
|
+
}
|
|
1384
1467
|
}
|
|
1385
1468
|
if (input.priority !== void 0 && !constraint.supportsPriority) {
|
|
1386
1469
|
throw new ApiError("invalid_request", `${model} does not support priority`);
|
|
@@ -1406,15 +1489,17 @@ import { join as join2, resolve } from "path";
|
|
|
1406
1489
|
import { pipeline } from "stream/promises";
|
|
1407
1490
|
import { Readable } from "stream";
|
|
1408
1491
|
var SUCCESS_STATES = /* @__PURE__ */ new Set(["success", "succeeded", "completed", "done", "finish", "finished"]);
|
|
1409
|
-
var FAILED_STATES = /* @__PURE__ */ new Set(["failed", "failure", "error", "
|
|
1492
|
+
var FAILED_STATES = /* @__PURE__ */ new Set(["failed", "failure", "error", "expired"]);
|
|
1493
|
+
var CANCELLED_STATES = /* @__PURE__ */ new Set(["cancelled", "canceled"]);
|
|
1410
1494
|
var RUNNING_STATES = /* @__PURE__ */ new Set(["running", "processing", "in_progress", "generating"]);
|
|
1411
1495
|
var PENDING_STATES = /* @__PURE__ */ new Set(["pending", "queued", "submitted", "waiting", "not_start"]);
|
|
1412
1496
|
function normalizeTaskStatus(raw) {
|
|
1413
1497
|
const s = String(raw ?? "").toLowerCase();
|
|
1414
1498
|
if (SUCCESS_STATES.has(s)) return "success";
|
|
1499
|
+
if (CANCELLED_STATES.has(s)) return "cancelled";
|
|
1415
1500
|
if (FAILED_STATES.has(s)) return "failed";
|
|
1416
1501
|
if (RUNNING_STATES.has(s)) return "running";
|
|
1417
|
-
if (PENDING_STATES.has(s)) return "pending";
|
|
1502
|
+
if (PENDING_STATES.has(s) || s.startsWith("queued_")) return "pending";
|
|
1418
1503
|
return "unknown";
|
|
1419
1504
|
}
|
|
1420
1505
|
function extractTaskId(body) {
|
|
@@ -1441,6 +1526,39 @@ function extractProgress(body) {
|
|
|
1441
1526
|
}
|
|
1442
1527
|
return void 0;
|
|
1443
1528
|
}
|
|
1529
|
+
async function cancelTask(baseUrl, apiKey, taskId) {
|
|
1530
|
+
try {
|
|
1531
|
+
await request({
|
|
1532
|
+
baseUrl,
|
|
1533
|
+
path: `/v1/video/generations/${encodeURIComponent(taskId)}`,
|
|
1534
|
+
method: "DELETE",
|
|
1535
|
+
apiKey
|
|
1536
|
+
});
|
|
1537
|
+
} catch (err) {
|
|
1538
|
+
if (err instanceof ApiError) {
|
|
1539
|
+
const code = err.upstreamCode;
|
|
1540
|
+
if (code === "task_already_running") {
|
|
1541
|
+
throw new ApiError("task_already_running", `\u4EFB\u52A1 ${taskId} \u5DF2\u5F00\u59CB\u8FD0\u884C\uFF0C\u65E0\u6CD5\u53D6\u6D88`, {
|
|
1542
|
+
status: err.status,
|
|
1543
|
+
hint: "\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4E0D\u53EF\u53D6\u6D88\uFF1B\u7528 focalapi task status " + taskId + " \u8DDF\u8E2A\u5230\u5B8C\u6210\u540E\u4E0B\u8F7D\u4EA7\u7269\u3002"
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
if (code === "task_already_finished") {
|
|
1547
|
+
throw new ApiError("task_already_finished", `\u4EFB\u52A1 ${taskId} \u5DF2\u7ED3\u675F\uFF0C\u65E0\u9700\u53D6\u6D88`, {
|
|
1548
|
+
status: err.status,
|
|
1549
|
+
hint: "\u8FD0\u884C focalapi task status " + taskId + " \u67E5\u770B\u7ED3\u679C\uFF1B\u6210\u529F\u540E\u53EF\u4E0B\u8F7D\u4EA7\u7269\u3002"
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
if (code === "task_cancel_incomplete") {
|
|
1553
|
+
throw new ApiError("task_cancel_incomplete", `\u4EFB\u52A1 ${taskId} \u5DF2\u53D6\u6D88\u4F46\u6E05\u7406\u672A\u5B8C\u6210`, {
|
|
1554
|
+
status: err.status,
|
|
1555
|
+
hint: "\u8BF7\u91CD\u8BD5\u540C\u4E00\u6761 cancel \u547D\u4EE4\u5B8C\u6210\u6E05\u7406\u3002"
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
throw err;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1444
1562
|
async function fetchTask(baseUrl, apiKey, taskId) {
|
|
1445
1563
|
let raw;
|
|
1446
1564
|
try {
|
|
@@ -1476,10 +1594,17 @@ async function pollTask(baseUrl, apiKey, taskId, opts) {
|
|
|
1476
1594
|
last = await fetchTask(baseUrl, apiKey, taskId);
|
|
1477
1595
|
opts?.onUpdate?.(last);
|
|
1478
1596
|
if (last.status === "success") return last;
|
|
1597
|
+
if (last.status === "cancelled") {
|
|
1598
|
+
throw new ApiError("task_cancelled", `\u4EFB\u52A1 ${taskId} \u5DF2\u53D6\u6D88\uFF08\u4E0A\u6E38\u72B6\u6001\uFF1A${last.rawStatus || "cancelled"}\uFF09`, {
|
|
1599
|
+
body: last.raw,
|
|
1600
|
+
hint: "\u4EFB\u52A1\u5DF2\u505C\u6B62\u4E14\u4E0D\u518D\u4EA7\u751F\u8D39\u7528\uFF1B\u5982\u9700\u91CD\u65B0\u751F\u6210\u8BF7\u63D0\u4EA4\u65B0\u4EFB\u52A1\u3002"
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1479
1603
|
if (last.status === "failed") {
|
|
1480
|
-
|
|
1604
|
+
const expired = last.rawStatus?.toLowerCase() === "expired";
|
|
1605
|
+
throw new ApiError("task_failed", expired ? `\u4EFB\u52A1 ${taskId} \u8D85\u8FC7\u6267\u884C\u671F\u9650\uFF08expired\uFF09\uFF0C\u8D39\u7528\u5DF2\u81EA\u52A8\u9000\u8FD8` : `\u4EFB\u52A1 ${taskId} \u5931\u8D25\uFF08\u4E0A\u6E38\u72B6\u6001\uFF1A${last.rawStatus || "unknown"}\uFF09`, {
|
|
1481
1606
|
body: last.raw,
|
|
1482
|
-
hint: "\u8FD0\u884C focalapi task status " + taskId + " --json \u67E5\u770B\u4E0A\u6E38\u8FD4\u56DE\u8BE6\u60C5\uFF1B\u82E5\u662F\u63D0\u793A\u8BCD\u6216\u53C2\u6570\u95EE\u9898\u8BF7\u8C03\u6574\u540E\u91CD\u8BD5\u3002"
|
|
1607
|
+
hint: expired ? "\u63D0\u4EA4\u72B6\u6001\u672A\u77E5\u4E14\u8D85\u8FC7 10 \u5206\u949F\u5BF9\u8D26\u671F\u9650\u7684\u4EFB\u52A1\u4F1A\u4EE5 expired \u7EC8\u6B62\u5E76\u9000\u6B3E\u3002\u53EF\u67E5\u770B focalapi task status " + taskId + " --json \u540E\u91CD\u65B0\u63D0\u4EA4\u3002" : "\u8FD0\u884C focalapi task status " + taskId + " --json \u67E5\u770B\u4E0A\u6E38\u8FD4\u56DE\u8BE6\u60C5\uFF1B\u82E5\u662F\u63D0\u793A\u8BCD\u6216\u53C2\u6570\u95EE\u9898\u8BF7\u8C03\u6574\u540E\u91CD\u8BD5\u3002"
|
|
1483
1608
|
});
|
|
1484
1609
|
}
|
|
1485
1610
|
if (Date.now() > deadline) {
|
|
@@ -1734,7 +1859,9 @@ function registerGen(program) {
|
|
|
1734
1859
|
seed: opts.seed,
|
|
1735
1860
|
thinkingLevel: opts.thinkingLevel,
|
|
1736
1861
|
temperature: opts.temperature,
|
|
1737
|
-
topP: opts.topP
|
|
1862
|
+
topP: opts.topP,
|
|
1863
|
+
referenceImageCount: opts.image?.length,
|
|
1864
|
+
nonDataUriReferenceCount: opts.image?.filter((source) => !source.trim().startsWith("data:")).length
|
|
1738
1865
|
});
|
|
1739
1866
|
const suppliedConfig = parseGenerationConfig(opts.config);
|
|
1740
1867
|
const suppliedResponseFormat = suppliedConfig.responseFormat;
|
|
@@ -1824,7 +1951,7 @@ function registerGen(program) {
|
|
|
1824
1951
|
if (res.id) info(`\u4EA4\u4E92 ID\uFF1A${res.id}`);
|
|
1825
1952
|
}
|
|
1826
1953
|
});
|
|
1827
|
-
gen.command("video").description("\u751F\u6210\u89C6\u9891\uFF08\u7701\u7565 --model \u65F6\u81EA\u52A8\u9009\u62E9\u5F53\u524D\u53EF\u7528\u9ED8\u8BA4\u6A21\u578B\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u89C6\u9891\u6A21\u578B ID\uFF1B\u7701\u7565\u65F6\u7531 focalapi \u81EA\u52A8\u9009\u62E9").option("--seconds <n>", "\u65F6\u957F\u79D2\u6570\uFF1B\u7CBE\u786E\u8303\u56F4\u8FD0\u884C focalapi models get <model> \u67E5\u770B", (v) => Number.parseInt(v, 10)).option("--size <size>", "\u5206\u8FA8\u7387\uFF0C\u5982 1280x720").option("--resolution <resolution>", "\u539F\u751F\u8F93\u51FA\u5206\u8FA8\u7387\uFF0C\u5982 480p\u3001720p\u30011080p\u30014k").option("--ratio <ratio>", "\u539F\u751F\u5BBD\u9AD8\u6BD4\uFF0C\u5982 16:9\u30019:16\u3001adaptive").option("--aspect-ratio <ratio>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9\u30019:16\u3001auto").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--fps <n>", "\u8F93\u51FA\u5E27\u7387\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-tolerance <n>", "\u5B89\u5168\u5BB9\u5FCD\u5EA6\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--image <url...>", "\u56FE\
|
|
1954
|
+
gen.command("video").description("\u751F\u6210\u89C6\u9891\uFF08\u7701\u7565 --model \u65F6\u81EA\u52A8\u9009\u62E9\u5F53\u524D\u53EF\u7528\u9ED8\u8BA4\u6A21\u578B\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u89C6\u9891\u6A21\u578B ID\uFF1B\u7701\u7565\u65F6\u7531 focalapi \u81EA\u52A8\u9009\u62E9").option("--seconds <n>", "\u65F6\u957F\u79D2\u6570\uFF1B\u7CBE\u786E\u8303\u56F4\u8FD0\u884C focalapi models get <model> \u67E5\u770B", (v) => Number.parseInt(v, 10)).option("--size <size>", "\u5206\u8FA8\u7387\uFF0C\u5982 1280x720").option("--resolution <resolution>", "\u539F\u751F\u8F93\u51FA\u5206\u8FA8\u7387\uFF0C\u5982 480p\u3001720p\u30011080p\u30014k").option("--ratio <ratio>", "\u539F\u751F\u5BBD\u9AD8\u6BD4\uFF0C\u5982 16:9\u30019:16\u3001adaptive").option("--aspect-ratio <ratio>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9\u30019:16\u3001auto").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--fps <n>", "\u8F93\u51FA\u5E27\u7387\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-tolerance <n>", "\u5B89\u5168\u5BB9\u5FCD\u5EA6\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--image <url...>", "\u53C2\u8003\u56FE URL\uFF08Grok \u89C6\u9891\u4E3A reference-to-video \u6A21\u5F0F\uFF0C\u53EF\u591A\u4E2A\uFF09").option("--first-frame <url>", "\u56FE\u751F\u89C6\u9891\u9996\u5E27\u56FE URL\uFF08image-to-video \u6A21\u5F0F\uFF1B\u4E0E --image \u4E92\u65A5\uFF09").option("--generate-audio <boolean>", "\u662F\u5426\u751F\u6210\u97F3\u9891\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "generate-audio")).option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "watermark")).option("--service-tier <tier>", "\u670D\u52A1\u5C42\u7EA7\uFF08Seedance 2.0 \u9ED8\u8BA4 default\uFF09").option("--priority <n>", "\u4EFB\u52A1\u4F18\u5148\u7EA7\uFF08\u4EC5 Seedance 2.0 \u7CFB\u5217\uFF09", (v) => Number.parseInt(v, 10)).option("--callback-url <url>", "\u4EFB\u52A1\u5B8C\u6210\u56DE\u8C03 URL").option("--return-last-frame <boolean>", "\u662F\u5426\u8FD4\u56DE\u6700\u540E\u4E00\u5E27\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "return-last-frame")).option("--execution-expires-after <seconds>", "\u4EFB\u52A1\u8FC7\u671F\u79D2\u6570\uFF083600\u2013259200\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-identifier <identifier>", "Seedance \u5B89\u5168\u6807\u8BC6\u7B26\uFF081\u201364 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF09").option("--no-wait", "\u63D0\u4EA4\u540E\u7ACB\u5373\u8FD4\u56DE task_id\uFF0C\u4E0D\u7B49\u5F85\u5B8C\u6210").option("--poll-interval <ms>", "\u8F6E\u8BE2\u95F4\u9694\u6BEB\u79D2", (v) => Number.parseInt(v, 10), 5e3).option("--timeout <minutes>", "\u6700\u957F\u7B49\u5F85\u5206\u949F", (v) => Number.parseInt(v, 10), 30).option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).option("--content <json>", "Ark-compatible content JSON array; overrides prompt/image facade fields").action(
|
|
1828
1955
|
async (promptParts, opts, cmd) => {
|
|
1829
1956
|
const g = cmd.optsWithGlobals();
|
|
1830
1957
|
const auth = resolveAuth(g);
|
|
@@ -1838,6 +1965,7 @@ function registerGen(program) {
|
|
|
1838
1965
|
}
|
|
1839
1966
|
if (opts.size) body.size = opts.size;
|
|
1840
1967
|
if (opts.image) body.images = opts.image;
|
|
1968
|
+
if (opts.firstFrame) body.image = opts.firstFrame;
|
|
1841
1969
|
if (opts.resolution) metadata.resolution = opts.resolution.toLowerCase();
|
|
1842
1970
|
if (opts.ratio) metadata.ratio = opts.ratio;
|
|
1843
1971
|
if (opts.aspectRatio) metadata.ratio = opts.aspectRatio;
|
|
@@ -1864,7 +1992,11 @@ function registerGen(program) {
|
|
|
1864
1992
|
serviceTier: opts.serviceTier,
|
|
1865
1993
|
priority: opts.priority,
|
|
1866
1994
|
executionExpiresAfter: opts.executionExpiresAfter,
|
|
1867
|
-
safetyIdentifier: opts.safetyIdentifier
|
|
1995
|
+
safetyIdentifier: opts.safetyIdentifier,
|
|
1996
|
+
imageCount: opts.image?.length,
|
|
1997
|
+
firstFrameCount: opts.firstFrame ? 1 : 0,
|
|
1998
|
+
generateAudio: opts.generateAudio,
|
|
1999
|
+
watermark: opts.watermark
|
|
1868
2000
|
});
|
|
1869
2001
|
if (Object.keys(metadata).length > 0) body.metadata = metadata;
|
|
1870
2002
|
const created = await withProgress("\u6B63\u5728\u63D0\u4EA4\u89C6\u9891\u4EFB\u52A1", () => request({
|
|
@@ -1883,7 +2015,7 @@ function registerGen(program) {
|
|
|
1883
2015
|
printJson({ model, task_id: taskId, submitted: true, next_command: `focalapi task status ${taskId} --json` });
|
|
1884
2016
|
} else {
|
|
1885
2017
|
process.stdout.write(taskId + "\n");
|
|
1886
|
-
info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u7EED\u53D6\uFF1Afocalapi task status ${taskId} / focalapi task download ${taskId}`);
|
|
2018
|
+
info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u7EED\u53D6\uFF1Afocalapi task status ${taskId} / focalapi task download ${taskId}\uFF1B\u6392\u961F\u4E2D\u53EF\u53D6\u6D88\uFF1Afocalapi task cancel ${taskId}`);
|
|
1887
2019
|
}
|
|
1888
2020
|
return;
|
|
1889
2021
|
}
|
|
@@ -1909,7 +2041,7 @@ function registerGen(program) {
|
|
|
1909
2041
|
|
|
1910
2042
|
// src/commands/task.ts
|
|
1911
2043
|
function registerTask(program) {
|
|
1912
|
-
const task = program.command("task").description("\u4EFB\u52A1\u67E5\u8BE2\u4E0E\u4EA7\u7269\u4E0B\u8F7D\uFF08\u89C6\u9891\u7B49\u4EFB\u52A1\u5236\u80FD\u529B\uFF09");
|
|
2044
|
+
const task = program.command("task").description("\u4EFB\u52A1\u67E5\u8BE2\u3001\u53D6\u6D88\u4E0E\u4EA7\u7269\u4E0B\u8F7D\uFF08\u89C6\u9891\u7B49\u4EFB\u52A1\u5236\u80FD\u529B\uFF09");
|
|
1913
2045
|
task.command("status").description("\u67E5\u8BE2\u4EFB\u52A1\u72B6\u6001").argument("<task_id>", "\u4EFB\u52A1 ID").action(async (taskId, _opts, cmd) => {
|
|
1914
2046
|
const g = cmd.optsWithGlobals();
|
|
1915
2047
|
const auth = resolveAuth(g);
|
|
@@ -1928,6 +2060,19 @@ function registerTask(program) {
|
|
|
1928
2060
|
if (info_.status === "success") {
|
|
1929
2061
|
info(`\u4EA7\u7269\u4E0B\u8F7D\uFF1Afocalapi task download ${taskId}`);
|
|
1930
2062
|
}
|
|
2063
|
+
if (info_.status === "pending" || info_.status === "running") {
|
|
2064
|
+
info(`\u5982\u9700\u505C\u6B62\u6392\u961F\u4E2D\u7684\u4EFB\u52A1\uFF1Afocalapi task cancel ${taskId}`);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
task.command("cancel").description("\u53D6\u6D88\u6392\u961F\u4E2D\u7684\u4EFB\u52A1\uFF08\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4E0D\u53EF\u53D6\u6D88\uFF1B\u53D6\u6D88\u540E\u8D39\u7528\u81EA\u52A8\u9000\u8FD8\uFF09").argument("<task_id>", "\u4EFB\u52A1 ID").action(async (taskId, _opts, cmd) => {
|
|
2069
|
+
const g = cmd.optsWithGlobals();
|
|
2070
|
+
const auth = resolveAuth(g);
|
|
2071
|
+
await cancelTask(auth.baseUrl, auth.apiKey, taskId);
|
|
2072
|
+
if (g.json) {
|
|
2073
|
+
printJson({ task_id: taskId, status: "cancelled", cancelled: true });
|
|
2074
|
+
} else {
|
|
2075
|
+
info(`\u2713 \u4EFB\u52A1 ${taskId} \u5DF2\u53D6\u6D88`);
|
|
1931
2076
|
}
|
|
1932
2077
|
});
|
|
1933
2078
|
task.command("download").description("\u4E0B\u8F7D\u4EFB\u52A1\u4EA7\u7269\uFF08\u7ECF focalapi \u5185\u5BB9\u4EE3\u7406\uFF0C\u65E0\u9700\u4E0A\u6E38\u7B7E\u540D URL\uFF09").argument("<task_id>", "\u4EFB\u52A1 ID").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", "focalapi-out").action(async (taskId, opts, cmd) => {
|
package/package.json
CHANGED
package/skills/focalapi/SKILL.md
CHANGED
|
@@ -28,6 +28,7 @@ focalapi gen video "<user prompt>" --no-wait -o ./focalapi-out --json
|
|
|
28
28
|
# Continue an asynchronous video task.
|
|
29
29
|
focalapi task status <task-id> --json
|
|
30
30
|
focalapi task download <task-id> -o ./focalapi-out --json
|
|
31
|
+
focalapi task cancel <task-id> --json # queued tasks only; cancelled tasks are refunded
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
## Routing table
|
|
@@ -37,7 +38,7 @@ focalapi task download <task-id> -o ./focalapi-out --json
|
|
|
37
38
|
| Generate or edit images; create from reference images | `focalapi gen image` | focalapi-gen |
|
|
38
39
|
| Generate video; animate images or reference media | `focalapi gen video` | focalapi-gen |
|
|
39
40
|
| Select, compare, or inspect model parameters | `focalapi models resolve/get/search` | focalapi-models |
|
|
40
|
-
| Inspect progress or failures; download results | `focalapi task status/download` | focalapi-task |
|
|
41
|
+
| Inspect progress or failures; cancel queued tasks; download results | `focalapi task status/cancel/download` | focalapi-task |
|
|
41
42
|
| Resolve key, sign-in, or 401 issues | `focalapi auth status/login` | focalapi-auth |
|
|
42
43
|
| Inspect quota, usage, or service failures | `focalapi usage/doctor` | focalapi-usage |
|
|
43
44
|
| Provide text assistance explicitly requested by the user | `focalapi chat` | focalapi-chat |
|
|
@@ -1,50 +1,56 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: focalapi-gen
|
|
3
|
-
version: 2.
|
|
4
|
-
description: "Use FocalAPI for image and video generation, image editing, image-to-video, and reference-media creation. Trigger directly when the user asks to draw, generate or edit an image, create video, or animate media, even without naming FocalAPI. Select a model automatically by default and do not probe models first."
|
|
5
|
-
metadata:
|
|
6
|
-
requires:
|
|
7
|
-
bins: ["focalapi"]
|
|
8
|
-
cliHelp: "focalapi gen --help"
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# FocalAPI image and video generation
|
|
12
|
-
|
|
13
|
-
## Default: select automatically and run once
|
|
14
|
-
|
|
15
|
-
When the user does not specify a model, run:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
focalapi gen image "<complete prompt>" -o ./focalapi-out --json
|
|
19
|
-
focalapi gen video "<complete prompt>" --no-wait -o ./focalapi-out --json
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
The CLI selects a default from the live model pool and detailed contracts available to the current key. Do not generate separate samples with a low-cost model, test prompt, or multiple models. Doing so creates unnecessary cost and ambiguity.
|
|
23
|
-
|
|
24
|
-
## Explicit models and advanced parameters
|
|
25
|
-
|
|
26
|
-
When the user names a model, read its live contract once:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
focalapi models get <model-id> --json
|
|
30
|
-
focalapi gen image "<prompt>" -m <model-id> [contract-supported options] -o ./focalapi-out --json
|
|
31
|
-
focalapi gen video "<prompt>" -m <model-id> [contract-supported options] --no-wait -o ./focalapi-out --json
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
- Use `--image <url...>` for image editing and reference images. Pass `--mask` only when the contract lists it.
|
|
35
|
-
- Use `--negative-prompt`, `--creativity`, `--prompt-extend`, `--style-references`, and `--moodboards` only when the image contract lists the corresponding field.
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
focalapi task
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
---
|
|
2
|
+
name: focalapi-gen
|
|
3
|
+
version: 2.2.0
|
|
4
|
+
description: "Use FocalAPI for image and video generation, image editing, image-to-video, and reference-media creation. Trigger directly when the user asks to draw, generate or edit an image, create video, or animate media, even without naming FocalAPI. Select a model automatically by default and do not probe models first."
|
|
5
|
+
metadata:
|
|
6
|
+
requires:
|
|
7
|
+
bins: ["focalapi"]
|
|
8
|
+
cliHelp: "focalapi gen --help"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# FocalAPI image and video generation
|
|
12
|
+
|
|
13
|
+
## Default: select automatically and run once
|
|
14
|
+
|
|
15
|
+
When the user does not specify a model, run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
focalapi gen image "<complete prompt>" -o ./focalapi-out --json
|
|
19
|
+
focalapi gen video "<complete prompt>" --no-wait -o ./focalapi-out --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The CLI selects a default from the live model pool and detailed contracts available to the current key. Do not generate separate samples with a low-cost model, test prompt, or multiple models. Doing so creates unnecessary cost and ambiguity.
|
|
23
|
+
|
|
24
|
+
## Explicit models and advanced parameters
|
|
25
|
+
|
|
26
|
+
When the user names a model, read its live contract once:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
focalapi models get <model-id> --json
|
|
30
|
+
focalapi gen image "<prompt>" -m <model-id> [contract-supported options] -o ./focalapi-out --json
|
|
31
|
+
focalapi gen video "<prompt>" -m <model-id> [contract-supported options] --no-wait -o ./focalapi-out --json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- Use `--image <url...>` for image editing and reference images. Pass `--mask` only when the contract lists it.
|
|
35
|
+
- Use `--negative-prompt`, `--creativity`, `--prompt-extend`, `--style-references`, and `--moodboards` only when the image contract lists the corresponding field.
|
|
36
|
+
- For video inputs, `--image <url...>` means reference images (Grok 1.5 reference-to-video, capped at 720p and 7 images) and `--first-frame <url>` means image-to-video from a single starting frame. The two flags are mutually exclusive and both are validated against the live contract before submission.
|
|
37
|
+
- Pass duration, resolution, aspect ratio, and audio options only as allowed by `supported_params`.
|
|
38
|
+
- Use `--content '<json-array>'` for models such as LTX 2.5, FLUX 3, Kling 3.0, and Vidu Q3 when the contract requires role-aware media content. LTX also exposes `--fps`; FLUX 3 exposes `--safety-tolerance` (0-4 text-to-video, capped at 2 once images are attached).
|
|
39
|
+
- Never copy one model's `ratio`, `aspect_ratio`, `size`, or `resolution` to another model.
|
|
40
|
+
- Use `gen gemini-image` only when the user explicitly selects a native Gemini image model. Continue to use the automatic `gen image` entry point for ordinary requests.
|
|
41
|
+
|
|
42
|
+
## Complete the result workflow
|
|
43
|
+
|
|
44
|
+
Synchronous image results contain local absolute paths in `files`; return them directly to the user. For asynchronous results, run the returned `next_command`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
focalapi task status <task-id> --json
|
|
48
|
+
focalapi task download <task-id> -o ./focalapi-out --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`pending` and `running` are not failures. Keep checking the same `task_id` and never resubmit generation. On failure, read the structured `error.code` and `hint`, and fix only the explicit problem instead of rotating models blindly.
|
|
52
|
+
|
|
53
|
+
Two transient outcomes need no parameter changes:
|
|
54
|
+
|
|
55
|
+
- `capacity_exhausted` (HTTP 503): the platform queue is full. Retry the same command after roughly 10 seconds; do not switch models or shrink the request.
|
|
56
|
+
- `expired` on a task: submission state stayed unknown past the 10-minute reconciliation window; the charge is refunded automatically. Resubmitting once is correct.
|
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: focalapi-models
|
|
3
|
-
version: 2.1.0
|
|
4
|
-
description: "Select FocalAPI creative models and inspect live parameter contracts. Use when the user does not specify a model, names a model or provider, compares models, or encounters a generation-parameter error. Use resolve to obtain a callable default and never infer capabilities from names or probe models one by one."
|
|
5
|
-
metadata:
|
|
6
|
-
requires:
|
|
7
|
-
bins: ["focalapi"]
|
|
8
|
-
cliHelp: "focalapi models --help"
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# FocalAPI model selection
|
|
12
|
-
|
|
13
|
-
## Shortest path
|
|
14
|
-
|
|
15
|
-
When the user does not specify a model, do not list every model or rank them yourself:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
focalapi models resolve image --json
|
|
19
|
-
focalapi models resolve video --json
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
`resolve` reads the live list available to the current key, then reads detailed contracts for candidate models and returns:
|
|
23
|
-
|
|
24
|
-
- `model.id`: the exact ID accepted by generation commands;
|
|
25
|
-
- `endpoint_type`: the generation endpoint verified by model details;
|
|
26
|
-
- `model.supported_params`: available parameters, defaults, enumerations, and ranges;
|
|
27
|
-
- `next_command`: the next command with no guessing required.
|
|
28
|
-
|
|
29
|
-
Omitting `--model` from `focalapi gen image/video` uses the same selection logic internally.
|
|
30
|
-
|
|
31
|
-
## User-selected models
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
focalapi models get <complete-model-id> --json
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Search once only when the user provides an incomplete provider or family name:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
focalapi models search <keyword> --json
|
|
41
|
-
focalapi models get <selected-complete-id> --json
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Rules:
|
|
45
|
-
|
|
46
|
-
1. `models get` is authoritative for endpoints and parameters. A list summary may show only a protocol family and cannot be used to infer modality.
|
|
47
|
-
2. Do not send generation requests to models one by one as an availability test. Discovery and detail queries are read-only preflight checks.
|
|
48
|
-
3. If an explicitly selected model is unavailable, present available candidates or return to `models resolve`; never replace it silently.
|
|
49
|
-
4. Return to the user's original generation task after the query instead of stopping at the model list.
|
|
50
|
-
|
|
51
|
-
## Current verified creative families
|
|
52
|
-
|
|
53
|
-
The maintained defaults currently prefer Seedream 5.0, GPT Image 2, Gemini 3.1 Image, Grok Imagine Image 2.0, Kling Image 3.0, Qwen Image 3.0, and Krea 2 for images. Video selection currently covers Seedance 2.5, Kling 3.0, Vidu Q3, Gemini Omni Flash, Grok Imagine Video 1.5, LTX 2.5, FLUX 3
|
|
54
|
-
|
|
55
|
-
These names are routing context, not permission to guess a model ID. Use the exact canonical ID returned by `models resolve` or `models get`. Removed IDs, including the former Veo 3.1 preview models, must not be retried.
|
|
1
|
+
---
|
|
2
|
+
name: focalapi-models
|
|
3
|
+
version: 2.1.0
|
|
4
|
+
description: "Select FocalAPI creative models and inspect live parameter contracts. Use when the user does not specify a model, names a model or provider, compares models, or encounters a generation-parameter error. Use resolve to obtain a callable default and never infer capabilities from names or probe models one by one."
|
|
5
|
+
metadata:
|
|
6
|
+
requires:
|
|
7
|
+
bins: ["focalapi"]
|
|
8
|
+
cliHelp: "focalapi models --help"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# FocalAPI model selection
|
|
12
|
+
|
|
13
|
+
## Shortest path
|
|
14
|
+
|
|
15
|
+
When the user does not specify a model, do not list every model or rank them yourself:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
focalapi models resolve image --json
|
|
19
|
+
focalapi models resolve video --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`resolve` reads the live list available to the current key, then reads detailed contracts for candidate models and returns:
|
|
23
|
+
|
|
24
|
+
- `model.id`: the exact ID accepted by generation commands;
|
|
25
|
+
- `endpoint_type`: the generation endpoint verified by model details;
|
|
26
|
+
- `model.supported_params`: available parameters, defaults, enumerations, and ranges;
|
|
27
|
+
- `next_command`: the next command with no guessing required.
|
|
28
|
+
|
|
29
|
+
Omitting `--model` from `focalapi gen image/video` uses the same selection logic internally.
|
|
30
|
+
|
|
31
|
+
## User-selected models
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
focalapi models get <complete-model-id> --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Search once only when the user provides an incomplete provider or family name:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
focalapi models search <keyword> --json
|
|
41
|
+
focalapi models get <selected-complete-id> --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Rules:
|
|
45
|
+
|
|
46
|
+
1. `models get` is authoritative for endpoints and parameters. A list summary may show only a protocol family and cannot be used to infer modality.
|
|
47
|
+
2. Do not send generation requests to models one by one as an availability test. Discovery and detail queries are read-only preflight checks.
|
|
48
|
+
3. If an explicitly selected model is unavailable, present available candidates or return to `models resolve`; never replace it silently.
|
|
49
|
+
4. Return to the user's original generation task after the query instead of stopping at the model list.
|
|
50
|
+
|
|
51
|
+
## Current verified creative families
|
|
52
|
+
|
|
53
|
+
The maintained defaults currently prefer Seedream 5.0, GPT Image 2, Gemini 3.1 Image, Grok Imagine Image 2.0, Kling Image 3.0, Qwen Image 3.0, and Krea 2 for images. Video selection currently covers Seedance 2.5, Kling 3.0, Vidu Q3, Gemini Omni Flash, Grok Imagine Video 1.5, LTX 2.5, and FLUX 3.
|
|
54
|
+
|
|
55
|
+
These names are routing context, not permission to guess a model ID. Use the exact canonical ID returned by `models resolve` or `models get`. Removed IDs, including the former Veo 3.1 preview models, must not be retried.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: focalapi-task
|
|
3
|
-
version: 2.
|
|
4
|
-
description: "Continue asynchronous FocalAPI image or video tasks and download their outputs. Use when a generation response contains task_id or next_command, or when the user asks about progress, failure reasons, or result files. Reuse the original task_id and never generate again."
|
|
3
|
+
version: 2.1.0
|
|
4
|
+
description: "Continue asynchronous FocalAPI image or video tasks, cancel queued tasks, and download their outputs. Use when a generation response contains task_id or next_command, or when the user asks about progress, failure reasons, cancellation, or result files. Reuse the original task_id and never generate again."
|
|
5
5
|
metadata:
|
|
6
6
|
requires:
|
|
7
7
|
bins: ["focalapi"]
|
|
@@ -15,11 +15,15 @@ After a generation command returns `task_id`, prefer the response's `next_comman
|
|
|
15
15
|
```bash
|
|
16
16
|
focalapi task status <task-id> --json
|
|
17
17
|
focalapi task download <task-id> -o ./focalapi-out --json
|
|
18
|
+
focalapi task cancel <task-id> --json
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
- `pending` or `running`: this is still the same valid task. Check it later and do not resubmit generation.
|
|
21
22
|
- `success`: run download, verify that the file exists, and return its absolute path to the user.
|
|
22
|
-
- `failed`: show the upstream error summary and hint. Generate again only when an explicit parameter or content issue requires it.
|
|
23
|
+
- `failed`: show the upstream error summary and hint. Generate again only when an explicit parameter or content issue requires it. A raw status of `expired` means the submission state stayed unknown past the reconciliation window; the charge was refunded automatically.
|
|
24
|
+
- `cancelled`: the task was stopped; a cancelled queued task is refunded. Do not download or resubmit unless the user asks for a new attempt.
|
|
23
25
|
- `unknown`: preserve the raw response and run `focalapi doctor --json`; never fabricate a success state.
|
|
24
26
|
|
|
27
|
+
`task cancel` only works while a task is still queued (`pending`). A 409 `task_already_running` means generation already started and cannot be stopped — keep tracking with `task status`. Cancellation failures return explicit codes (`task_already_finished`, `task_cancel_failed`); follow `error.hint` instead of retrying blindly.
|
|
28
|
+
|
|
25
29
|
Polling must be bounded. When the user does not ask for blocking wait behavior, report the current status and `task_id`.
|
|
@@ -19,6 +19,7 @@ focalapi doctor --json
|
|
|
19
19
|
- For quota, balance, usage, or billing questions, run `usage`.
|
|
20
20
|
- For `missing_api_key` or `invalid_api_key`, route to focalapi-auth.
|
|
21
21
|
- For `insufficient_quota`, run `usage`, explain the shortfall, and do not add funds automatically.
|
|
22
|
+
- For `capacity_exhausted` (HTTP 503), wait about 10 seconds and retry the exact same command; the queue admission gate is full and parameters are not the problem.
|
|
22
23
|
- For `network_error`, `timeout`, or 5xx errors, run `doctor` once and follow `checks[].hint`.
|
|
23
24
|
- For `invalid_request`, return to the live `models get` parameter contract and do not resend the same request.
|
|
24
25
|
- For `upstream_auth_failed`, preserve the request ID and escalate to the service operator; do not ask the user to replace the FocalAPI key.
|