@mrgoonie/multix 0.15.3 → 1.0.1
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 +18 -0
- package/README.md +2 -0
- package/dist/cli.js +557 -257
- package/package.json +1 -1
- package/skill/SKILL.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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
|
+
## [1.0.1](https://github.com/mrgoonie/multix-cli/compare/v1.0.0...v1.0.1) (2026-10-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
* **gemini:** move file-system work out of the Gemini client ([#109](https://github.com/mrgoonie/multix-cli/issues/109)) ([7df49b2](https://github.com/mrgoonie/multix-cli/commit/7df49b24994bf169cb5709adbada61663a66c786))
|
|
14
|
+
|
|
15
|
+
## [1.0.0](https://github.com/mrgoonie/multix-cli/compare/v0.15.3...v1.0.0) (2026-10-11)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### ⚠ BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* **core:** a project `.env` can no longer redirect a provider base URL or Cloudflare routing ID while the API key comes from the shell or `~/.multix/.env`. Keep the key in the same project `.env`, move the override to `~/.multix/.env`, or opt in explicitly as described in the environment reference.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
* **core:** only trust project .env endpoint overrides that ship their own key ([#97](https://github.com/mrgoonie/multix-cli/issues/97)) ([5c503ec](https://github.com/mrgoonie/multix-cli/commit/5c503eca08aeca6998cf5cf520180a32c0aeee0c))
|
|
25
|
+
|
|
8
26
|
## [0.15.3](https://github.com/mrgoonie/multix-cli/compare/v0.15.2...v0.15.3) (2026-10-11)
|
|
9
27
|
|
|
10
28
|
|
package/README.md
CHANGED
|
@@ -117,6 +117,8 @@ Set extra keys as `GEMINI_API_KEY_2`, `GEMINI_API_KEY_3`, … (any numeric suffi
|
|
|
117
117
|
|
|
118
118
|
Priority: `process.env` > `cwd/.env` > `~/.multix/.env`. Set `MULTIX_LEGACY_CLAUDE_ENV=1` to also read legacy AgentKit files (`.claude/skills/{ai-multimodal,ak-ai-multimodal}/.env`, `.claude/skills/.env`, `.claude/.env`, project first, then home) with the lowest priority (credential names only, never endpoint/proxy settings); `multix check -v` lists the env files that were loaded.
|
|
119
119
|
|
|
120
|
+
`*_BASE_URL`, `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_AI_GATEWAY_ID` decide where your API key is sent. Values from the shell or `~/.multix/.env` are always used. A value from the project `.env` (or a legacy `.claude` file under it) is used only when every key the provider sends with it comes from that same project file (the repository's own key). That protects your key, not your data: a project `.env` that ships its own key and base URL routes that project's requests, including your prompts and any files you upload, to that host, and its key replaces yours from `~/.multix/.env`, so review a cloned repository's `.env` before running `multix` in it. If a key comes from anywhere else, the project value is ignored: the `~/.multix/.env` value is used with a warning (variable, file and key source named, never the value), or, if there is none, the command stops with an error instead of falling back to the vendor default. `multix check` shows whether each such setting is in effect or ignored. To use a project value for one command, run `MULTIX_ALLOW_PROJECT_BASE_URL=1 multix ...`; do not put the opt-in in `~/.multix/.env`, which disables the check for every directory. Non-default base URLs must be `https://` (plain `http://` only for `localhost`, `127.0.0.1`, `[::1]`), and `multix` warns once per variable on stderr with the provider and hostname only. This covers base URLs and Cloudflare routing IDs only; other variables a project `.env` can set are tracked in [#99](https://github.com/mrgoonie/multix-cli/issues/99).
|
|
121
|
+
|
|
120
122
|
## Commands
|
|
121
123
|
|
|
122
124
|
### `multix check`
|
package/dist/cli.js
CHANGED
|
@@ -308,6 +308,11 @@ function validateRawInput(spec, body) {
|
|
|
308
308
|
|
|
309
309
|
// src/providers/atlascloud/models.ts
|
|
310
310
|
var DEFAULT_ATLAS_BASE_URL = "https://api.atlascloud.ai/api/v1";
|
|
311
|
+
var ATLAS_ROUTING = {
|
|
312
|
+
name: "ATLASCLOUD_BASE_URL",
|
|
313
|
+
defaultUrl: DEFAULT_ATLAS_BASE_URL,
|
|
314
|
+
keyEnvs: ["ATLASCLOUD_API_KEY"]
|
|
315
|
+
};
|
|
311
316
|
var SUBMIT_PATHS = {
|
|
312
317
|
image: "/model/generateImage",
|
|
313
318
|
video: "/model/generateVideo",
|
|
@@ -339,13 +344,20 @@ import fs2 from "fs/promises";
|
|
|
339
344
|
import path2 from "path";
|
|
340
345
|
|
|
341
346
|
// src/core/env-loader.ts
|
|
347
|
+
init_errors();
|
|
342
348
|
init_redact();
|
|
343
349
|
import fs from "fs";
|
|
344
350
|
import os from "os";
|
|
345
351
|
import path from "path";
|
|
346
|
-
import {
|
|
352
|
+
import { parse as dotenvParse } from "dotenv";
|
|
353
|
+
function envNameKey(name, platform = process.platform) {
|
|
354
|
+
return platform === "win32" ? name.toUpperCase() : name;
|
|
355
|
+
}
|
|
347
356
|
var loaded = false;
|
|
348
357
|
var loadedFiles = [];
|
|
358
|
+
var fileOrigins = /* @__PURE__ */ new Map();
|
|
359
|
+
var userFileValues = {};
|
|
360
|
+
var warned = /* @__PURE__ */ new Set();
|
|
349
361
|
var LEGACY_SKILL_DIRS = ["ai-multimodal", "ak-ai-multimodal"];
|
|
350
362
|
function legacyFilesUnder(root) {
|
|
351
363
|
const claude = path.join(root, ".claude");
|
|
@@ -368,8 +380,7 @@ function legacyClaudeEnvFiles(cwd, home) {
|
|
|
368
380
|
return files;
|
|
369
381
|
}
|
|
370
382
|
function legacyClaudeEnvEnabled(env = process.env) {
|
|
371
|
-
|
|
372
|
-
return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
383
|
+
return isTruthy(env.MULTIX_LEGACY_CLAUDE_ENV);
|
|
373
384
|
}
|
|
374
385
|
var LEGACY_ALLOWED_NAME = /^[A-Z][A-Z0-9_]*_(API_KEY(_\d+)?|API_TOKEN|ACCOUNT_ID|GATEWAY_ID)$|^(FAL_KEY|ARK_API_KEY)$/;
|
|
375
386
|
function isLegacyAllowedName(name) {
|
|
@@ -382,36 +393,174 @@ function isReadableFile(file) {
|
|
|
382
393
|
return false;
|
|
383
394
|
}
|
|
384
395
|
}
|
|
385
|
-
function
|
|
386
|
-
if (!isReadableFile(file)) return;
|
|
387
|
-
const result = dotenvConfig({ path: file, override: false });
|
|
388
|
-
if (!result.error) loadedFiles.push(file);
|
|
389
|
-
}
|
|
390
|
-
function loadLegacyFile(file) {
|
|
391
|
-
if (!isReadableFile(file)) return;
|
|
392
|
-
let parsed;
|
|
396
|
+
function parseFile(file) {
|
|
397
|
+
if (!isReadableFile(file)) return void 0;
|
|
393
398
|
try {
|
|
394
|
-
|
|
399
|
+
return dotenvParse(fs.readFileSync(file));
|
|
395
400
|
} catch {
|
|
396
|
-
return;
|
|
401
|
+
return void 0;
|
|
397
402
|
}
|
|
403
|
+
}
|
|
404
|
+
function loadFile(file, source, accept = () => true) {
|
|
405
|
+
const parsed = parseFile(file);
|
|
406
|
+
if (!parsed) return void 0;
|
|
398
407
|
for (const [name, value] of Object.entries(parsed)) {
|
|
399
|
-
if (
|
|
408
|
+
if (!accept(name) || process.env[name] !== void 0) continue;
|
|
409
|
+
process.env[name] = value;
|
|
410
|
+
fileOrigins.set(envNameKey(name), { source, file, value });
|
|
400
411
|
}
|
|
401
412
|
loadedFiles.push(file);
|
|
413
|
+
return parsed;
|
|
414
|
+
}
|
|
415
|
+
function byNameKey(values) {
|
|
416
|
+
const out = {};
|
|
417
|
+
for (const [name, value] of Object.entries(values ?? {})) {
|
|
418
|
+
const key = envNameKey(name);
|
|
419
|
+
if (!(key in out)) out[key] = value;
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
402
422
|
}
|
|
403
423
|
function loadEnv() {
|
|
404
424
|
if (loaded) return;
|
|
405
425
|
loaded = true;
|
|
406
426
|
loadedFiles = [];
|
|
427
|
+
fileOrigins = /* @__PURE__ */ new Map();
|
|
428
|
+
userFileValues = {};
|
|
407
429
|
const cwd = process.cwd();
|
|
408
430
|
const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os.homedir();
|
|
409
|
-
loadFile(path.resolve(cwd, ".env"));
|
|
410
|
-
if (home) loadFile(path.join(home, ".multix", ".env"));
|
|
431
|
+
loadFile(path.resolve(cwd, ".env"), "project");
|
|
432
|
+
if (home) userFileValues = byNameKey(loadFile(path.join(home, ".multix", ".env"), "user"));
|
|
411
433
|
if (legacyClaudeEnvEnabled()) {
|
|
412
|
-
|
|
434
|
+
const projectLegacy = new Set(legacyFilesUnder(cwd));
|
|
435
|
+
for (const file of legacyClaudeEnvFiles(cwd, home)) {
|
|
436
|
+
loadFile(file, projectLegacy.has(file) ? "project" : "legacy", isLegacyAllowedName);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function envFileOrigin(name) {
|
|
441
|
+
const origin = fileOrigins.get(envNameKey(name));
|
|
442
|
+
if (!origin || process.env[name] !== origin.value) return void 0;
|
|
443
|
+
return { source: origin.source, file: origin.file };
|
|
444
|
+
}
|
|
445
|
+
function isTruthy(value) {
|
|
446
|
+
return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
447
|
+
}
|
|
448
|
+
var ALLOW_PROJECT_BASE_URL_ENV = "MULTIX_ALLOW_PROJECT_BASE_URL";
|
|
449
|
+
function projectBaseUrlAllowed() {
|
|
450
|
+
if (!isTruthy(process.env[ALLOW_PROJECT_BASE_URL_ENV])) return false;
|
|
451
|
+
return envFileOrigin(ALLOW_PROJECT_BASE_URL_ENV)?.source !== "project";
|
|
452
|
+
}
|
|
453
|
+
var OPT_IN_HINT = `To use the project value for one command, run it as \`${ALLOW_PROJECT_BASE_URL_ENV}=1 multix ...\`. Do not put ${ALLOW_PROJECT_BASE_URL_ENV} in ~/.multix/.env: that trusts the .env of every directory you run multix in.`;
|
|
454
|
+
function describeSource(origin) {
|
|
455
|
+
if (!origin) return "the shell";
|
|
456
|
+
if (origin.source === "user") return "~/.multix/.env";
|
|
457
|
+
if (origin.source === "legacy") return origin.file;
|
|
458
|
+
return `another project file (${origin.file})`;
|
|
459
|
+
}
|
|
460
|
+
function foreignCredential(credentials, file) {
|
|
461
|
+
for (const spec of credentials) {
|
|
462
|
+
const names = typeof spec === "string" ? [spec] : spec;
|
|
463
|
+
const used = names.find((n) => process.env[n]?.trim());
|
|
464
|
+
if (!used) continue;
|
|
465
|
+
const origin = envFileOrigin(used);
|
|
466
|
+
if (origin?.source === "project" && origin.file === file) continue;
|
|
467
|
+
return `${used} comes from ${describeSource(origin)}`;
|
|
468
|
+
}
|
|
469
|
+
return void 0;
|
|
470
|
+
}
|
|
471
|
+
function inspectTrustedSetting(name, credentials, harmless) {
|
|
472
|
+
const value = process.env[name]?.trim() || void 0;
|
|
473
|
+
const origin = envFileOrigin(name);
|
|
474
|
+
if (origin?.source !== "project") {
|
|
475
|
+
return value ? { status: "used", value, source: origin?.source ?? "shell" } : { status: "unset" };
|
|
476
|
+
}
|
|
477
|
+
const userValue = userFileValues[envNameKey(name)]?.trim() || void 0;
|
|
478
|
+
if (value === void 0) {
|
|
479
|
+
return userValue ? { status: "fallback", value: userValue, file: origin.file, reason: "it is empty" } : { status: "unset" };
|
|
480
|
+
}
|
|
481
|
+
const project = (basis) => ({
|
|
482
|
+
status: "used",
|
|
483
|
+
value,
|
|
484
|
+
source: "project",
|
|
485
|
+
basis
|
|
486
|
+
});
|
|
487
|
+
if (value === harmless) return project("default");
|
|
488
|
+
if (projectBaseUrlAllowed()) return project("opt-in");
|
|
489
|
+
const reason = foreignCredential(credentials, origin.file);
|
|
490
|
+
if (!reason) return project("same-file");
|
|
491
|
+
return userValue ? { status: "fallback", value: userValue, file: origin.file, reason } : { status: "refused", file: origin.file, reason };
|
|
492
|
+
}
|
|
493
|
+
function trustedSettingRefusal(name, state) {
|
|
494
|
+
return `${name} is set in project env file ${state.file}, but ${state.reason}, so the project is not trusted to choose where that key is sent. Set ${name} in your shell or ~/.multix/.env, or keep the key in the same project file. ${OPT_IN_HINT}`;
|
|
495
|
+
}
|
|
496
|
+
function applyTrustedSetting(name, state) {
|
|
497
|
+
if (state.status === "unset") return void 0;
|
|
498
|
+
if (state.status === "used") return state.value;
|
|
499
|
+
if (state.status === "refused") throw new ConfigError(trustedSettingRefusal(name, state));
|
|
500
|
+
const hint = state.reason === "it is empty" ? "" : ` ${OPT_IN_HINT}`;
|
|
501
|
+
warnOnce(
|
|
502
|
+
`ignored:${name}`,
|
|
503
|
+
`Ignoring ${name} from project env file ${state.file} (${state.reason}); using the value from ~/.multix/.env.${hint}`
|
|
504
|
+
);
|
|
505
|
+
return state.value;
|
|
506
|
+
}
|
|
507
|
+
function warnOnce(key, message) {
|
|
508
|
+
if (warned.has(key)) return;
|
|
509
|
+
warned.add(key);
|
|
510
|
+
console.warn(`\u26A0 ${message}`);
|
|
511
|
+
}
|
|
512
|
+
function hostOf(url) {
|
|
513
|
+
try {
|
|
514
|
+
return new URL(url).hostname || "(no host)";
|
|
515
|
+
} catch {
|
|
516
|
+
return "(unparseable URL)";
|
|
413
517
|
}
|
|
414
518
|
}
|
|
519
|
+
var stripSlash = (url) => url.replace(/\/+$/, "");
|
|
520
|
+
var LOOPBACK_HTTP_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
521
|
+
function baseUrlSchemeError(name, value) {
|
|
522
|
+
let url;
|
|
523
|
+
try {
|
|
524
|
+
url = new URL(value);
|
|
525
|
+
} catch {
|
|
526
|
+
return `${name} must be an absolute https:// URL`;
|
|
527
|
+
}
|
|
528
|
+
if (url.protocol === "https:") return void 0;
|
|
529
|
+
if (url.protocol === "http:" && LOOPBACK_HTTP_HOSTS.has(url.hostname)) return void 0;
|
|
530
|
+
return `${name} must use https:// (host ${url.hostname || "(none)"}); plain http:// is only allowed for localhost, 127.0.0.1, or [::1]`;
|
|
531
|
+
}
|
|
532
|
+
function defaultHarmless(name, defaultUrl) {
|
|
533
|
+
const raw = process.env[name]?.trim();
|
|
534
|
+
return raw && stripSlash(raw) === stripSlash(defaultUrl) ? raw : void 0;
|
|
535
|
+
}
|
|
536
|
+
function inspectBaseUrl(name, defaultUrl, credentials) {
|
|
537
|
+
const state = inspectTrustedSetting(name, credentials, defaultHarmless(name, defaultUrl));
|
|
538
|
+
if (state.status !== "used" && state.status !== "fallback") return state;
|
|
539
|
+
if (stripSlash(state.value) === stripSlash(defaultUrl)) return state;
|
|
540
|
+
const schemeError = baseUrlSchemeError(name, state.value);
|
|
541
|
+
return schemeError ? { ...state, schemeError } : state;
|
|
542
|
+
}
|
|
543
|
+
function inspectProviderBaseUrl(routing) {
|
|
544
|
+
return inspectBaseUrl(routing.name, routing.defaultUrl, [routing.keyEnvs]);
|
|
545
|
+
}
|
|
546
|
+
function resolveBaseUrl(name, defaultUrl, credentials) {
|
|
547
|
+
const state = inspectBaseUrl(name, defaultUrl, credentials);
|
|
548
|
+
const value = applyTrustedSetting(name, state);
|
|
549
|
+
if (!value || stripSlash(value) === stripSlash(defaultUrl)) return defaultUrl;
|
|
550
|
+
if (state.schemeError) throw new ConfigError(state.schemeError);
|
|
551
|
+
const provider = name.replace(/_BASE_URL$/, "");
|
|
552
|
+
warnOnce(
|
|
553
|
+
`custom:${name}`,
|
|
554
|
+
`${name} is set: ${provider} API requests, including your API key, go to ${hostOf(value)} instead of the default host.`
|
|
555
|
+
);
|
|
556
|
+
return value;
|
|
557
|
+
}
|
|
558
|
+
function resolveProviderBaseUrl(routing) {
|
|
559
|
+
return resolveBaseUrl(routing.name, routing.defaultUrl, [routing.keyEnvs]);
|
|
560
|
+
}
|
|
561
|
+
function resolveRoutedKey(routing) {
|
|
562
|
+
return resolveFirstKey(...routing.keyEnvs)?.value;
|
|
563
|
+
}
|
|
415
564
|
function getLoadedEnvFiles() {
|
|
416
565
|
return [...loadedFiles];
|
|
417
566
|
}
|
|
@@ -1158,12 +1307,17 @@ var MODEL_ENV = {
|
|
|
1158
1307
|
function atlasDefaultModel(key) {
|
|
1159
1308
|
return resolveKey(MODEL_ENV[key])?.trim() || ATLAS_DEFAULT_MODELS[key];
|
|
1160
1309
|
}
|
|
1161
|
-
function
|
|
1162
|
-
const apiKey =
|
|
1310
|
+
function requireAtlasKey() {
|
|
1311
|
+
const apiKey = resolveRoutedKey(ATLAS_ROUTING);
|
|
1163
1312
|
if (!apiKey) {
|
|
1164
1313
|
throw new ConfigError(`ATLASCLOUD_API_KEY is not set. Get one at ${ATLAS_KEY_URL}`);
|
|
1165
1314
|
}
|
|
1166
|
-
return
|
|
1315
|
+
return apiKey;
|
|
1316
|
+
}
|
|
1317
|
+
function createAtlasClientFromEnv() {
|
|
1318
|
+
const apiKey = requireAtlasKey();
|
|
1319
|
+
const baseUrl = resolveProviderBaseUrl(ATLAS_ROUTING);
|
|
1320
|
+
return new AtlasCloudClient({ apiKey, baseUrl });
|
|
1167
1321
|
}
|
|
1168
1322
|
async function resolveMediaRef(client, ref, logger) {
|
|
1169
1323
|
const trimmed = ref.trim();
|
|
@@ -2346,6 +2500,11 @@ init_errors();
|
|
|
2346
2500
|
|
|
2347
2501
|
// src/providers/byteplus/models.ts
|
|
2348
2502
|
var DEFAULT_BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3";
|
|
2503
|
+
var BYTEPLUS_ROUTING = {
|
|
2504
|
+
name: "BYTEPLUS_BASE_URL",
|
|
2505
|
+
defaultUrl: DEFAULT_BYTEPLUS_BASE_URL,
|
|
2506
|
+
keyEnvs: ["BYTEPLUS_API_KEY", "ARK_API_KEY"]
|
|
2507
|
+
};
|
|
2349
2508
|
var DEFAULT_BYTEPLUS_IMAGE_MODEL = "seedream-4-0-250828";
|
|
2350
2509
|
var DEFAULT_BYTEPLUS_VIDEO_MODEL = "seedance-2.0";
|
|
2351
2510
|
var DEFAULT_BYTEPLUS_3D_MODEL = "hyper3d-gen2-260112";
|
|
@@ -2369,7 +2528,7 @@ function bytePlusVideoParamsMode() {
|
|
|
2369
2528
|
|
|
2370
2529
|
// src/providers/byteplus/client.ts
|
|
2371
2530
|
function requireBytePlusKey() {
|
|
2372
|
-
const key =
|
|
2531
|
+
const key = resolveRoutedKey(BYTEPLUS_ROUTING);
|
|
2373
2532
|
if (!key) {
|
|
2374
2533
|
throw new ConfigError(
|
|
2375
2534
|
"BytePlus API key not set. Set BYTEPLUS_API_KEY or ARK_API_KEY. Get one at https://console.byteplus.com/auth/api-keys"
|
|
@@ -2378,7 +2537,7 @@ function requireBytePlusKey() {
|
|
|
2378
2537
|
return key;
|
|
2379
2538
|
}
|
|
2380
2539
|
function bytePlusBaseUrl() {
|
|
2381
|
-
return
|
|
2540
|
+
return resolveProviderBaseUrl(BYTEPLUS_ROUTING);
|
|
2382
2541
|
}
|
|
2383
2542
|
var BytePlusClient = class {
|
|
2384
2543
|
constructor(apiKey, baseUrl = bytePlusBaseUrl()) {
|
|
@@ -3159,11 +3318,15 @@ var CLOUDFLARE_TTS_MODEL = "@cf/myshell-ai/melotts";
|
|
|
3159
3318
|
var CLOUDFLARE_VIDEO_MODEL = "prunaai/p-video";
|
|
3160
3319
|
|
|
3161
3320
|
// src/providers/cloudflare/models.ts
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3321
|
+
var CLOUDFLARE_ACCOUNT_ID_ENV = "CLOUDFLARE_ACCOUNT_ID";
|
|
3322
|
+
var CLOUDFLARE_GATEWAY_ID_ENV = "CLOUDFLARE_AI_GATEWAY_ID";
|
|
3323
|
+
var CLOUDFLARE_TOKEN_ENV = "CLOUDFLARE_API_TOKEN";
|
|
3324
|
+
var GATEWAY_REPLICATE_TOKEN_ENV = "REPLICATE_API_TOKEN";
|
|
3325
|
+
var CLOUDFLARE_NATIVE_CREDENTIALS = [CLOUDFLARE_TOKEN_ENV];
|
|
3326
|
+
var CLOUDFLARE_VIDEO_CREDENTIALS = [
|
|
3327
|
+
CLOUDFLARE_TOKEN_ENV,
|
|
3328
|
+
GATEWAY_REPLICATE_TOKEN_ENV
|
|
3329
|
+
];
|
|
3167
3330
|
function getCloudflareModel(kind, requested) {
|
|
3168
3331
|
const expected = kind === "image" ? CLOUDFLARE_IMAGE_MODEL : CLOUDFLARE_TTS_MODEL;
|
|
3169
3332
|
const overrideName = kind === "image" ? "CLOUDFLARE_AI_IMAGE_MODEL" : "CLOUDFLARE_AI_TTS_MODEL";
|
|
@@ -3183,22 +3346,57 @@ function validateImageSteps(value) {
|
|
|
3183
3346
|
}
|
|
3184
3347
|
return steps;
|
|
3185
3348
|
}
|
|
3186
|
-
|
|
3349
|
+
var requiredMessage = (name) => `${name} is required for Cloudflare media generation`;
|
|
3350
|
+
var token = (name) => resolveKey(name)?.trim() || void 0;
|
|
3351
|
+
function inspectCloudflareConfig(use = "native") {
|
|
3352
|
+
const credentials = use === "video" ? CLOUDFLARE_VIDEO_CREDENTIALS : CLOUDFLARE_NATIVE_CREDENTIALS;
|
|
3353
|
+
const accountId = inspectTrustedSetting(CLOUDFLARE_ACCOUNT_ID_ENV, credentials);
|
|
3354
|
+
const gatewayId = inspectTrustedSetting(CLOUDFLARE_GATEWAY_ID_ENV, credentials);
|
|
3355
|
+
const result = (problem, refused = false) => ({
|
|
3356
|
+
problem,
|
|
3357
|
+
refused,
|
|
3358
|
+
accountId,
|
|
3359
|
+
gatewayId
|
|
3360
|
+
});
|
|
3361
|
+
if (accountId.status === "refused") {
|
|
3362
|
+
return result(trustedSettingRefusal(CLOUDFLARE_ACCOUNT_ID_ENV, accountId), true);
|
|
3363
|
+
}
|
|
3364
|
+
if (accountId.status === "unset") return result(requiredMessage(CLOUDFLARE_ACCOUNT_ID_ENV));
|
|
3365
|
+
if (!token(CLOUDFLARE_TOKEN_ENV)) return result(requiredMessage(CLOUDFLARE_TOKEN_ENV));
|
|
3366
|
+
if (gatewayId.status === "refused") {
|
|
3367
|
+
return result(trustedSettingRefusal(CLOUDFLARE_GATEWAY_ID_ENV, gatewayId), true);
|
|
3368
|
+
}
|
|
3369
|
+
if (use === "video") {
|
|
3370
|
+
if (gatewayId.status === "unset") {
|
|
3371
|
+
return result(`${CLOUDFLARE_GATEWAY_ID_ENV} is required for Cloudflare video generation`);
|
|
3372
|
+
}
|
|
3373
|
+
if (!token(GATEWAY_REPLICATE_TOKEN_ENV)) {
|
|
3374
|
+
return result(requiredMessage(GATEWAY_REPLICATE_TOKEN_ENV));
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
return result();
|
|
3378
|
+
}
|
|
3379
|
+
function requireConfig(use) {
|
|
3380
|
+
const check = inspectCloudflareConfig(use);
|
|
3381
|
+
if (check.problem) throw new ConfigError(check.problem);
|
|
3187
3382
|
return {
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3383
|
+
// applyTrustedSetting prints the one-time warning when a ~/.multix/.env fallback is used.
|
|
3384
|
+
accountId: applyTrustedSetting(CLOUDFLARE_ACCOUNT_ID_ENV, check.accountId),
|
|
3385
|
+
apiToken: token(CLOUDFLARE_TOKEN_ENV),
|
|
3386
|
+
gatewayId: applyTrustedSetting(CLOUDFLARE_GATEWAY_ID_ENV, check.gatewayId),
|
|
3387
|
+
...use === "video" ? { replicateApiToken: token(GATEWAY_REPLICATE_TOKEN_ENV) } : {}
|
|
3191
3388
|
};
|
|
3192
3389
|
}
|
|
3390
|
+
function requireCloudflareConfig() {
|
|
3391
|
+
return requireConfig("native");
|
|
3392
|
+
}
|
|
3193
3393
|
function requireCloudflareVideoConfig() {
|
|
3194
|
-
const config =
|
|
3195
|
-
if (!config.gatewayId) {
|
|
3196
|
-
throw new ConfigError("CLOUDFLARE_AI_GATEWAY_ID is required for Cloudflare video generation");
|
|
3197
|
-
}
|
|
3394
|
+
const config = requireConfig("video");
|
|
3198
3395
|
return {
|
|
3199
|
-
|
|
3396
|
+
accountId: config.accountId,
|
|
3397
|
+
apiToken: config.apiToken,
|
|
3200
3398
|
gatewayId: config.gatewayId,
|
|
3201
|
-
replicateApiToken:
|
|
3399
|
+
replicateApiToken: config.replicateApiToken
|
|
3202
3400
|
};
|
|
3203
3401
|
}
|
|
3204
3402
|
|
|
@@ -4307,6 +4505,11 @@ import path22 from "path";
|
|
|
4307
4505
|
|
|
4308
4506
|
// src/providers/fal/models.ts
|
|
4309
4507
|
var DEFAULT_FAL_BASE_URL = "https://queue.fal.run";
|
|
4508
|
+
var FAL_ROUTING = {
|
|
4509
|
+
name: "FAL_BASE_URL",
|
|
4510
|
+
defaultUrl: DEFAULT_FAL_BASE_URL,
|
|
4511
|
+
keyEnvs: ["FAL_KEY"]
|
|
4512
|
+
};
|
|
4310
4513
|
var FAL_DEFAULTS = {
|
|
4311
4514
|
// FLUX.1 [schnell] — fast, low-cost default text-to-image model.
|
|
4312
4515
|
imageModel: process.env.FAL_IMAGE_MODEL ?? "fal-ai/flux/schnell",
|
|
@@ -4326,14 +4529,14 @@ init_errors();
|
|
|
4326
4529
|
init_errors();
|
|
4327
4530
|
init_redact();
|
|
4328
4531
|
function requireFalKey() {
|
|
4329
|
-
const key =
|
|
4532
|
+
const key = resolveRoutedKey(FAL_ROUTING);
|
|
4330
4533
|
if (!key) {
|
|
4331
4534
|
throw new ConfigError("FAL_KEY is not set. Get one at https://fal.ai/dashboard/keys");
|
|
4332
4535
|
}
|
|
4333
4536
|
return key;
|
|
4334
4537
|
}
|
|
4335
4538
|
function falBaseUrl() {
|
|
4336
|
-
return
|
|
4539
|
+
return resolveProviderBaseUrl(FAL_ROUTING);
|
|
4337
4540
|
}
|
|
4338
4541
|
var FalClient = class {
|
|
4339
4542
|
constructor(apiKey, baseUrl = falBaseUrl()) {
|
|
@@ -4947,118 +5150,9 @@ async function resolveFileInputs(files, opts = {}) {
|
|
|
4947
5150
|
}
|
|
4948
5151
|
|
|
4949
5152
|
// src/providers/gemini/client.ts
|
|
4950
|
-
import fs21 from "fs";
|
|
4951
|
-
import path25 from "path";
|
|
4952
|
-
init_errors();
|
|
4953
|
-
|
|
4954
|
-
// src/providers/gemini/key-pool.ts
|
|
4955
5153
|
init_errors();
|
|
4956
|
-
var PRIMARY_KEY_ENV = "GEMINI_API_KEY";
|
|
4957
|
-
var DEFAULT_COOLDOWN_MS = 6e4;
|
|
4958
|
-
var cooldownUntil = /* @__PURE__ */ new Map();
|
|
4959
|
-
function listGeminiKeys(env = process.env) {
|
|
4960
|
-
const numbered = Object.keys(env).map((name) => ({ name, match: /^GEMINI_API_KEY_(\d+)$/.exec(name) })).filter((e) => e.match !== null).sort((a, b) => Number(a.match[1]) - Number(b.match[1])).map((e) => e.name);
|
|
4961
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4962
|
-
const keys = [];
|
|
4963
|
-
for (const name of [PRIMARY_KEY_ENV, ...numbered]) {
|
|
4964
|
-
const value = env[name]?.trim();
|
|
4965
|
-
if (!value || seen.has(value)) continue;
|
|
4966
|
-
seen.add(value);
|
|
4967
|
-
keys.push({ name, value });
|
|
4968
|
-
}
|
|
4969
|
-
return keys;
|
|
4970
|
-
}
|
|
4971
|
-
function isGeminiRateLimitError(error) {
|
|
4972
|
-
if (!(error instanceof HttpError)) return false;
|
|
4973
|
-
if (error.status === 429) return true;
|
|
4974
|
-
return (error.status === 400 || error.status === 403) && error.snippet.includes("RESOURCE_EXHAUSTED");
|
|
4975
|
-
}
|
|
4976
|
-
function cooldownMs(error) {
|
|
4977
|
-
if (!(error instanceof HttpError)) return DEFAULT_COOLDOWN_MS;
|
|
4978
|
-
if (error.retryAfterMs !== void 0 && error.retryAfterMs > 0) return error.retryAfterMs;
|
|
4979
|
-
const delay = /"retryDelay"\s*:\s*"(\d+(?:\.\d+)?)s"/.exec(error.snippet);
|
|
4980
|
-
if (delay?.[1]) return Math.ceil(Number(delay[1]) * 1e3);
|
|
4981
|
-
return DEFAULT_COOLDOWN_MS;
|
|
4982
|
-
}
|
|
4983
|
-
function keysToTry(keys, now) {
|
|
4984
|
-
const until = (k) => cooldownUntil.get(k.value) ?? 0;
|
|
4985
|
-
const ready = keys.filter((k) => until(k) <= now);
|
|
4986
|
-
if (ready.length > 0) return ready;
|
|
4987
|
-
const soonest = [...keys].sort((a, b) => until(a) - until(b))[0];
|
|
4988
|
-
return soonest ? [soonest] : [];
|
|
4989
|
-
}
|
|
4990
|
-
async function withGeminiKey(fn, opts = {}) {
|
|
4991
|
-
const now = opts.now ?? Date.now;
|
|
4992
|
-
const keys = listGeminiKeys(opts.env);
|
|
4993
|
-
if (keys.length === 0) {
|
|
4994
|
-
throw new ConfigError(
|
|
4995
|
-
"GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
|
|
4996
|
-
);
|
|
4997
|
-
}
|
|
4998
|
-
const ordered = keysToTry(keys, now());
|
|
4999
|
-
let lastError;
|
|
5000
|
-
for (const [i, key] of ordered.entries()) {
|
|
5001
|
-
try {
|
|
5002
|
-
return await fn(key.value, key.name);
|
|
5003
|
-
} catch (error) {
|
|
5004
|
-
if (!isGeminiRateLimitError(error)) throw error;
|
|
5005
|
-
lastError = error;
|
|
5006
|
-
cooldownUntil.set(key.value, now() + cooldownMs(error));
|
|
5007
|
-
const next = ordered[i + 1];
|
|
5008
|
-
if (next) opts.logger?.warn(`${key.name} is rate-limited; retrying with ${next.name}`);
|
|
5009
|
-
}
|
|
5010
|
-
}
|
|
5011
|
-
if (keys.length > 1) {
|
|
5012
|
-
opts.logger?.warn(`All ${keys.length} Gemini keys are rate-limited; try again later`);
|
|
5013
|
-
}
|
|
5014
|
-
throw lastError;
|
|
5015
|
-
}
|
|
5016
5154
|
|
|
5017
|
-
// src/providers/gemini/
|
|
5018
|
-
import path24 from "path";
|
|
5019
|
-
var MIME_MAP = {
|
|
5020
|
-
// Audio
|
|
5021
|
-
".mp3": "audio/mp3",
|
|
5022
|
-
".wav": "audio/wav",
|
|
5023
|
-
".aac": "audio/aac",
|
|
5024
|
-
".flac": "audio/flac",
|
|
5025
|
-
".ogg": "audio/ogg",
|
|
5026
|
-
".aiff": "audio/aiff",
|
|
5027
|
-
".m4a": "audio/mp4",
|
|
5028
|
-
// Image
|
|
5029
|
-
".jpg": "image/jpeg",
|
|
5030
|
-
".jpeg": "image/jpeg",
|
|
5031
|
-
".png": "image/png",
|
|
5032
|
-
".webp": "image/webp",
|
|
5033
|
-
".heic": "image/heic",
|
|
5034
|
-
".heif": "image/heif",
|
|
5035
|
-
".gif": "image/gif",
|
|
5036
|
-
".bmp": "image/bmp",
|
|
5037
|
-
// Video
|
|
5038
|
-
".mp4": "video/mp4",
|
|
5039
|
-
".mpeg": "video/mpeg",
|
|
5040
|
-
".mov": "video/quicktime",
|
|
5041
|
-
".avi": "video/x-msvideo",
|
|
5042
|
-
".flv": "video/x-flv",
|
|
5043
|
-
".mpg": "video/mpeg",
|
|
5044
|
-
".webm": "video/webm",
|
|
5045
|
-
".wmv": "video/x-ms-wmv",
|
|
5046
|
-
".3gpp": "video/3gpp",
|
|
5047
|
-
// Document
|
|
5048
|
-
".pdf": "application/pdf",
|
|
5049
|
-
".txt": "text/plain",
|
|
5050
|
-
".html": "text/html",
|
|
5051
|
-
".htm": "text/html",
|
|
5052
|
-
".md": "text/markdown",
|
|
5053
|
-
".csv": "text/csv",
|
|
5054
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
5055
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
5056
|
-
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
5057
|
-
};
|
|
5058
|
-
function getMimeType(filePath) {
|
|
5059
|
-
const ext = path24.extname(filePath).toLowerCase();
|
|
5060
|
-
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
5061
|
-
}
|
|
5155
|
+
// src/providers/gemini/media-types.ts
|
|
5062
5156
|
function requiresProcessingWait(mimeType) {
|
|
5063
5157
|
return mimeType.startsWith("audio/") || mimeType.startsWith("video/");
|
|
5064
5158
|
}
|
|
@@ -5067,39 +5161,24 @@ function requiresProcessingWait(mimeType) {
|
|
|
5067
5161
|
var BASE = "https://generativelanguage.googleapis.com";
|
|
5068
5162
|
var FILES_BASE = `${BASE}/upload/v1beta/files`;
|
|
5069
5163
|
var GENERATE_BASE = `${BASE}/v1beta/models`;
|
|
5070
|
-
function withKey(opts, fn) {
|
|
5071
|
-
return opts.apiKey ? fn(opts.apiKey) : withGeminiKey(fn, { logger: opts.logger });
|
|
5072
|
-
}
|
|
5073
5164
|
function authHeaders2(apiKey) {
|
|
5074
5165
|
return { "x-goog-api-key": apiKey };
|
|
5075
5166
|
}
|
|
5076
5167
|
var DEFAULT_TIMEOUT_MS3 = 3e5;
|
|
5077
|
-
var UPLOAD_MIN_KBPS_ENV = "MULTIX_UPLOAD_MIN_KBPS";
|
|
5078
5168
|
var DEFAULT_MIN_UPLOAD_BYTES_PER_SEC = 1e6;
|
|
5079
5169
|
var UPLOAD_TIMEOUT_SLACK_MS = 6e4;
|
|
5080
|
-
function minUploadBytesPerSec() {
|
|
5081
|
-
const raw = resolveKey(UPLOAD_MIN_KBPS_ENV)?.trim();
|
|
5082
|
-
if (!raw) return DEFAULT_MIN_UPLOAD_BYTES_PER_SEC;
|
|
5083
|
-
const kbps = Number(raw);
|
|
5084
|
-
if (!Number.isFinite(kbps) || kbps < 1) {
|
|
5085
|
-
throw new ConfigError(
|
|
5086
|
-
`${UPLOAD_MIN_KBPS_ENV} must be a number of kilobytes per second, at least 1, got "${raw}"`
|
|
5087
|
-
);
|
|
5088
|
-
}
|
|
5089
|
-
return kbps * 1e3;
|
|
5090
|
-
}
|
|
5091
5170
|
function uploadTimeoutMs(size, timeoutMs = DEFAULT_TIMEOUT_MS3, bytesPerSec = DEFAULT_MIN_UPLOAD_BYTES_PER_SEC) {
|
|
5092
5171
|
return Math.max(timeoutMs, UPLOAD_TIMEOUT_SLACK_MS + Math.ceil(size / bytesPerSec * 1e3));
|
|
5093
5172
|
}
|
|
5094
|
-
async function uploadFile(
|
|
5095
|
-
const size =
|
|
5173
|
+
async function uploadFile(source, opts) {
|
|
5174
|
+
const size = source.blob.size;
|
|
5096
5175
|
if (size > FILES_API_MAX_BYTES) {
|
|
5097
5176
|
throw new ValidationError(
|
|
5098
|
-
`File too large for the Gemini Files API: ${
|
|
5177
|
+
`File too large for the Gemini Files API: ${source.displayName} is ${formatMb3(size)} MB (limit 2 GB = ${FILES_API_MAX_BYTES} bytes)`
|
|
5099
5178
|
);
|
|
5100
5179
|
}
|
|
5101
|
-
const
|
|
5102
|
-
return
|
|
5180
|
+
const { apiKey, minUploadBytesPerSec: minUploadBytesPerSec2 = DEFAULT_MIN_UPLOAD_BYTES_PER_SEC } = opts;
|
|
5181
|
+
return uploadFileWithKey(source, apiKey, { ...opts, bytesPerSec: minUploadBytesPerSec2 });
|
|
5103
5182
|
}
|
|
5104
5183
|
function formatMb3(bytes) {
|
|
5105
5184
|
return (bytes / 1024 / 1024).toFixed(2);
|
|
@@ -5148,11 +5227,10 @@ async function readFinalizedFile(resp) {
|
|
|
5148
5227
|
}
|
|
5149
5228
|
return file;
|
|
5150
5229
|
}
|
|
5151
|
-
async function uploadFileWithKey(
|
|
5230
|
+
async function uploadFileWithKey(source, apiKey, opts) {
|
|
5152
5231
|
const { timeoutMs = DEFAULT_TIMEOUT_MS3, logger, bytesPerSec } = opts;
|
|
5153
|
-
const mimeType =
|
|
5154
|
-
const
|
|
5155
|
-
const file = await fs21.openAsBlob(filePath);
|
|
5232
|
+
const { mimeType, displayName, label = source.displayName } = source;
|
|
5233
|
+
const file = source.blob.type ? source.blob.slice(0, source.blob.size, "") : source.blob;
|
|
5156
5234
|
logger?.debug(`Uploading ${displayName} (${formatMb3(file.size)} MB)`);
|
|
5157
5235
|
const start = await uploadFetch(
|
|
5158
5236
|
FILES_BASE,
|
|
@@ -5194,44 +5272,38 @@ async function uploadFileWithKey(filePath, apiKey, opts) {
|
|
|
5194
5272
|
const deadline = Date.now() + timeoutMs;
|
|
5195
5273
|
while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
|
|
5196
5274
|
await sleep2(2e3);
|
|
5197
|
-
fileRef = await
|
|
5275
|
+
fileRef = await fetchJson({
|
|
5198
5276
|
url: `${BASE}/v1beta/${fileRef.name}`,
|
|
5199
5277
|
headers: authHeaders2(apiKey)
|
|
5200
5278
|
}).then((r) => r.file);
|
|
5201
5279
|
logger?.debug(`Processing... state=${fileRef.state}`);
|
|
5202
5280
|
}
|
|
5203
5281
|
if (fileRef.state === "FAILED")
|
|
5204
|
-
throw new ProviderError(`File processing failed: ${
|
|
5282
|
+
throw new ProviderError(`File processing failed: ${label}`, "gemini");
|
|
5205
5283
|
if (fileRef.state === "PROCESSING")
|
|
5206
|
-
throw new ProviderError(`Processing timeout: ${
|
|
5284
|
+
throw new ProviderError(`Processing timeout: ${label}`, "gemini");
|
|
5207
5285
|
}
|
|
5208
5286
|
logger?.debug(`Uploaded: ${fileRef.name} (${fileRef.uri})`);
|
|
5209
5287
|
return fileRef;
|
|
5210
5288
|
}
|
|
5211
|
-
async function generateContent(req, opts
|
|
5212
|
-
return
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
}
|
|
5222
|
-
})
|
|
5223
|
-
);
|
|
5289
|
+
async function generateContent(req, opts) {
|
|
5290
|
+
return fetchJson({
|
|
5291
|
+
url: `${GENERATE_BASE}/${req.model}:generateContent`,
|
|
5292
|
+
method: "POST",
|
|
5293
|
+
headers: authHeaders2(opts.apiKey),
|
|
5294
|
+
body: {
|
|
5295
|
+
contents: req.contents,
|
|
5296
|
+
...req.generationConfig ? { generationConfig: req.generationConfig } : {}
|
|
5297
|
+
}
|
|
5298
|
+
});
|
|
5224
5299
|
}
|
|
5225
|
-
async function createInteraction(body, opts
|
|
5226
|
-
return
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
body
|
|
5233
|
-
})
|
|
5234
|
-
);
|
|
5300
|
+
async function createInteraction(body, opts) {
|
|
5301
|
+
return fetchJson({
|
|
5302
|
+
url: `${BASE}/v1beta/interactions`,
|
|
5303
|
+
method: "POST",
|
|
5304
|
+
headers: authHeaders2(opts.apiKey),
|
|
5305
|
+
body
|
|
5306
|
+
});
|
|
5235
5307
|
}
|
|
5236
5308
|
function extractInteractionAudio(resp) {
|
|
5237
5309
|
let found = null;
|
|
@@ -5270,6 +5342,150 @@ function extractImages(resp) {
|
|
|
5270
5342
|
(p) => p.inlineData !== void 0
|
|
5271
5343
|
).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
|
|
5272
5344
|
}
|
|
5345
|
+
function sleep2(ms) {
|
|
5346
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5347
|
+
}
|
|
5348
|
+
|
|
5349
|
+
// src/providers/gemini/key-pool.ts
|
|
5350
|
+
init_errors();
|
|
5351
|
+
var PRIMARY_KEY_ENV = "GEMINI_API_KEY";
|
|
5352
|
+
var DEFAULT_COOLDOWN_MS = 6e4;
|
|
5353
|
+
var cooldownUntil = /* @__PURE__ */ new Map();
|
|
5354
|
+
function listGeminiKeys(env = process.env) {
|
|
5355
|
+
const numbered = Object.keys(env).map((name) => ({ name, match: /^GEMINI_API_KEY_(\d+)$/.exec(name) })).filter((e) => e.match !== null).sort((a, b) => Number(a.match[1]) - Number(b.match[1])).map((e) => e.name);
|
|
5356
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5357
|
+
const keys = [];
|
|
5358
|
+
for (const name of [PRIMARY_KEY_ENV, ...numbered]) {
|
|
5359
|
+
const value = env[name]?.trim();
|
|
5360
|
+
if (!value || seen.has(value)) continue;
|
|
5361
|
+
seen.add(value);
|
|
5362
|
+
keys.push({ name, value });
|
|
5363
|
+
}
|
|
5364
|
+
return keys;
|
|
5365
|
+
}
|
|
5366
|
+
function isGeminiRateLimitError(error) {
|
|
5367
|
+
if (!(error instanceof HttpError)) return false;
|
|
5368
|
+
if (error.status === 429) return true;
|
|
5369
|
+
return (error.status === 400 || error.status === 403) && error.snippet.includes("RESOURCE_EXHAUSTED");
|
|
5370
|
+
}
|
|
5371
|
+
function cooldownMs(error) {
|
|
5372
|
+
if (!(error instanceof HttpError)) return DEFAULT_COOLDOWN_MS;
|
|
5373
|
+
if (error.retryAfterMs !== void 0 && error.retryAfterMs > 0) return error.retryAfterMs;
|
|
5374
|
+
const delay = /"retryDelay"\s*:\s*"(\d+(?:\.\d+)?)s"/.exec(error.snippet);
|
|
5375
|
+
if (delay?.[1]) return Math.ceil(Number(delay[1]) * 1e3);
|
|
5376
|
+
return DEFAULT_COOLDOWN_MS;
|
|
5377
|
+
}
|
|
5378
|
+
function keysToTry(keys, now) {
|
|
5379
|
+
const until = (k) => cooldownUntil.get(k.value) ?? 0;
|
|
5380
|
+
const ready = keys.filter((k) => until(k) <= now);
|
|
5381
|
+
if (ready.length > 0) return ready;
|
|
5382
|
+
const soonest = [...keys].sort((a, b) => until(a) - until(b))[0];
|
|
5383
|
+
return soonest ? [soonest] : [];
|
|
5384
|
+
}
|
|
5385
|
+
async function withGeminiKey(fn, opts = {}) {
|
|
5386
|
+
const now = opts.now ?? Date.now;
|
|
5387
|
+
const keys = listGeminiKeys(opts.env);
|
|
5388
|
+
if (keys.length === 0) {
|
|
5389
|
+
throw new ConfigError(
|
|
5390
|
+
"GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
|
|
5391
|
+
);
|
|
5392
|
+
}
|
|
5393
|
+
const ordered = keysToTry(keys, now());
|
|
5394
|
+
let lastError;
|
|
5395
|
+
for (const [i, key] of ordered.entries()) {
|
|
5396
|
+
try {
|
|
5397
|
+
return await fn(key.value, key.name);
|
|
5398
|
+
} catch (error) {
|
|
5399
|
+
if (!isGeminiRateLimitError(error)) throw error;
|
|
5400
|
+
lastError = error;
|
|
5401
|
+
cooldownUntil.set(key.value, now() + cooldownMs(error));
|
|
5402
|
+
const next = ordered[i + 1];
|
|
5403
|
+
if (next) opts.logger?.warn(`${key.name} is rate-limited; retrying with ${next.name}`);
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
if (keys.length > 1) {
|
|
5407
|
+
opts.logger?.warn(`All ${keys.length} Gemini keys are rate-limited; try again later`);
|
|
5408
|
+
}
|
|
5409
|
+
throw lastError;
|
|
5410
|
+
}
|
|
5411
|
+
|
|
5412
|
+
// src/providers/gemini/local-files.ts
|
|
5413
|
+
import fs21 from "fs";
|
|
5414
|
+
import path25 from "path";
|
|
5415
|
+
init_errors();
|
|
5416
|
+
|
|
5417
|
+
// src/providers/gemini/task-resolver.ts
|
|
5418
|
+
import path24 from "path";
|
|
5419
|
+
var MIME_MAP = {
|
|
5420
|
+
// Audio
|
|
5421
|
+
".mp3": "audio/mp3",
|
|
5422
|
+
".wav": "audio/wav",
|
|
5423
|
+
".aac": "audio/aac",
|
|
5424
|
+
".flac": "audio/flac",
|
|
5425
|
+
".ogg": "audio/ogg",
|
|
5426
|
+
".aiff": "audio/aiff",
|
|
5427
|
+
".m4a": "audio/mp4",
|
|
5428
|
+
// Image
|
|
5429
|
+
".jpg": "image/jpeg",
|
|
5430
|
+
".jpeg": "image/jpeg",
|
|
5431
|
+
".png": "image/png",
|
|
5432
|
+
".webp": "image/webp",
|
|
5433
|
+
".heic": "image/heic",
|
|
5434
|
+
".heif": "image/heif",
|
|
5435
|
+
".gif": "image/gif",
|
|
5436
|
+
".bmp": "image/bmp",
|
|
5437
|
+
// Video
|
|
5438
|
+
".mp4": "video/mp4",
|
|
5439
|
+
".mpeg": "video/mpeg",
|
|
5440
|
+
".mov": "video/quicktime",
|
|
5441
|
+
".avi": "video/x-msvideo",
|
|
5442
|
+
".flv": "video/x-flv",
|
|
5443
|
+
".mpg": "video/mpeg",
|
|
5444
|
+
".webm": "video/webm",
|
|
5445
|
+
".wmv": "video/x-ms-wmv",
|
|
5446
|
+
".3gpp": "video/3gpp",
|
|
5447
|
+
// Document
|
|
5448
|
+
".pdf": "application/pdf",
|
|
5449
|
+
".txt": "text/plain",
|
|
5450
|
+
".html": "text/html",
|
|
5451
|
+
".htm": "text/html",
|
|
5452
|
+
".md": "text/markdown",
|
|
5453
|
+
".csv": "text/csv",
|
|
5454
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
5455
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
5456
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
5457
|
+
};
|
|
5458
|
+
function getMimeType(filePath) {
|
|
5459
|
+
const ext = path24.extname(filePath).toLowerCase();
|
|
5460
|
+
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
5461
|
+
}
|
|
5462
|
+
|
|
5463
|
+
// src/providers/gemini/local-files.ts
|
|
5464
|
+
var INLINE_MAX_BYTES = 20 * 1024 * 1024;
|
|
5465
|
+
var UPLOAD_MIN_KBPS_ENV = "MULTIX_UPLOAD_MIN_KBPS";
|
|
5466
|
+
function minUploadBytesPerSec() {
|
|
5467
|
+
const raw = resolveKey(UPLOAD_MIN_KBPS_ENV)?.trim();
|
|
5468
|
+
if (!raw) return DEFAULT_MIN_UPLOAD_BYTES_PER_SEC;
|
|
5469
|
+
const kbps = Number(raw);
|
|
5470
|
+
if (!Number.isFinite(kbps) || kbps < 1) {
|
|
5471
|
+
throw new ConfigError(
|
|
5472
|
+
`${UPLOAD_MIN_KBPS_ENV} must be a number of kilobytes per second, at least 1, got "${raw}"`
|
|
5473
|
+
);
|
|
5474
|
+
}
|
|
5475
|
+
return kbps * 1e3;
|
|
5476
|
+
}
|
|
5477
|
+
async function uploadLocalFile(filePath, opts = {}) {
|
|
5478
|
+
const bytesPerSec = minUploadBytesPerSec();
|
|
5479
|
+
const blob = await fs21.openAsBlob(filePath);
|
|
5480
|
+
const source = {
|
|
5481
|
+
blob,
|
|
5482
|
+
displayName: path25.basename(filePath),
|
|
5483
|
+
mimeType: getMimeType(filePath),
|
|
5484
|
+
label: filePath
|
|
5485
|
+
};
|
|
5486
|
+
const upload = (apiKey) => uploadFile(source, { ...opts, apiKey, minUploadBytesPerSec: bytesPerSec });
|
|
5487
|
+
return opts.apiKey ? upload(opts.apiKey) : withGeminiKey(upload, { logger: opts.logger });
|
|
5488
|
+
}
|
|
5273
5489
|
function inlinePart(filePath) {
|
|
5274
5490
|
const data = fs21.readFileSync(filePath).toString("base64");
|
|
5275
5491
|
const mimeType = getMimeType(filePath);
|
|
@@ -5277,10 +5493,7 @@ function inlinePart(filePath) {
|
|
|
5277
5493
|
}
|
|
5278
5494
|
function shouldUseFileApi(filePath) {
|
|
5279
5495
|
const stat = fs21.statSync(filePath);
|
|
5280
|
-
return stat.size >
|
|
5281
|
-
}
|
|
5282
|
-
function sleep2(ms) {
|
|
5283
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5496
|
+
return stat.size > INLINE_MAX_BYTES;
|
|
5284
5497
|
}
|
|
5285
5498
|
|
|
5286
5499
|
// src/providers/gemini/models.ts
|
|
@@ -5416,7 +5629,7 @@ function registerAnalyzeCommand(parent) {
|
|
|
5416
5629
|
let contentParts;
|
|
5417
5630
|
if (useApi) {
|
|
5418
5631
|
logger.debug("File >20MB \u2014 using Files API");
|
|
5419
|
-
const ref = await
|
|
5632
|
+
const ref = await uploadLocalFile(filePath, { logger, apiKey });
|
|
5420
5633
|
contentParts = [
|
|
5421
5634
|
{ text: opts.prompt },
|
|
5422
5635
|
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
@@ -5479,7 +5692,7 @@ function registerExtractCommand(parent) {
|
|
|
5479
5692
|
const useApi = shouldUseFileApi(filePath);
|
|
5480
5693
|
let contentParts;
|
|
5481
5694
|
if (useApi) {
|
|
5482
|
-
const ref = await
|
|
5695
|
+
const ref = await uploadLocalFile(filePath, { logger, apiKey });
|
|
5483
5696
|
contentParts = [
|
|
5484
5697
|
{ text: opts.prompt },
|
|
5485
5698
|
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
@@ -5605,12 +5818,15 @@ async function generateGeminiSpeech(opts) {
|
|
|
5605
5818
|
logger?.debug(`Gemini TTS model=${model} mode=${speakers?.length ? "multi" : "single"}`);
|
|
5606
5819
|
let resp;
|
|
5607
5820
|
try {
|
|
5608
|
-
resp = await
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5821
|
+
resp = await withGeminiKey(
|
|
5822
|
+
(apiKey) => generateContent(
|
|
5823
|
+
{
|
|
5824
|
+
model,
|
|
5825
|
+
contents: [{ parts: [{ text }] }],
|
|
5826
|
+
generationConfig
|
|
5827
|
+
},
|
|
5828
|
+
{ apiKey, logger }
|
|
5829
|
+
),
|
|
5614
5830
|
{ logger }
|
|
5615
5831
|
);
|
|
5616
5832
|
} catch (e) {
|
|
@@ -5648,7 +5864,10 @@ async function generateInteractionsSpeech(opts) {
|
|
|
5648
5864
|
}
|
|
5649
5865
|
let resp;
|
|
5650
5866
|
try {
|
|
5651
|
-
resp = await
|
|
5867
|
+
resp = await withGeminiKey(
|
|
5868
|
+
(apiKey) => createInteraction(buildInteractionsSpeechBody(opts), { apiKey, logger }),
|
|
5869
|
+
{ logger }
|
|
5870
|
+
);
|
|
5652
5871
|
} catch (e) {
|
|
5653
5872
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
5654
5873
|
}
|
|
@@ -6006,12 +6225,15 @@ function registerGenerateCommand(parent) {
|
|
|
6006
6225
|
const savedFiles = [];
|
|
6007
6226
|
for (let i = 0; i < numImages; i++) {
|
|
6008
6227
|
try {
|
|
6009
|
-
const resp = await
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6228
|
+
const resp = await withGeminiKey(
|
|
6229
|
+
(apiKey) => generateContent(
|
|
6230
|
+
{
|
|
6231
|
+
model,
|
|
6232
|
+
contents: [{ parts: [{ text: opts.prompt }] }],
|
|
6233
|
+
generationConfig
|
|
6234
|
+
},
|
|
6235
|
+
{ apiKey, logger }
|
|
6236
|
+
),
|
|
6015
6237
|
{ logger }
|
|
6016
6238
|
);
|
|
6017
6239
|
const images = extractImages(resp);
|
|
@@ -6101,7 +6323,9 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
6101
6323
|
contents: [{ parts }],
|
|
6102
6324
|
generationConfig: { responseModalities: ["IMAGE"] }
|
|
6103
6325
|
};
|
|
6104
|
-
const resp = await generateContent(req, { logger })
|
|
6326
|
+
const resp = await withGeminiKey((apiKey) => generateContent(req, { apiKey, logger }), {
|
|
6327
|
+
logger
|
|
6328
|
+
});
|
|
6105
6329
|
const images = extractImages(resp);
|
|
6106
6330
|
if (images.length === 0) {
|
|
6107
6331
|
logger.error(
|
|
@@ -6234,7 +6458,7 @@ function registerTranscribeCommand2(parent) {
|
|
|
6234
6458
|
let contentParts;
|
|
6235
6459
|
if (useApi) {
|
|
6236
6460
|
logger.debug("File >20MB \u2014 using Files API");
|
|
6237
|
-
const ref = await
|
|
6461
|
+
const ref = await uploadLocalFile(filePath, { logger, apiKey });
|
|
6238
6462
|
contentParts = [
|
|
6239
6463
|
{ text: opts.prompt },
|
|
6240
6464
|
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
@@ -6291,6 +6515,11 @@ function registerGeminiCommands(program2) {
|
|
|
6291
6515
|
|
|
6292
6516
|
// src/providers/heygen/models.ts
|
|
6293
6517
|
var DEFAULT_HEYGEN_BASE_URL = "https://api.heygen.com";
|
|
6518
|
+
var HEYGEN_ROUTING = {
|
|
6519
|
+
name: "HEYGEN_BASE_URL",
|
|
6520
|
+
defaultUrl: DEFAULT_HEYGEN_BASE_URL,
|
|
6521
|
+
keyEnvs: ["HEYGEN_API_KEY"]
|
|
6522
|
+
};
|
|
6294
6523
|
var HEYGEN_API_SETTINGS_URL = "https://app.heygen.com/developers/api";
|
|
6295
6524
|
var HEYGEN_USAGE_URL = "https://app.heygen.com/developers/usage";
|
|
6296
6525
|
var AVATAR_VIDEO_RESOLUTIONS = ["4k", "1080p", "720p"];
|
|
@@ -7312,7 +7541,7 @@ function qualityMode(value) {
|
|
|
7312
7541
|
|
|
7313
7542
|
// src/providers/heygen/node-helpers.ts
|
|
7314
7543
|
function requireHeygenKey() {
|
|
7315
|
-
const key =
|
|
7544
|
+
const key = resolveRoutedKey(HEYGEN_ROUTING);
|
|
7316
7545
|
if (!key) {
|
|
7317
7546
|
throw new ConfigError(
|
|
7318
7547
|
`HEYGEN_API_KEY is not set. Create an API key at ${HEYGEN_API_SETTINGS_URL} (API usage is billed separately from HeyGen web plans).`
|
|
@@ -7322,13 +7551,10 @@ function requireHeygenKey() {
|
|
|
7322
7551
|
}
|
|
7323
7552
|
function createHeygenClientFromEnv() {
|
|
7324
7553
|
const apiKey = requireHeygenKey();
|
|
7325
|
-
const
|
|
7326
|
-
return new HeygenClient({
|
|
7327
|
-
apiKey,
|
|
7328
|
-
baseUrl: override ? checkBaseUrl(override, "HEYGEN_BASE_URL") : DEFAULT_HEYGEN_BASE_URL
|
|
7329
|
-
});
|
|
7554
|
+
const baseUrl = resolveProviderBaseUrl(HEYGEN_ROUTING);
|
|
7555
|
+
return new HeygenClient({ apiKey, baseUrl: checkBaseUrl(baseUrl, "HEYGEN_BASE_URL") });
|
|
7330
7556
|
}
|
|
7331
|
-
async function
|
|
7557
|
+
async function uploadLocalFile2(client, filePath, logger) {
|
|
7332
7558
|
const abs = path32.resolve(filePath);
|
|
7333
7559
|
let stat;
|
|
7334
7560
|
try {
|
|
@@ -7364,7 +7590,7 @@ async function resolveMediaInput(client, value, logger) {
|
|
|
7364
7590
|
const ref = classifyMediaRef(value);
|
|
7365
7591
|
if (ref.kind === "url") return { type: "url", url: ref.url };
|
|
7366
7592
|
if (ref.kind === "asset") return { type: "asset_id", asset_id: ref.assetId };
|
|
7367
|
-
return
|
|
7593
|
+
return uploadLocalFile2(client, ref.path, logger);
|
|
7368
7594
|
}
|
|
7369
7595
|
async function resolveMediaInputs(client, values, logger) {
|
|
7370
7596
|
const out = [];
|
|
@@ -8186,6 +8412,11 @@ init_errors();
|
|
|
8186
8412
|
|
|
8187
8413
|
// src/providers/leonardo/models.ts
|
|
8188
8414
|
var DEFAULT_LEONARDO_BASE_URL = "https://cloud.leonardo.ai/api/rest/v1";
|
|
8415
|
+
var LEONARDO_ROUTING = {
|
|
8416
|
+
name: "LEONARDO_BASE_URL",
|
|
8417
|
+
defaultUrl: DEFAULT_LEONARDO_BASE_URL,
|
|
8418
|
+
keyEnvs: ["LEONARDO_API_KEY"]
|
|
8419
|
+
};
|
|
8189
8420
|
var DEFAULT_LEONARDO_MODEL_ID = "7b592283-e8a7-4c5a-9ba6-d18c31f258b9";
|
|
8190
8421
|
var LEONARDO_DEFAULTS = {
|
|
8191
8422
|
modelId: process.env.LEONARDO_DEFAULT_MODEL ?? DEFAULT_LEONARDO_MODEL_ID,
|
|
@@ -8234,7 +8465,7 @@ function isGptImageModel(modelId) {
|
|
|
8234
8465
|
|
|
8235
8466
|
// src/providers/leonardo/client.ts
|
|
8236
8467
|
function requireLeonardoKey() {
|
|
8237
|
-
const key =
|
|
8468
|
+
const key = resolveRoutedKey(LEONARDO_ROUTING);
|
|
8238
8469
|
if (!key) {
|
|
8239
8470
|
throw new ConfigError(
|
|
8240
8471
|
"LEONARDO_API_KEY is not set. Get one at https://app.leonardo.ai/settings/api-keys"
|
|
@@ -8243,7 +8474,7 @@ function requireLeonardoKey() {
|
|
|
8243
8474
|
return key;
|
|
8244
8475
|
}
|
|
8245
8476
|
function leonardoBaseUrl() {
|
|
8246
|
-
return
|
|
8477
|
+
return resolveProviderBaseUrl(LEONARDO_ROUTING);
|
|
8247
8478
|
}
|
|
8248
8479
|
var LeonardoClient = class {
|
|
8249
8480
|
constructor(apiKey, baseUrl = leonardoBaseUrl()) {
|
|
@@ -10686,6 +10917,11 @@ function registerOpenRouterCommands(program2) {
|
|
|
10686
10917
|
// src/providers/replicate/models.ts
|
|
10687
10918
|
init_errors();
|
|
10688
10919
|
var DEFAULT_REPLICATE_BASE_URL = "https://api.replicate.com/v1";
|
|
10920
|
+
var REPLICATE_ROUTING = {
|
|
10921
|
+
name: "REPLICATE_BASE_URL",
|
|
10922
|
+
defaultUrl: DEFAULT_REPLICATE_BASE_URL,
|
|
10923
|
+
keyEnvs: ["REPLICATE_API_TOKEN", "REPLICATE_API_KEY"]
|
|
10924
|
+
};
|
|
10689
10925
|
var DEFAULT_REPLICATE_IMAGE_MODEL = "black-forest-labs/flux-schnell";
|
|
10690
10926
|
var DEFAULT_REPLICATE_VIDEO_MODEL = "prunaai/p-video";
|
|
10691
10927
|
var TERMINAL_STATUSES = [
|
|
@@ -11009,10 +11245,9 @@ var ReplicateClient = class {
|
|
|
11009
11245
|
};
|
|
11010
11246
|
|
|
11011
11247
|
// src/providers/replicate/node-client.ts
|
|
11012
|
-
var REPLICATE_TOKEN_ENV =
|
|
11013
|
-
var REPLICATE_TOKEN_FALLBACK_ENV = "REPLICATE_API_KEY";
|
|
11248
|
+
var [REPLICATE_TOKEN_ENV, REPLICATE_TOKEN_FALLBACK_ENV] = REPLICATE_ROUTING.keyEnvs;
|
|
11014
11249
|
function resolveReplicateToken() {
|
|
11015
|
-
const found = resolveFirstKey(
|
|
11250
|
+
const found = resolveFirstKey(...REPLICATE_ROUTING.keyEnvs);
|
|
11016
11251
|
return found ? { token: found.value, envUsed: found.envUsed } : void 0;
|
|
11017
11252
|
}
|
|
11018
11253
|
function requireReplicateToken() {
|
|
@@ -11025,7 +11260,7 @@ function requireReplicateToken() {
|
|
|
11025
11260
|
return resolved.token;
|
|
11026
11261
|
}
|
|
11027
11262
|
function replicateBaseUrl() {
|
|
11028
|
-
return
|
|
11263
|
+
return resolveProviderBaseUrl(REPLICATE_ROUTING);
|
|
11029
11264
|
}
|
|
11030
11265
|
function replicateImageModel() {
|
|
11031
11266
|
return resolveKey("REPLICATE_IMAGE_MODEL")?.trim() || DEFAULT_REPLICATE_IMAGE_MODEL;
|
|
@@ -11364,7 +11599,7 @@ async function saveOne(item, predictionId, index, count, opts) {
|
|
|
11364
11599
|
fs44.writeFileSync(dest, bytes);
|
|
11365
11600
|
return dest;
|
|
11366
11601
|
}
|
|
11367
|
-
const
|
|
11602
|
+
const warned2 = opts.warnedHosts;
|
|
11368
11603
|
try {
|
|
11369
11604
|
const { dest } = await downloadToFile(item.url, {
|
|
11370
11605
|
dest: (response) => {
|
|
@@ -11376,8 +11611,8 @@ async function saveOne(item, predictionId, index, count, opts) {
|
|
|
11376
11611
|
fetch: opts.fetch,
|
|
11377
11612
|
validateUrl: (url) => {
|
|
11378
11613
|
const { host, trusted } = checkOutputUrl(url);
|
|
11379
|
-
if (!trusted && !
|
|
11380
|
-
|
|
11614
|
+
if (!trusted && !warned2.has(host)) {
|
|
11615
|
+
warned2.add(host);
|
|
11381
11616
|
opts.logger.warn(
|
|
11382
11617
|
`Output ${item.path} is served from ${host}, not ${REPLICATE_DELIVERY_HOST}`
|
|
11383
11618
|
);
|
|
@@ -11807,6 +12042,10 @@ var FFMPEG_HINT = "ffmpeg not found \u2014 media optimize/split/batch commands u
|
|
|
11807
12042
|
var MAGICK_HINT = "ImageMagick (magick) not found \u2014 image optimization unavailable.\n Linux: sudo apt-get install imagemagick\n macOS: brew install imagemagick\n Windows: https://imagemagick.org/script/download.php";
|
|
11808
12043
|
|
|
11809
12044
|
// src/commands/check.ts
|
|
12045
|
+
function keyNames(routing) {
|
|
12046
|
+
const [envPrimary, envFallback] = routing.keyEnvs;
|
|
12047
|
+
return envFallback ? { envPrimary, envFallback } : { envPrimary };
|
|
12048
|
+
}
|
|
11810
12049
|
var PROVIDERS = [
|
|
11811
12050
|
{
|
|
11812
12051
|
name: "Gemini",
|
|
@@ -11834,14 +12073,13 @@ var PROVIDERS = [
|
|
|
11834
12073
|
},
|
|
11835
12074
|
{
|
|
11836
12075
|
name: "Leonardo.Ai",
|
|
11837
|
-
|
|
12076
|
+
...keyNames(LEONARDO_ROUTING),
|
|
11838
12077
|
optional: true,
|
|
11839
12078
|
link: "https://app.leonardo.ai/settings/api-keys"
|
|
11840
12079
|
},
|
|
11841
12080
|
{
|
|
11842
12081
|
name: "BytePlus",
|
|
11843
|
-
|
|
11844
|
-
envFallback: "ARK_API_KEY",
|
|
12082
|
+
...keyNames(BYTEPLUS_ROUTING),
|
|
11845
12083
|
optional: true,
|
|
11846
12084
|
link: "https://console.byteplus.com/auth/api-keys"
|
|
11847
12085
|
},
|
|
@@ -11853,26 +12091,25 @@ var PROVIDERS = [
|
|
|
11853
12091
|
},
|
|
11854
12092
|
{
|
|
11855
12093
|
name: "fal.ai",
|
|
11856
|
-
|
|
12094
|
+
...keyNames(FAL_ROUTING),
|
|
11857
12095
|
optional: true,
|
|
11858
12096
|
link: "https://fal.ai/dashboard/keys"
|
|
11859
12097
|
},
|
|
11860
12098
|
{
|
|
11861
12099
|
name: "Atlas Cloud",
|
|
11862
|
-
|
|
12100
|
+
...keyNames(ATLAS_ROUTING),
|
|
11863
12101
|
optional: true,
|
|
11864
12102
|
link: "https://www.atlascloud.ai/console/api-keys"
|
|
11865
12103
|
},
|
|
11866
12104
|
{
|
|
11867
12105
|
name: "Replicate",
|
|
11868
|
-
|
|
11869
|
-
envFallback: "REPLICATE_API_KEY",
|
|
12106
|
+
...keyNames(REPLICATE_ROUTING),
|
|
11870
12107
|
optional: true,
|
|
11871
12108
|
link: "https://replicate.com/account/api-tokens"
|
|
11872
12109
|
},
|
|
11873
12110
|
{
|
|
11874
12111
|
name: "HeyGen",
|
|
11875
|
-
|
|
12112
|
+
...keyNames(HEYGEN_ROUTING),
|
|
11876
12113
|
optional: true,
|
|
11877
12114
|
link: "https://app.heygen.com/developers/api"
|
|
11878
12115
|
}
|
|
@@ -11887,14 +12124,64 @@ function resolveProviderKey(p) {
|
|
|
11887
12124
|
return found ? { key: found.value, envUsed: found.envUsed } : {};
|
|
11888
12125
|
}
|
|
11889
12126
|
function cloudflareReadiness() {
|
|
11890
|
-
const
|
|
11891
|
-
const
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
12127
|
+
const nativeCheck = inspectCloudflareConfig("native");
|
|
12128
|
+
const videoCheck = inspectCloudflareConfig("video");
|
|
12129
|
+
return { native: !nativeCheck.problem, video: !videoCheck.problem, nativeCheck, videoCheck };
|
|
12130
|
+
}
|
|
12131
|
+
var BASE_URL_ROUTING = [
|
|
12132
|
+
ATLAS_ROUTING,
|
|
12133
|
+
BYTEPLUS_ROUTING,
|
|
12134
|
+
FAL_ROUTING,
|
|
12135
|
+
HEYGEN_ROUTING,
|
|
12136
|
+
LEONARDO_ROUTING,
|
|
12137
|
+
REPLICATE_ROUTING
|
|
12138
|
+
];
|
|
12139
|
+
function usedFrom(state) {
|
|
12140
|
+
if (state.source === "project") {
|
|
12141
|
+
if (state.basis === "default") return "project .env; same as the default endpoint";
|
|
12142
|
+
if (state.basis === "opt-in") return `project .env; allowed by ${ALLOW_PROJECT_BASE_URL_ENV}`;
|
|
12143
|
+
return "project .env, which also holds every key sent with it";
|
|
12144
|
+
}
|
|
12145
|
+
if (state.source === "user") return "~/.multix/.env";
|
|
12146
|
+
if (state.source === "legacy") return "legacy .claude file";
|
|
12147
|
+
return "shell";
|
|
12148
|
+
}
|
|
12149
|
+
function settingLine(label, state, commands = "commands that use it") {
|
|
12150
|
+
const stops = `${commands} will stop with an error`;
|
|
12151
|
+
if (state.status === "unset") return void 0;
|
|
12152
|
+
if (state.status === "refused") {
|
|
12153
|
+
return {
|
|
12154
|
+
level: "warn",
|
|
12155
|
+
message: `${label} ignored: set in project .env while ${state.reason}; ${stops}`
|
|
12156
|
+
};
|
|
12157
|
+
}
|
|
12158
|
+
if (state.schemeError) return { level: "warn", message: `${state.schemeError}; ${stops}` };
|
|
12159
|
+
if (state.status === "used") {
|
|
12160
|
+
return { level: "info", message: `${label} in effect (from ${usedFrom(state)})` };
|
|
12161
|
+
}
|
|
12162
|
+
const why = state.reason === "it is empty" ? "set empty in project .env" : `set in project .env while ${state.reason}`;
|
|
12163
|
+
return { level: "warn", message: `${label} ignored: ${why}; using ~/.multix/.env` };
|
|
12164
|
+
}
|
|
12165
|
+
var sameState = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
12166
|
+
function routingSettingReport() {
|
|
12167
|
+
const lines = [];
|
|
12168
|
+
const push = (line) => {
|
|
12169
|
+
if (line) lines.push(line);
|
|
11897
12170
|
};
|
|
12171
|
+
for (const routing of BASE_URL_ROUTING) {
|
|
12172
|
+
push(settingLine(routing.name, inspectProviderBaseUrl(routing)));
|
|
12173
|
+
}
|
|
12174
|
+
const { nativeCheck, videoCheck } = cloudflareReadiness();
|
|
12175
|
+
for (const [name, key] of [
|
|
12176
|
+
[CLOUDFLARE_ACCOUNT_ID_ENV, "accountId"],
|
|
12177
|
+
[CLOUDFLARE_GATEWAY_ID_ENV, "gatewayId"]
|
|
12178
|
+
]) {
|
|
12179
|
+
push(settingLine(name, nativeCheck[key]));
|
|
12180
|
+
if (!sameState(nativeCheck[key], videoCheck[key])) {
|
|
12181
|
+
push(settingLine(`${name} (video)`, videoCheck[key], "video commands"));
|
|
12182
|
+
}
|
|
12183
|
+
}
|
|
12184
|
+
return lines;
|
|
11898
12185
|
}
|
|
11899
12186
|
async function checkCodexImageReadiness(hasAnyProviderKey, verbose = false, checker = checkCodexAuthenticated) {
|
|
11900
12187
|
if (hasAnyProviderKey && !verbose) return false;
|
|
@@ -11958,18 +12245,31 @@ function registerCheckCommand(program2) {
|
|
|
11958
12245
|
}
|
|
11959
12246
|
}
|
|
11960
12247
|
const cloudflare = cloudflareReadiness();
|
|
12248
|
+
const blocked = (what) => logger.warn(
|
|
12249
|
+
`${what} will stop: a project .env setting is not trusted (see Endpoint Overrides)`
|
|
12250
|
+
);
|
|
11961
12251
|
if (cloudflare.native) {
|
|
11962
12252
|
anyKey = true;
|
|
11963
12253
|
logger.success("Cloudflare Workers AI credentials configured (image and speech)");
|
|
11964
12254
|
if (cloudflare.video) {
|
|
11965
12255
|
logger.success("Cloudflare AI Gateway video credentials configured");
|
|
12256
|
+
} else if (cloudflare.videoCheck.refused) {
|
|
12257
|
+
blocked("Cloudflare video");
|
|
11966
12258
|
} else {
|
|
11967
|
-
logger.info(
|
|
12259
|
+
logger.info(`Cloudflare video unavailable: ${cloudflare.videoCheck.problem}`);
|
|
11968
12260
|
}
|
|
12261
|
+
} else if (cloudflare.nativeCheck.refused) {
|
|
12262
|
+
blocked("Cloudflare image, speech and video");
|
|
11969
12263
|
} else {
|
|
11970
|
-
logger.info(
|
|
11971
|
-
|
|
11972
|
-
|
|
12264
|
+
logger.info(`Cloudflare media unavailable (optional): ${cloudflare.nativeCheck.problem}`);
|
|
12265
|
+
}
|
|
12266
|
+
const routing = routingSettingReport();
|
|
12267
|
+
if (routing.length > 0) {
|
|
12268
|
+
logger.header("Endpoint Overrides");
|
|
12269
|
+
for (const line of routing) {
|
|
12270
|
+
if (line.level === "warn") logger.warn(line.message);
|
|
12271
|
+
else logger.info(line.message);
|
|
12272
|
+
}
|
|
11973
12273
|
}
|
|
11974
12274
|
const shouldCheckCodex = !anyKey || opts.verbose;
|
|
11975
12275
|
let codexReady = false;
|
|
@@ -12096,7 +12396,7 @@ async function convertResolved(inputs, ctx) {
|
|
|
12096
12396
|
let contentParts;
|
|
12097
12397
|
if (useApi) {
|
|
12098
12398
|
logger?.debug("File >20MB \u2014 using Files API");
|
|
12099
|
-
const ref = await
|
|
12399
|
+
const ref = await uploadLocalFile(filePath, { logger, apiKey });
|
|
12100
12400
|
contentParts = [
|
|
12101
12401
|
{ text: prompt },
|
|
12102
12402
|
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrgoonie/multix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
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.
|
|
4
|
+
version: 1.0.1 # x-release-please-version
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|
|
@@ -414,6 +414,7 @@ Requires `cwebp` (libwebp: `brew install webp`, `apt install webp`) on `PATH`.
|
|
|
414
414
|
| `CLOUDFLARE_AI_IMAGE_MODEL` / `CLOUDFLARE_AI_TTS_MODEL` | Fixed-model settings: only `@cf/black-forest-labs/flux-1-schnell` and `@cf/myshell-ai/melotts` respectively are accepted |
|
|
415
415
|
| `MULTIX_DISABLE_HOME_ENV` | `1` to skip every home-directory env file (`~/.multix/.env`, `~/.claude/...`) |
|
|
416
416
|
| `MULTIX_LEGACY_CLAUDE_ENV` | `1` to also read legacy `.claude` env files (credential names only) |
|
|
417
|
+
| `MULTIX_ALLOW_PROJECT_BASE_URL` | Set `1` for one command (`MULTIX_ALLOW_PROJECT_BASE_URL=1 multix ...`) to honour `*_BASE_URL`, `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_AI_GATEWAY_ID` from the project `.env` even when the key comes from elsewhere. Without it, a project value is used only when every key sent with it is in the same project file (a project `.env` that ships its own key and base URL then receives that project's prompts and uploaded files, and its key replaces yours, so review a cloned repo's `.env` first); otherwise the `~/.multix/.env` value is used with a warning, or the command stops with a config error. `multix check` shows the effective state. Ignored in a project `.env`; avoid `~/.multix/.env` (it trusts every directory). Non-default base URLs must be `https://` (http only for localhost/127.0.0.1/[::1]) |
|
|
417
418
|
|
|
418
419
|
## Quick examples for agents
|
|
419
420
|
|