diff --git a/README.md b/README.md index ddeafd8..e838e49 100644 --- a/README.md +++ b/README.md @@ -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`. | diff --git a/README_CN.md b/README_CN.md index db48fb4..49ab43e 100644 --- a/README_CN.md +++ b/README_CN.md @@ -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` diff --git a/src/auth/resolver.ts b/src/auth/resolver.ts index 8046aaf..b505776 100644 --- a/src/auth/resolver.ts +++ b/src/auth/resolver.ts @@ -7,9 +7,13 @@ import { ExitCode } from '../errors/codes'; import { buildNoCredentialsHint } from './hints'; export async function resolveCredential(config: Config): Promise { - // 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 diff --git a/src/auth/setup.ts b/src/auth/setup.ts index 0b404b7..bda5c4f 100644 --- a/src/auth/setup.ts +++ b/src/auth/setup.ts @@ -26,10 +26,13 @@ function stripScheme(url: string): string { } export async function ensureAuth(config: Config): Promise { - 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 diff --git a/src/config/loader.ts b/src/config/loader.ts index 7768044..fec543b 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -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; @@ -100,6 +103,7 @@ export function loadConfig(flags: GlobalFlags): Config { return { apiKey, + apiKeySource, fileApiKey, fileRegion: file.region, configPath: getConfigPath(), diff --git a/src/config/schema.ts b/src/config/schema.ts index 5eaaea4..4486b2b 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -83,6 +83,7 @@ export function parseConfigFile(raw: unknown): ConfigFile { export interface Config { apiKey?: string; + apiKeySource?: 'flag' | 'env'; fileApiKey?: string; fileRegion?: Region; configPath?: string; diff --git a/src/output/status-bar.ts b/src/output/status-bar.ts index f02f8f2..79fd70c 100644 --- a/src/output/status-bar.ts +++ b/src/output/status-bar.ts @@ -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}` : ''; diff --git a/test/auth/resolver.test.ts b/test/auth/resolver.test.ts index 25b8cb0..7b06d2b 100644 --- a/test/auth/resolver.test.ts +++ b/test/auth/resolver.test.ts @@ -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); diff --git a/test/config/loader.test.ts b/test/config/loader.test.ts index 1fcd815..3be6946 100644 --- a/test/config/loader.test.ts +++ b/test/config/loader.test.ts @@ -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 }); }); @@ -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', () => {