Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,13 @@ manual refresh via `mmx auth refresh`.
Useful for CI/CD (`mmx auth login --api-key sk-xxxxx`), or pass per-command via `--api-key`.

OAuth and API key are mutually exclusive — logging in with one clears the other.
Credential priority: `--api-key` flag > OAuth (config) > `api_key` (config).
Credential priority: `--api-key` flag > `MINIMAX_API_KEY` > OAuth (config) > `api_key` (config).

### Environment variables

| Variable | Description |
|---|---|
| `MINIMAX_API_KEY` | API key for non-interactive use. Overridden by `--api-key`; overrides saved credentials. |
| `MINIMAX_REGION` | `global` or `cn`. |
| `MINIMAX_BASE_URL` | Override the API base URL. |
| `MINIMAX_OUTPUT` | `text` or `json`. |
Expand Down
2 changes: 2 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ mmx auth logout
都保存在 `~/.mmx/config.json` 里,**两者互斥** —— 用一种登录会清掉另一种。
也可以每次通过 `--api-key` 直接传入。使用 API Key 登录时,会自动同时探测
Global 与中国两个 region,选用能通过的那个。
凭据优先级:`--api-key` 参数 > `MINIMAX_API_KEY` 环境变量 >
OAuth(配置文件)> `api_key`(配置文件)。

### `mmx config` · `mmx quota`

Expand Down
8 changes: 6 additions & 2 deletions src/auth/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import { ExitCode } from '../errors/codes';
import { buildNoCredentialsHint } from './hints';

export async function resolveCredential(config: Config): Promise<ResolvedCredential> {
// 1. --api-key flag
// 1. Explicit API key (--api-key takes precedence over MINIMAX_API_KEY)
if (config.apiKey) {
return { token: config.apiKey, method: 'api-key', source: 'flag' };
return {
token: config.apiKey,
method: 'api-key',
source: config.apiKeySource ?? 'flag',
};
}

// 2. OAuth credentials in config file
Expand Down
7 changes: 5 additions & 2 deletions src/auth/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ function stripScheme(url: string): string {
}

export async function ensureAuth(config: Config): Promise<void> {
if (config.apiKey || config.fileApiKey) return;
if (config.apiKey && config.apiKeySource !== 'env') return;
if (config.fileApiKey) return;
if (await loadCredentials()) return;

const envKey = process.env.MINIMAX_API_KEY;
const envKey = config.apiKeySource === 'env'
? config.apiKey
: process.env.MINIMAX_API_KEY;
if (envKey) {
if (!isInteractive({ nonInteractive: config.nonInteractive })) {
return; // env key is enough; no prompt
Expand Down
6 changes: 5 additions & 1 deletion src/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ export async function writeConfigFile(
export function loadConfig(flags: GlobalFlags): Config {
const file = readConfigFile();

const apiKey = flags.apiKey || undefined;
const flagApiKey = flags.apiKey || undefined;
const envApiKey = process.env.MINIMAX_API_KEY || undefined;
const apiKey = flagApiKey || envApiKey;
const apiKeySource = flagApiKey ? 'flag' : envApiKey ? 'env' : undefined;
const fileApiKey = file.api_key;

const explicitRegion = (flags.region as string) || process.env.MINIMAX_REGION || undefined;
Expand Down Expand Up @@ -100,6 +103,7 @@ export function loadConfig(flags: GlobalFlags): Config {

return {
apiKey,
apiKeySource,
fileApiKey,
fileRegion: file.region,
configPath: getConfigPath(),
Expand Down
1 change: 1 addition & 0 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export function parseConfigFile(raw: unknown): ConfigFile {

export interface Config {
apiKey?: string;
apiKeySource?: 'flag' | 'env';
fileApiKey?: string;
fileRegion?: Region;
configPath?: string;
Expand Down
6 changes: 5 additions & 1 deletion src/output/status-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export function maybeShowStatusBar(config: Config, token: string, model?: string

const filePath = config.configPath ? tildePath(config.configPath) : '~/.mmx/config.json';
const baseUrlStr = stripScheme(config.baseUrl);
const keySrc = config.apiKey ? '(flag)' : '(file)';
const keySrc = config.apiKeySource === 'env'
? '(env)'
: config.apiKey
? '(flag)'
: '(file)';
const maskedKey = maskToken(token);
const modelStr = model ? ` ${dim}|${reset} ${dim}Model:${reset} ${mmPurple}${model}${reset}` : '';

Expand Down
11 changes: 11 additions & 0 deletions test/auth/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ describe('resolveCredential', () => {
expect(cred.method).toBe('api-key');
});

it('preserves the environment credential source', async () => {
const config = makeConfig({
apiKey: 'sk-from-env',
apiKeySource: 'env',
});
const cred = await resolveCredential(config);
expect(cred.token).toBe('sk-from-env');
expect(cred.method).toBe('api-key');
expect(cred.source).toBe('env');
});

it('resolves from config file api key', async () => {
const config = makeConfig({ fileApiKey: 'sk-from-file' });
const cred = await resolveCredential(config);
Expand Down
23 changes: 23 additions & 0 deletions test/config/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@ describe('loadConfig', () => {
const testDir = join(tmpdir(), `mmx-config-test-${Date.now()}`);
const originalHome = process.env.HOME;
const originalRegion = process.env.MINIMAX_REGION;
const originalApiKey = process.env.MINIMAX_API_KEY;

beforeEach(() => {
mkdirSync(join(testDir, '.mmx'), { recursive: true });
process.env.HOME = testDir;
delete process.env.MINIMAX_REGION;
delete process.env.MINIMAX_API_KEY;
});

afterEach(() => {
process.env.HOME = originalHome;
if (originalRegion === undefined) delete process.env.MINIMAX_REGION;
else process.env.MINIMAX_REGION = originalRegion;
if (originalApiKey === undefined) delete process.env.MINIMAX_API_KEY;
else process.env.MINIMAX_API_KEY = originalApiKey;
rmSync(testDir, { recursive: true, force: true });
});

Expand All @@ -56,6 +60,25 @@ describe('loadConfig', () => {
expect(config.region).toBe('cn');
expect(config.baseUrl).toBe('https://api.minimaxi.com');
});

it('loads MINIMAX_API_KEY as an environment credential', () => {
process.env.MINIMAX_API_KEY = 'sk-from-env';

const config = loadConfig(baseFlags);

expect(config.apiKey).toBe('sk-from-env');
expect(config.apiKeySource).toBe('env');
expect(config.needsRegionDetection).toBe(true);
});

it('prefers --api-key over MINIMAX_API_KEY', () => {
process.env.MINIMAX_API_KEY = 'sk-from-env';

const config = loadConfig({ ...baseFlags, apiKey: 'sk-from-flag' });

expect(config.apiKey).toBe('sk-from-flag');
expect(config.apiKeySource).toBe('flag');
});
});

describe('writeConfigFile', () => {
Expand Down