@mrgoonie/multix 0.15.0 → 0.15.2
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 +14 -0
- package/README.md +1 -0
- package/dist/cli.js +140 -57
- package/package.json +1 -1
- package/skill/SKILL.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.15.2](https://github.com/mrgoonie/multix-cli/compare/v0.15.1...v0.15.2) (2026-10-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
* **doc:** do not retry configuration and non-transient errors ([#96](https://github.com/mrgoonie/multix-cli/issues/96)) ([6c505a6](https://github.com/mrgoonie/multix-cli/commit/6c505a64f88cbcc3ee992ca4112473400189305f))
|
|
14
|
+
|
|
15
|
+
## [0.15.1](https://github.com/mrgoonie/multix-cli/compare/v0.15.0...v0.15.1) (2026-10-11)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Performance
|
|
19
|
+
|
|
20
|
+
* **gemini:** stream file uploads with resumable Files API ([#85](https://github.com/mrgoonie/multix-cli/issues/85)) ([b2cc886](https://github.com/mrgoonie/multix-cli/commit/b2cc886cf5bdf960e88aeea5155ae2623547d918))
|
|
21
|
+
|
|
8
22
|
## [0.15.0](https://github.com/mrgoonie/multix-cli/compare/v0.14.1...v0.15.0) (2026-10-11)
|
|
9
23
|
|
|
10
24
|
|
package/README.md
CHANGED
|
@@ -99,6 +99,7 @@ Set at least one provider key. Add to `.env` in your project root or `~/.multix/
|
|
|
99
99
|
| `CLOUDFLARE_AI_TTS_MODEL` | No | Speech model setting; only `@cf/myshell-ai/melotts` is accepted |
|
|
100
100
|
| `MULTIX_OUTPUT_DIR` | No | Override default output dir (`./multix-output`) |
|
|
101
101
|
| `MULTIX_MAX_DOWNLOAD_MB` | No | Size limit per downloaded Replicate output file (default `2048`) |
|
|
102
|
+
| `MULTIX_UPLOAD_MIN_KBPS` | No | Slowest Gemini Files API upload speed to allow for, in KB/s (default `1000`); lower it on slow links |
|
|
102
103
|
| `MULTIX_IMAGE_FORMAT` | No | `webp` (default) or `original` to keep provider image format/quality |
|
|
103
104
|
| `OPENROUTER_IMAGE_MODEL` | No | Default OpenRouter model |
|
|
104
105
|
| `OPENROUTER_FALLBACK_MODELS` | No | Comma-separated fallback model ids |
|
package/dist/cli.js
CHANGED
|
@@ -46,8 +46,8 @@ var init_errors = __esm({
|
|
|
46
46
|
"use strict";
|
|
47
47
|
init_redact();
|
|
48
48
|
MultixError = class extends Error {
|
|
49
|
-
constructor(message, code) {
|
|
50
|
-
super(message);
|
|
49
|
+
constructor(message, code, options) {
|
|
50
|
+
super(message, options);
|
|
51
51
|
this.code = code;
|
|
52
52
|
this.name = "MultixError";
|
|
53
53
|
Error.captureStackTrace?.(
|
|
@@ -64,8 +64,8 @@ var init_errors = __esm({
|
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
ProviderError = class extends MultixError {
|
|
67
|
-
constructor(message, provider) {
|
|
68
|
-
super(message, "PROVIDER_ERROR");
|
|
67
|
+
constructor(message, provider, options) {
|
|
68
|
+
super(message, "PROVIDER_ERROR", options);
|
|
69
69
|
this.provider = provider;
|
|
70
70
|
this.name = "ProviderError";
|
|
71
71
|
}
|
|
@@ -78,8 +78,8 @@ var init_errors = __esm({
|
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
80
|
HttpError = class extends MultixError {
|
|
81
|
-
constructor(status, snippet, url, retryAfterMs) {
|
|
82
|
-
super(`HTTP ${status} from ${redactUrl(url)}: ${snippet}`, "HTTP_ERROR");
|
|
81
|
+
constructor(status, snippet, url, retryAfterMs, options) {
|
|
82
|
+
super(`HTTP ${status} from ${redactUrl(url)}: ${snippet}`, "HTTP_ERROR", options);
|
|
83
83
|
this.status = status;
|
|
84
84
|
this.snippet = snippet;
|
|
85
85
|
this.retryAfterMs = retryAfterMs;
|
|
@@ -489,7 +489,8 @@ var PollTimeoutError = class extends MultixError {
|
|
|
489
489
|
};
|
|
490
490
|
function isTransientHttpError(error) {
|
|
491
491
|
if (!(error instanceof HttpError)) return false;
|
|
492
|
-
|
|
492
|
+
const { status } = error;
|
|
493
|
+
return status === 0 || status === 408 || status === 429 || status >= 500;
|
|
493
494
|
}
|
|
494
495
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
495
496
|
async function pollUntil(opts) {
|
|
@@ -902,7 +903,7 @@ var AtlasCloudClient = class {
|
|
|
902
903
|
}
|
|
903
904
|
/**
|
|
904
905
|
* Poll until the prediction is terminal (success or failure) or the deadline
|
|
905
|
-
* passes. Network errors, 429 (honouring Retry-After) and 5xx are retried.
|
|
906
|
+
* passes. Network errors, 408, 429 (honouring Retry-After) and 5xx are retried.
|
|
906
907
|
* Throws PollTimeoutError on timeout; returns the terminal prediction otherwise.
|
|
907
908
|
*/
|
|
908
909
|
async waitForPrediction(id, opts) {
|
|
@@ -4651,11 +4652,20 @@ init_errors();
|
|
|
4651
4652
|
import fs20 from "fs";
|
|
4652
4653
|
import os2 from "os";
|
|
4653
4654
|
import path23 from "path";
|
|
4655
|
+
|
|
4656
|
+
// src/core/limits.ts
|
|
4657
|
+
var FILES_API_MAX_BYTES = 2e9;
|
|
4658
|
+
|
|
4659
|
+
// src/core/stdin-file.ts
|
|
4654
4660
|
var STDIN_ARG = "-";
|
|
4655
4661
|
var STDIN_LABEL = "<stdin>";
|
|
4656
4662
|
var HEAD_BYTES = 4096;
|
|
4657
|
-
var MAX_STDIN_BYTES =
|
|
4663
|
+
var MAX_STDIN_BYTES = FILES_API_MAX_BYTES;
|
|
4658
4664
|
var WAIT_HINT_MS = 2e3;
|
|
4665
|
+
function stdinTooLargeMessage(maxBytes) {
|
|
4666
|
+
const bytes = maxBytes.toLocaleString("en-US");
|
|
4667
|
+
return maxBytes === FILES_API_MAX_BYTES ? `Piped stdin exceeds the Gemini Files API limit of 2 GB (${bytes} bytes)` : `Piped stdin exceeds the ${bytes}-byte limit`;
|
|
4668
|
+
}
|
|
4659
4669
|
var ascii = (buf, start, end) => buf.toString("latin1", start, end);
|
|
4660
4670
|
var HEIF_BRANDS = /* @__PURE__ */ new Set([
|
|
4661
4671
|
"heic",
|
|
@@ -4795,9 +4805,7 @@ async function stdinToTempFile(opts = {}) {
|
|
|
4795
4805
|
clearTimeout(hint);
|
|
4796
4806
|
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
4797
4807
|
if (size + buf.length > maxBytes) {
|
|
4798
|
-
throw new ValidationError(
|
|
4799
|
-
`Piped stdin exceeds ${Math.floor(maxBytes / 1024 ** 2)} MB; save it to a file instead`
|
|
4800
|
-
);
|
|
4808
|
+
throw new ValidationError(stdinTooLargeMessage(maxBytes));
|
|
4801
4809
|
}
|
|
4802
4810
|
if (head.length < HEAD_BYTES) {
|
|
4803
4811
|
head = Buffer.concat([head, buf.subarray(0, HEAD_BYTES - head.length)]);
|
|
@@ -4859,9 +4867,9 @@ async function resolveFileInputs(files, opts = {}) {
|
|
|
4859
4867
|
}
|
|
4860
4868
|
|
|
4861
4869
|
// src/providers/gemini/client.ts
|
|
4862
|
-
init_errors();
|
|
4863
4870
|
import fs21 from "fs";
|
|
4864
4871
|
import path25 from "path";
|
|
4872
|
+
init_errors();
|
|
4865
4873
|
|
|
4866
4874
|
// src/providers/gemini/key-pool.ts
|
|
4867
4875
|
init_errors();
|
|
@@ -4985,49 +4993,123 @@ function withKey(opts, fn) {
|
|
|
4985
4993
|
function authHeaders2(apiKey) {
|
|
4986
4994
|
return { "x-goog-api-key": apiKey };
|
|
4987
4995
|
}
|
|
4996
|
+
var DEFAULT_TIMEOUT_MS3 = 3e5;
|
|
4997
|
+
var UPLOAD_MIN_KBPS_ENV = "MULTIX_UPLOAD_MIN_KBPS";
|
|
4998
|
+
var DEFAULT_MIN_UPLOAD_BYTES_PER_SEC = 1e6;
|
|
4999
|
+
var UPLOAD_TIMEOUT_SLACK_MS = 6e4;
|
|
5000
|
+
function minUploadBytesPerSec() {
|
|
5001
|
+
const raw = resolveKey(UPLOAD_MIN_KBPS_ENV)?.trim();
|
|
5002
|
+
if (!raw) return DEFAULT_MIN_UPLOAD_BYTES_PER_SEC;
|
|
5003
|
+
const kbps = Number(raw);
|
|
5004
|
+
if (!Number.isFinite(kbps) || kbps < 1) {
|
|
5005
|
+
throw new ConfigError(
|
|
5006
|
+
`${UPLOAD_MIN_KBPS_ENV} must be a number of kilobytes per second, at least 1, got "${raw}"`
|
|
5007
|
+
);
|
|
5008
|
+
}
|
|
5009
|
+
return kbps * 1e3;
|
|
5010
|
+
}
|
|
5011
|
+
function uploadTimeoutMs(size, timeoutMs = DEFAULT_TIMEOUT_MS3, bytesPerSec = DEFAULT_MIN_UPLOAD_BYTES_PER_SEC) {
|
|
5012
|
+
return Math.max(timeoutMs, UPLOAD_TIMEOUT_SLACK_MS + Math.ceil(size / bytesPerSec * 1e3));
|
|
5013
|
+
}
|
|
4988
5014
|
async function uploadFile(filePath, opts = {}) {
|
|
4989
|
-
|
|
5015
|
+
const size = fs21.statSync(filePath).size;
|
|
5016
|
+
if (size > FILES_API_MAX_BYTES) {
|
|
5017
|
+
throw new ValidationError(
|
|
5018
|
+
`File too large for the Gemini Files API: ${path25.basename(filePath)} is ${formatMb3(size)} MB (limit 2 GB = ${FILES_API_MAX_BYTES} bytes)`
|
|
5019
|
+
);
|
|
5020
|
+
}
|
|
5021
|
+
const bytesPerSec = minUploadBytesPerSec();
|
|
5022
|
+
return withKey(opts, (apiKey) => uploadFileWithKey(filePath, apiKey, { ...opts, bytesPerSec }));
|
|
5023
|
+
}
|
|
5024
|
+
function formatMb3(bytes) {
|
|
5025
|
+
return (bytes / 1024 / 1024).toFixed(2);
|
|
5026
|
+
}
|
|
5027
|
+
async function uploadFetch(url, init, timeoutMs, step) {
|
|
5028
|
+
let resp;
|
|
5029
|
+
try {
|
|
5030
|
+
resp = await globalThis.fetch(url, {
|
|
5031
|
+
...init,
|
|
5032
|
+
// With the default "follow", fetch clones the request body (tee) to replay
|
|
5033
|
+
// it on a redirect, and that copy keeps every chunk in memory.
|
|
5034
|
+
redirect: "error",
|
|
5035
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
5036
|
+
});
|
|
5037
|
+
} catch (err2) {
|
|
5038
|
+
const e = err2;
|
|
5039
|
+
const timedOut = e?.name === "TimeoutError";
|
|
5040
|
+
const redirected = e?.cause instanceof Error && e.cause.message === "unexpected redirect";
|
|
5041
|
+
const reason = timedOut ? `timed out after ${Math.round(timeoutMs / 1e3)} s` : e?.cause instanceof Error ? `${e.message}: ${e.cause.message}` : String(e?.message ?? err2);
|
|
5042
|
+
const message = `File upload failed: ${reason}`;
|
|
5043
|
+
if (redirected || timedOut && step === "upload") {
|
|
5044
|
+
throw new ProviderError(message, "gemini", { cause: err2 });
|
|
5045
|
+
}
|
|
5046
|
+
throw new HttpError(0, message, FILES_BASE, void 0, { cause: err2 });
|
|
5047
|
+
}
|
|
5048
|
+
if (!resp.ok) {
|
|
5049
|
+
const txt = await resp.text();
|
|
5050
|
+
throw new HttpError(resp.status, `File upload failed: ${txt.slice(0, 500)}`, FILES_BASE);
|
|
5051
|
+
}
|
|
5052
|
+
return resp;
|
|
5053
|
+
}
|
|
5054
|
+
async function readFinalizedFile(resp) {
|
|
5055
|
+
const status = resp.headers.get("x-goog-upload-status")?.trim().toLowerCase();
|
|
5056
|
+
let file;
|
|
5057
|
+
let readError;
|
|
5058
|
+
try {
|
|
5059
|
+
file = (await resp.json())?.file;
|
|
5060
|
+
} catch (err2) {
|
|
5061
|
+
readError = err2;
|
|
5062
|
+
}
|
|
5063
|
+
if (status !== "final" || typeof file?.uri !== "string" || typeof file?.name !== "string") {
|
|
5064
|
+
const detail = status !== "final" ? `upload status is "${status?.slice(0, 40) ?? "missing"}"` : readError ? "unreadable response body" : "no file in the response";
|
|
5065
|
+
throw new ProviderError(`File upload failed: upload was not finalized (${detail})`, "gemini", {
|
|
5066
|
+
cause: readError
|
|
5067
|
+
});
|
|
5068
|
+
}
|
|
5069
|
+
return file;
|
|
4990
5070
|
}
|
|
4991
5071
|
async function uploadFileWithKey(filePath, apiKey, opts) {
|
|
4992
|
-
const { timeoutMs =
|
|
5072
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS3, logger, bytesPerSec } = opts;
|
|
4993
5073
|
const mimeType = getMimeType(filePath);
|
|
4994
|
-
const fileBytes = fs21.readFileSync(filePath);
|
|
4995
5074
|
const displayName = path25.basename(filePath);
|
|
4996
|
-
const
|
|
4997
|
-
|
|
4998
|
-
const
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
--${boundary}--`)
|
|
5012
|
-
]);
|
|
5013
|
-
logger?.debug(`Uploading ${displayName} (${(fileBytes.length / 1024 / 1024).toFixed(2)} MB)`);
|
|
5014
|
-
const resp = await globalThis.fetch(FILES_BASE, {
|
|
5015
|
-
method: "POST",
|
|
5016
|
-
headers: {
|
|
5017
|
-
"x-goog-api-key": apiKey,
|
|
5018
|
-
"Content-Type": `multipart/related; boundary=${boundary}`,
|
|
5019
|
-
"Content-Length": String(body.length),
|
|
5020
|
-
"X-Goog-Upload-Protocol": "multipart"
|
|
5075
|
+
const file = await fs21.openAsBlob(filePath);
|
|
5076
|
+
logger?.debug(`Uploading ${displayName} (${formatMb3(file.size)} MB)`);
|
|
5077
|
+
const start = await uploadFetch(
|
|
5078
|
+
FILES_BASE,
|
|
5079
|
+
{
|
|
5080
|
+
method: "POST",
|
|
5081
|
+
headers: {
|
|
5082
|
+
"x-goog-api-key": apiKey,
|
|
5083
|
+
"Content-Type": "application/json",
|
|
5084
|
+
"X-Goog-Upload-Protocol": "resumable",
|
|
5085
|
+
"X-Goog-Upload-Command": "start",
|
|
5086
|
+
"X-Goog-Upload-Header-Content-Length": String(file.size),
|
|
5087
|
+
"X-Goog-Upload-Header-Content-Type": mimeType
|
|
5088
|
+
},
|
|
5089
|
+
body: JSON.stringify({ file: { display_name: displayName } })
|
|
5021
5090
|
},
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5091
|
+
timeoutMs,
|
|
5092
|
+
"start"
|
|
5093
|
+
);
|
|
5094
|
+
const uploadUrl = start.headers.get("x-goog-upload-url");
|
|
5095
|
+
await start.body?.cancel();
|
|
5096
|
+
if (!uploadUrl) {
|
|
5097
|
+
throw new ProviderError("File upload failed: the Files API returned no upload URL", "gemini");
|
|
5028
5098
|
}
|
|
5029
|
-
const
|
|
5030
|
-
|
|
5099
|
+
const resp = await uploadFetch(
|
|
5100
|
+
uploadUrl,
|
|
5101
|
+
{
|
|
5102
|
+
method: "POST",
|
|
5103
|
+
headers: {
|
|
5104
|
+
"X-Goog-Upload-Offset": "0",
|
|
5105
|
+
"X-Goog-Upload-Command": "upload, finalize"
|
|
5106
|
+
},
|
|
5107
|
+
body: file
|
|
5108
|
+
},
|
|
5109
|
+
uploadTimeoutMs(file.size, timeoutMs, bytesPerSec),
|
|
5110
|
+
"upload"
|
|
5111
|
+
);
|
|
5112
|
+
let fileRef = await readFinalizedFile(resp);
|
|
5031
5113
|
if (requiresProcessingWait(mimeType)) {
|
|
5032
5114
|
const deadline = Date.now() + timeoutMs;
|
|
5033
5115
|
while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
|
|
@@ -9474,7 +9556,7 @@ import path45 from "path";
|
|
|
9474
9556
|
import fs37 from "fs";
|
|
9475
9557
|
import path44 from "path";
|
|
9476
9558
|
init_errors();
|
|
9477
|
-
var
|
|
9559
|
+
var DEFAULT_TIMEOUT_MS4 = 24e4;
|
|
9478
9560
|
function requireOpenAIKey() {
|
|
9479
9561
|
const key = resolveKey("OPENAI_API_KEY");
|
|
9480
9562
|
if (!key) {
|
|
@@ -9490,7 +9572,7 @@ function endpoint(pathname) {
|
|
|
9490
9572
|
function authHeaders4(apiKey, extra) {
|
|
9491
9573
|
return { Authorization: `Bearer ${apiKey}`, ...extra ?? {} };
|
|
9492
9574
|
}
|
|
9493
|
-
function postOpenAIJson(pathname, body, apiKey, timeoutMs =
|
|
9575
|
+
function postOpenAIJson(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS4) {
|
|
9494
9576
|
return httpJson({
|
|
9495
9577
|
url: endpoint(pathname),
|
|
9496
9578
|
method: "POST",
|
|
@@ -9499,7 +9581,7 @@ function postOpenAIJson(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3)
|
|
|
9499
9581
|
timeoutMs
|
|
9500
9582
|
});
|
|
9501
9583
|
}
|
|
9502
|
-
async function postOpenAIForm(pathname, form, apiKey, timeoutMs =
|
|
9584
|
+
async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS4) {
|
|
9503
9585
|
const url = endpoint(pathname);
|
|
9504
9586
|
let res;
|
|
9505
9587
|
try {
|
|
@@ -9518,7 +9600,7 @@ async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOU
|
|
|
9518
9600
|
}
|
|
9519
9601
|
return res.json();
|
|
9520
9602
|
}
|
|
9521
|
-
async function postOpenAIBinary(pathname, body, apiKey, timeoutMs =
|
|
9603
|
+
async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS4) {
|
|
9522
9604
|
const url = endpoint(pathname);
|
|
9523
9605
|
let res;
|
|
9524
9606
|
try {
|
|
@@ -10796,7 +10878,7 @@ var ReplicateClient = class {
|
|
|
10796
10878
|
/**
|
|
10797
10879
|
* Poll until the prediction reaches a terminal status (succeeded, failed,
|
|
10798
10880
|
* canceled, aborted) and return it — callers decide success via
|
|
10799
|
-
* assertSucceeded. Retries network/429/5xx; throws PollTimeoutError on deadline.
|
|
10881
|
+
* assertSucceeded. Retries network/408/429/5xx; throws PollTimeoutError on deadline.
|
|
10800
10882
|
*/
|
|
10801
10883
|
async waitForPrediction(prediction, opts) {
|
|
10802
10884
|
if (typeof prediction !== "string" && isTerminalStatus2(prediction.status)) return prediction;
|
|
@@ -11441,7 +11523,7 @@ import fs45 from "fs";
|
|
|
11441
11523
|
import path52 from "path";
|
|
11442
11524
|
var DATA_URI_MAX_BYTES = 256 * 1024;
|
|
11443
11525
|
var UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
11444
|
-
function
|
|
11526
|
+
function formatMb4(bytes) {
|
|
11445
11527
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
11446
11528
|
}
|
|
11447
11529
|
async function resolveFileInput(value, client, logger) {
|
|
@@ -11457,7 +11539,7 @@ async function resolveFileInput(value, client, logger) {
|
|
|
11457
11539
|
if (stat.size === 0) throw new ValidationError(`Input file is empty: ${value}`);
|
|
11458
11540
|
if (stat.size >= UPLOAD_MAX_BYTES) {
|
|
11459
11541
|
throw new ValidationError(
|
|
11460
|
-
`Input file ${value} is ${
|
|
11542
|
+
`Input file ${value} is ${formatMb4(stat.size)}; Replicate accepts uploads under 100MB. Host it and pass an https URL instead.`
|
|
11461
11543
|
);
|
|
11462
11544
|
}
|
|
11463
11545
|
const filename = path52.basename(filePath);
|
|
@@ -11467,7 +11549,7 @@ async function resolveFileInput(value, client, logger) {
|
|
|
11467
11549
|
logger.debug(`Inlining ${filename} (${stat.size} bytes) as a data URI`);
|
|
11468
11550
|
return `data:${contentType};base64,${bytes.toString("base64")}`;
|
|
11469
11551
|
}
|
|
11470
|
-
logger.info(`Uploading ${filename} (${
|
|
11552
|
+
logger.info(`Uploading ${filename} (${formatMb4(stat.size)}) to Replicate Files...`);
|
|
11471
11553
|
const file = await client.uploadFile({
|
|
11472
11554
|
data: new Blob([bytes], { type: contentType }),
|
|
11473
11555
|
filename,
|
|
@@ -11980,6 +12062,7 @@ async function convertResolved(inputs, ctx) {
|
|
|
11980
12062
|
} catch (e) {
|
|
11981
12063
|
errorMsg = labelError(e instanceof Error ? e.message : String(e), input);
|
|
11982
12064
|
if (isGeminiRateLimitError(e)) break;
|
|
12065
|
+
if (!isTransientHttpError(e)) break;
|
|
11983
12066
|
if (attempt < 2) {
|
|
11984
12067
|
const wait = 2 ** attempt * 1e3;
|
|
11985
12068
|
logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrgoonie/multix",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "AI multimodal CLI — OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, Atlas Cloud, Replicate, HeyGen, ffmpeg, ImageMagick, doc-to-md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/skill/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: multix
|
|
3
3
|
description: AI multimodal CLI — generate and edit images (OpenAI, Codex driver, Gemini Nano Banana, Imagen, MiniMax, OpenRouter, Leonardo, BytePlus Seedream, Cloudflare Workers AI FLUX.1 Schnell, fal.ai FLUX, Replicate FLUX), generate video (Gemini Veo, MiniMax Hailuo, OpenRouter via Veo/Seedance/Kling, Leonardo via Motion/Veo/Kling/Hailuo/Seedance/LTX, BytePlus Seedance, Cloudflare AI Gateway via Replicate, fal.ai queue models, Replicate direct API models incl. P-Video, HeyGen Video Agent and scripted avatar videos), video translation and lipsync (HeyGen), TTS/STT/music (OpenAI, Gemini Flash TTS incl. Gemini 3.8 with delivery-style control, MiniMax, Cloudflare Workers AI MeloTTS, ElevenLabs), 3D models (BytePlus Hyper3D/Hitem3d), analyze/transcribe media, convert documents to Markdown, optimize media via ffmpeg/ImageMagick, and self-update the CLI. Use whenever the user wants to create, edit, or transform images/video/audio/3D/documents from a CLI, mentions any of: OpenAI, Codex image driver, image-to-image, i2i, image edit, watercolor/cyberpunk style transfer, reference image, OpenRouter, Nano Banana, Flux, Seedream, Veo, Hailuo, Seedance, Kling, Cloudflare Workers AI, AI Gateway, fal.ai, Atlas Cloud, Replicate, replicate.com models, Replicate predictions, HeyGen, avatar video, talking head, Video Agent, video translation, dubbing, lipsync, video generation, text-to-video, image-to-video, i2v, ElevenLabs voice cloning, updating/upgrading multix, or asks for batch media optimization.
|
|
4
|
-
version: 0.15.
|
|
4
|
+
version: 0.15.2 # x-release-please-version
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|
|
@@ -387,6 +387,7 @@ Requires `cwebp` (libwebp: `brew install webp`, `apt install webp`) on `PATH`.
|
|
|
387
387
|
|---|---|
|
|
388
388
|
| `MULTIX_OUTPUT_DIR` | Override default output dir |
|
|
389
389
|
| `MULTIX_MAX_DOWNLOAD_MB` | Size limit per downloaded Replicate output file (default 2048) |
|
|
390
|
+
| `MULTIX_UPLOAD_MIN_KBPS` | Slowest Gemini Files API upload speed to allow for, in KB/s (default 1000); lower it on slow links |
|
|
390
391
|
| `OPENAI_IMAGE_MODEL` / `OPENAI_TTS_MODEL` / `OPENAI_STT_MODEL` | OpenAI model defaults |
|
|
391
392
|
| `OPENROUTER_IMAGE_MODEL` | Default OpenRouter image model |
|
|
392
393
|
| `OPENROUTER_FALLBACK_MODELS` | CSV; applies to `generate` AND `i2i` |
|