@askalf/dario 6.13.28 → 6.13.30
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/dist/cli.js +3 -2
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/openai-backend.d.ts +5 -0
- package/dist/openai-backend.js +14 -0
- package/dist/proxy.d.ts +12 -0
- package/dist/proxy.js +36 -7
- package/docs/routing.md +25 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -64,7 +64,7 @@ import { grantAge, describeGrantAge } from './refresh-grant.js';
|
|
|
64
64
|
import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, addAccountViaManualOAuth, addAccountFromKeychain, KeychainImportError, removeAccount, ensureLoginCredentialsInPool, resyncLoginFromCredentialsIfStale, MIGRATED_LOGIN_ALIAS } from './accounts.js';
|
|
65
65
|
import { listCodexAccountAliases, loadAllCodexAccounts, startAddCodexAccount, completeAddCodexAccount, removeCodexAccount, parseCodexManualPaste } from './codex-accounts.js';
|
|
66
66
|
import { describeCodexWindow } from './codex-usage.js';
|
|
67
|
-
import { listBackends, saveBackend, removeBackend } from './openai-backend.js';
|
|
67
|
+
import { listBackends, saveBackend, removeBackend, defaultBackendBaseUrl } from './openai-backend.js';
|
|
68
68
|
import { parseOutboundProxy, installOutboundProxyWrapper } from './outbound-proxy.js';
|
|
69
69
|
// `args` / `command` at module scope — command handlers below close over
|
|
70
70
|
// `args` to read their own flags. Reading argv is harmless on import; only
|
|
@@ -1788,6 +1788,7 @@ async function backend() {
|
|
|
1788
1788
|
console.error(' dario backend add openai --key=sk-proj-...');
|
|
1789
1789
|
console.error(' dario backend add groq --key=gsk_... --base-url=https://api.groq.com/openai/v1');
|
|
1790
1790
|
console.error(' dario backend add openrouter --key=sk-or-... --base-url=https://openrouter.ai/api/v1');
|
|
1791
|
+
console.error(' dario backend add minimax --key=<api-key> (global; minimax-cn for China)');
|
|
1791
1792
|
console.error('');
|
|
1792
1793
|
process.exit(1);
|
|
1793
1794
|
}
|
|
@@ -1798,7 +1799,7 @@ async function backend() {
|
|
|
1798
1799
|
const keyArg = args.find(a => a.startsWith('--key='));
|
|
1799
1800
|
const baseUrlArg = args.find(a => a.startsWith('--base-url='));
|
|
1800
1801
|
const apiKey = keyArg ? keyArg.split('=').slice(1).join('=') : '';
|
|
1801
|
-
const baseUrl = baseUrlArg ? baseUrlArg.split('=').slice(1).join('=') :
|
|
1802
|
+
const baseUrl = baseUrlArg ? baseUrlArg.split('=').slice(1).join('=') : defaultBackendBaseUrl(name);
|
|
1802
1803
|
if (!apiKey) {
|
|
1803
1804
|
console.error('[dario] --key=<api-key> is required.');
|
|
1804
1805
|
process.exit(1);
|
|
@@ -518,7 +518,7 @@ export declare function detectDrift(t: TemplateData, installedOverride?: string
|
|
|
518
518
|
*/
|
|
519
519
|
export declare const SUPPORTED_CC_RANGE: {
|
|
520
520
|
readonly min: "1.0.0";
|
|
521
|
-
readonly maxTested: "2.1.
|
|
521
|
+
readonly maxTested: "2.1.309";
|
|
522
522
|
};
|
|
523
523
|
/**
|
|
524
524
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -1238,7 +1238,7 @@ export function detectDrift(t, installedOverride) {
|
|
|
1238
1238
|
*/
|
|
1239
1239
|
export const SUPPORTED_CC_RANGE = {
|
|
1240
1240
|
min: '1.0.0',
|
|
1241
|
-
maxTested: '2.1.
|
|
1241
|
+
maxTested: '2.1.309',
|
|
1242
1242
|
};
|
|
1243
1243
|
/**
|
|
1244
1244
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/openai-backend.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
export declare const MINIMAX_MODELS: readonly ["MiniMax-M3.1-Flash", "MiniMax-M3"];
|
|
3
|
+
/** Presets use the existing single OpenAI-compatible backend slot. */
|
|
4
|
+
export declare function defaultBackendBaseUrl(name: string): string;
|
|
5
|
+
/** Only direct endpoints imply a MiniMax catalog; custom gateways stay explicit. */
|
|
6
|
+
export declare function isMiniMaxBackend(backend: BackendCredentials | null): boolean;
|
|
2
7
|
export interface BackendCredentials {
|
|
3
8
|
provider: string;
|
|
4
9
|
name: string;
|
package/dist/openai-backend.js
CHANGED
|
@@ -17,6 +17,20 @@ import { join, basename } from 'node:path';
|
|
|
17
17
|
import { homedir } from 'node:os';
|
|
18
18
|
const DARIO_DIR = join(homedir(), '.dario');
|
|
19
19
|
const BACKENDS_DIR = join(DARIO_DIR, 'backends');
|
|
20
|
+
export const MINIMAX_MODELS = ['MiniMax-M3.1-Flash', 'MiniMax-M3'];
|
|
21
|
+
/** Presets use the existing single OpenAI-compatible backend slot. */
|
|
22
|
+
export function defaultBackendBaseUrl(name) {
|
|
23
|
+
if (name.toLowerCase() === 'minimax')
|
|
24
|
+
return 'https://api.minimax.io/v1';
|
|
25
|
+
if (name.toLowerCase() === 'minimax-cn')
|
|
26
|
+
return 'https://api.minimax.cn/v1';
|
|
27
|
+
return 'https://api.openai.com/v1';
|
|
28
|
+
}
|
|
29
|
+
/** Only direct endpoints imply a MiniMax catalog; custom gateways stay explicit. */
|
|
30
|
+
export function isMiniMaxBackend(backend) {
|
|
31
|
+
return backend?.provider === 'openai' &&
|
|
32
|
+
['https://api.minimax.io/v1', 'https://api.minimax.cn/v1', 'https://api.minimaxi.com/v1'].includes(backend.baseUrl.replace(/\/$/, ''));
|
|
33
|
+
}
|
|
20
34
|
/**
|
|
21
35
|
* Normalize a caller-supplied backend name into a filesystem-safe leaf.
|
|
22
36
|
* Strips any directory component and rejects names outside the allowed
|
package/dist/proxy.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type WriteStream } from 'node:fs';
|
|
|
3
3
|
import { getAccessToken, getStatus } from './oauth.js';
|
|
4
4
|
import { type EffortValue } from './cc-template.js';
|
|
5
5
|
import { type KeySocket } from './keys.js';
|
|
6
|
+
import { type BackendCredentials } from './openai-backend.js';
|
|
6
7
|
/**
|
|
7
8
|
* Assemble the `x-anthropic-billing-header` system-block text, following
|
|
8
9
|
* Claude Code's format:
|
|
@@ -87,6 +88,7 @@ export declare function budgetModelsFor(clientModel: string, ctx: {
|
|
|
87
88
|
cliProviderOverride: 'openai' | 'claude' | 'codex' | null;
|
|
88
89
|
cliModelRaw: string | null | undefined;
|
|
89
90
|
isOpenAI: boolean;
|
|
91
|
+
compatBackend?: CompatBackendKind;
|
|
90
92
|
}): string[];
|
|
91
93
|
/**
|
|
92
94
|
* Rebuild an OpenAI-shape request body with the model swapped to the pool-
|
|
@@ -95,6 +97,16 @@ export declare function budgetModelsFor(clientModel: string, ctx: {
|
|
|
95
97
|
* tests; the two call sites are the pool-exhausted dispatch paths.
|
|
96
98
|
*/
|
|
97
99
|
export declare function buildPoolFallbackBody(body: Buffer, fallbackModel: string): Buffer | null;
|
|
100
|
+
/** Which kind of OpenAI-compatible backend is configured: a direct MiniMax endpoint, any other gateway, or none. */
|
|
101
|
+
export type CompatBackendKind = 'minimax' | 'gateway' | null;
|
|
102
|
+
export declare function compatBackendKind(backend: BackendCredentials | null): CompatBackendKind;
|
|
103
|
+
/**
|
|
104
|
+
* The bare `minimax` model. On a direct MiniMax endpoint it is the shorthand for
|
|
105
|
+
* MiniMax-M3.1-Flash. On a custom gateway it is that gateway's own model id: the request goes
|
|
106
|
+
* to the gateway with the name unchanged (dario#1649 review). With no compatible backend it is
|
|
107
|
+
* left alone. `minimax:<model>` is explicit and always honored by the prefix parser.
|
|
108
|
+
*/
|
|
109
|
+
export declare function miniMaxShorthand(model: string, backend: CompatBackendKind): string;
|
|
98
110
|
export declare function parseProviderPrefix(model: string): {
|
|
99
111
|
provider: 'openai' | 'claude' | 'codex';
|
|
100
112
|
model: string;
|
package/dist/proxy.js
CHANGED
|
@@ -30,7 +30,7 @@ import { resolveSeatPin, SEAT_PIN_HEADER, SEAT_PIN_TOKEN_HEADER } from './seat-p
|
|
|
30
30
|
import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncLoginFromCredentialsIfStale, ensureLoginCredentialsInPool, mirrorLoginToCredentials, withObservedOrganization } from './accounts.js';
|
|
31
31
|
import { handleAdminRequest } from './admin-api.js';
|
|
32
32
|
import { createTokenBucket } from './rate-limit.js';
|
|
33
|
-
import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
|
|
33
|
+
import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI, isMiniMaxBackend, MINIMAX_MODELS } from './openai-backend.js';
|
|
34
34
|
import { forwardToCodex, forwardResponsesToCodex, getCodexModelSlugs, peekCodexModelSlugs, isCodexModel, pickCodexFallback, pickClaudeTarget, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
|
|
35
35
|
import { effortForCodex } from './effort.js';
|
|
36
36
|
import { MidstreamGuard, guardFor, loopbackBaseFor, chaosCutFetch, chaosCutState, CONTINUATION_HEADER, CONTINUATION_OF_HEADER, MAX_CONTINUATION_DEPTH, continuationDepth, continuationOfRequest } from './midstream.js';
|
|
@@ -378,7 +378,7 @@ export function selectModelOverride(incomingModel, modelOverride, fastModelOverr
|
|
|
378
378
|
*/
|
|
379
379
|
export function budgetModelsFor(clientModel, ctx) {
|
|
380
380
|
const out = new Set();
|
|
381
|
-
const aliased = applyModelAlias(clientModel, ctx.modelAliases) ?? clientModel;
|
|
381
|
+
const aliased = miniMaxShorthand(applyModelAlias(clientModel, ctx.modelAliases) ?? clientModel, ctx.compatBackend ?? null);
|
|
382
382
|
const prefix = parseProviderPrefix(aliased);
|
|
383
383
|
if (prefix) {
|
|
384
384
|
out.add(prefix.provider === 'claude' ? resolveClaudeAlias(parseEffortSuffix(prefix.model).model) : prefix.model);
|
|
@@ -406,6 +406,7 @@ const PROVIDER_PREFIXES = {
|
|
|
406
406
|
openai: 'openai',
|
|
407
407
|
openrouter: 'openai',
|
|
408
408
|
groq: 'openai',
|
|
409
|
+
minimax: 'openai',
|
|
409
410
|
compat: 'openai',
|
|
410
411
|
local: 'openai',
|
|
411
412
|
codex: 'codex',
|
|
@@ -431,6 +432,26 @@ export function buildPoolFallbackBody(body, fallbackModel) {
|
|
|
431
432
|
return null;
|
|
432
433
|
}
|
|
433
434
|
}
|
|
435
|
+
export function compatBackendKind(backend) {
|
|
436
|
+
if (!backend)
|
|
437
|
+
return null;
|
|
438
|
+
return isMiniMaxBackend(backend) ? 'minimax' : 'gateway';
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* The bare `minimax` model. On a direct MiniMax endpoint it is the shorthand for
|
|
442
|
+
* MiniMax-M3.1-Flash. On a custom gateway it is that gateway's own model id: the request goes
|
|
443
|
+
* to the gateway with the name unchanged (dario#1649 review). With no compatible backend it is
|
|
444
|
+
* left alone. `minimax:<model>` is explicit and always honored by the prefix parser.
|
|
445
|
+
*/
|
|
446
|
+
export function miniMaxShorthand(model, backend) {
|
|
447
|
+
if (model.toLowerCase() !== 'minimax')
|
|
448
|
+
return model;
|
|
449
|
+
if (backend === 'minimax')
|
|
450
|
+
return `minimax:${MINIMAX_MODELS[0]}`;
|
|
451
|
+
if (backend === 'gateway')
|
|
452
|
+
return `openai:${model}`;
|
|
453
|
+
return model;
|
|
454
|
+
}
|
|
434
455
|
export function parseProviderPrefix(model) {
|
|
435
456
|
const idx = model.indexOf(':');
|
|
436
457
|
if (idx <= 0)
|
|
@@ -1915,14 +1936,14 @@ export async function startProxy(opts = {}) {
|
|
|
1915
1936
|
// Parse --model once at startup. Supports `<provider>:<model>` to force
|
|
1916
1937
|
// a backend for every request (e.g. `--model=openai:gpt-4o`). Back-compat:
|
|
1917
1938
|
// bare names like `opus` resolve via MODEL_ALIASES.
|
|
1918
|
-
const modelPrefix = opts.model ? parseProviderPrefix(opts.model) : null;
|
|
1939
|
+
const modelPrefix = opts.model ? parseProviderPrefix(miniMaxShorthand(opts.model, compatBackendKind(openaiBackend))) : null;
|
|
1919
1940
|
const cliModelRaw = modelPrefix ? modelPrefix.model : opts.model;
|
|
1920
1941
|
const cliProviderOverride = modelPrefix ? modelPrefix.provider : null;
|
|
1921
1942
|
const modelOverride = cliModelRaw ? resolveClaudeAlias(cliModelRaw) : null;
|
|
1922
1943
|
// --fast-model: the model that Haiku-tier (Claude Code sub-agent) requests
|
|
1923
1944
|
// route to instead of the forced `--model`, so sub-agents stay cheap. Parsed
|
|
1924
1945
|
// like --model (alias + optional provider prefix). Null = disabled.
|
|
1925
|
-
const fastModelPrefix = opts.fastModel ? parseProviderPrefix(opts.fastModel) : null;
|
|
1946
|
+
const fastModelPrefix = opts.fastModel ? parseProviderPrefix(miniMaxShorthand(opts.fastModel, compatBackendKind(openaiBackend))) : null;
|
|
1926
1947
|
const fastModelRaw = fastModelPrefix ? fastModelPrefix.model : opts.fastModel;
|
|
1927
1948
|
const fastModelOverride = fastModelRaw ? resolveClaudeAlias(fastModelRaw) : null;
|
|
1928
1949
|
const identity = loadClaudeIdentity();
|
|
@@ -3146,7 +3167,8 @@ export async function startProxy(opts = {}) {
|
|
|
3146
3167
|
// User aliases are advertised after the real ids so pickers offer
|
|
3147
3168
|
// them; already-advertised names aren't duplicated (an alias that
|
|
3148
3169
|
// shadows a real id still applies at request time).
|
|
3149
|
-
const
|
|
3170
|
+
const minimaxNames = isMiniMaxBackend(openaiBackend) ? [...MINIMAX_MODELS] : [];
|
|
3171
|
+
const advertised = withLongContextVariants(catalog.bases).concat(minimaxNames);
|
|
3150
3172
|
const aliasNames = Object.keys(modelAliases).filter((n) => !advertised.includes(n));
|
|
3151
3173
|
// Codex/ChatGPT-subscription slugs the backend lists for the stored
|
|
3152
3174
|
// account (dario#1010), so a client's model picker can discover the
|
|
@@ -3164,7 +3186,8 @@ export async function startProxy(opts = {}) {
|
|
|
3164
3186
|
// Codex slugs are served by the ChatGPT subscription, not Anthropic, so
|
|
3165
3187
|
// they advertise `owned_by: "openai"` (dario#1137).
|
|
3166
3188
|
const codexOwners = Object.fromEntries(codexNames.map((s) => [s, 'openai']));
|
|
3167
|
-
const
|
|
3189
|
+
const minimaxOwners = Object.fromEntries(minimaxNames.map((s) => [s, 'minimax']));
|
|
3190
|
+
const body = JSON.stringify(buildOpenAIModelsList(advertised.concat(aliasNames, codexNames), { ...codexOwners, ...minimaxOwners }));
|
|
3168
3191
|
res.writeHead(200, { ...JSON_HEADERS, 'Access-Control-Allow-Origin': corsOrigin });
|
|
3169
3192
|
res.end(body);
|
|
3170
3193
|
return;
|
|
@@ -3749,6 +3772,7 @@ export async function startProxy(opts = {}) {
|
|
|
3749
3772
|
: resolveMaxTokens(opts.maxTokens, { max_tokens: clientMax });
|
|
3750
3773
|
const billedAs = budgetModelsFor(typeof pb?.model === 'string' ? pb.model : '', {
|
|
3751
3774
|
modelAliases, modelOverride, fastModelOverride, cliProviderOverride, cliModelRaw, isOpenAI,
|
|
3775
|
+
compatBackend: compatBackendKind(openaiBackend),
|
|
3752
3776
|
});
|
|
3753
3777
|
budgetReserved = requestBudgetReservation(billedAs, body.length, outboundMax, costOfTokensFailClosed, Date.now(), passthrough ? 0 : CC_TEMPLATE_PROMPT_BYTES);
|
|
3754
3778
|
budgetInflightKey = requestAuth.key.name;
|
|
@@ -3852,7 +3876,7 @@ export async function startProxy(opts = {}) {
|
|
|
3852
3876
|
}
|
|
3853
3877
|
}
|
|
3854
3878
|
const rawModel = parsed.model ?? '';
|
|
3855
|
-
const prefix = parseProviderPrefix(rawModel);
|
|
3879
|
+
const prefix = parseProviderPrefix(miniMaxShorthand(rawModel, compatBackendKind(openaiBackend)));
|
|
3856
3880
|
if (prefix) {
|
|
3857
3881
|
forcedProvider = prefix.provider;
|
|
3858
3882
|
// dario#419 — optional effort suffix (model:high / model-high). Claude
|
|
@@ -3884,6 +3908,11 @@ export async function startProxy(opts = {}) {
|
|
|
3884
3908
|
parsed.model = cliModelRaw;
|
|
3885
3909
|
body = Buffer.from(JSON.stringify(parsed));
|
|
3886
3910
|
}
|
|
3911
|
+
else if (isOpenAI && forcedProvider === null && isMiniMaxBackend(openaiBackend) && /^MiniMax-M/i.test(rawModel)) {
|
|
3912
|
+
// Direct MiniMax text IDs, including saved legacy IDs, use the
|
|
3913
|
+
// compatible backend unchanged. Gateway IDs still need a prefix.
|
|
3914
|
+
forcedProvider = 'openai';
|
|
3915
|
+
}
|
|
3887
3916
|
else if (!isOpenAIModel(rawModel) && forcedProvider !== 'openai') {
|
|
3888
3917
|
// dario#419 — bare Claude model name carrying an effort suffix with no
|
|
3889
3918
|
// provider prefix (e.g. Cursor's `claude-opus-4-8-high`). OpenAI-bound
|
package/docs/routing.md
CHANGED
|
@@ -16,6 +16,31 @@ The tool doesn't know. The backend doesn't know. dario is the seam.
|
|
|
16
16
|
|
|
17
17
|
**The full Claude lineup, autodetected.** Fable 5, Opus 5.5, Opus 5, Sonnet 5 and Haiku 4.5, plus `[1m]` long-context variants on every family except Haiku, by full id (`claude-opus-5-5`) or shortcut (`fable` / `opus` / `sonnet` / `haiku`; append `1m` for the long-context form; `opus5` / `opus48` / `opus47` / `opus46` / `sonnet46` pin a generation and never float). `GET /v1/models` reads Anthropic's live catalog (TTL-cached, baked fallback offline), so a new model resolves the day it lands with no dario release, and the model-specific request shape is applied automatically. Families pulled upstream are filtered from both the live catalog and the fallback, so `/v1/models` never advertises a model that 404s. A name no provider lists at all, such as a ChatGPT slug your account doesn't have or a typo that belongs to no family, is refused locally with `400` and `x-dario-upstream-rejection: model_unroutable` instead of spending a pool request on an upstream 404. The guard steps aside for `claude-*` names (Anthropic's own 404 stays authoritative there), for requests under a `--model` / `--fast-model` override, for upstream-API-key mode, and for the legacy OpenAI names the built-in map translates.
|
|
18
18
|
|
|
19
|
+
## MiniMax API-key backend
|
|
20
|
+
|
|
21
|
+
`dario backend add minimax --key=<api-key>` uses `https://api.minimax.io/v1`.
|
|
22
|
+
Use the name `minimax-cn` for `https://api.minimax.cn/v1`, or pass `--base-url`
|
|
23
|
+
to keep a custom endpoint. This uses the existing single compatible-backend
|
|
24
|
+
slot; it does not add a second simultaneously active API-key backend. Restart
|
|
25
|
+
the proxy after changing its backend. With no subscription account, start it
|
|
26
|
+
with `dario proxy --no-claude-auth`.
|
|
27
|
+
|
|
28
|
+
On a direct MiniMax endpoint, model `minimax` on `/v1/chat/completions` selects
|
|
29
|
+
`MiniMax-M3.1-Flash`; on a custom gateway, `minimax` is that gateway's own model id and the request goes to the gateway with the name unchanged.
|
|
30
|
+
Direct endpoints advertise `MiniMax-M3.1-Flash` followed by `MiniMax-M3` in
|
|
31
|
+
`/v1/models`; either full ID routes directly. Use `minimax:<model>` for an
|
|
32
|
+
explicit legacy or custom ID, or `--model=minimax:MiniMax-M3.1-Flash` for a server-wide Flash
|
|
33
|
+
override. Custom gateways retain their own IDs and are not assigned a direct
|
|
34
|
+
MiniMax catalog. This API-key route supports Chat Completions, not Messages.
|
|
35
|
+
|
|
36
|
+
Request fields and streamed responses pass through unchanged. Flash defaults
|
|
37
|
+
to adaptive thinking and `reasoning_effort: "max"` upstream; it accepts `low`,
|
|
38
|
+
`medium`, `high`, `xhigh`, and `max`. Do not send `thinking.type: "disabled"`,
|
|
39
|
+
`reasoning_effort: "none"`, `service_tier`, or `reasoning_split: false` to Flash.
|
|
40
|
+
M3 also accepts disabled thinking. The proxy does not rewrite explicit settings
|
|
41
|
+
or remove image/video content parts, tool calls, or reasoning history.
|
|
42
|
+
See the [MiniMax API reference](https://platform.minimax.io/docs/api-reference/text-openai-api).
|
|
43
|
+
|
|
19
44
|
---
|
|
20
45
|
|
|
21
46
|
[← README](../README.md) · [all reference docs](../README.md#reference)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.13.
|
|
3
|
+
"version": "6.13.30",
|
|
4
4
|
"description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, with automatic failover when one hits its limit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|