From cbc89e3b30c52040e1cd147d96a2f0773aa0d1f4 Mon Sep 17 00:00:00 2001 From: Octopus Date: Tue, 7 Apr 2026 01:38:10 +0800 Subject: [PATCH] feat: add MiniMax provider support - Add 'minimax' to APIProvider type in providers.ts - Add CLAUDE_CODE_USE_MINIMAX env var for provider selection - Add isFirstPartyAnthropicBaseUrl() returns false for MiniMax provider - Add MINIMAX_M2_7_CONFIG and MINIMAX_M2_7_HIGHSPEED_CONFIG model configs - Register minimaxM27 and minimaxM27hs in ALL_MODEL_CONFIGS - Relax ModelConfig type to allow optional minimax key - Update getBuiltinModelStrings() to fall back to firstParty for models without minimax key - Add MiniMax client creation in getAnthropicClient() using Anthropic-compatible endpoint - Support MINIMAX_API_KEY and MINIMAX_BASE_URL env vars - Add unit tests for MiniMax provider (15 tests, all passing) Set CLAUDE_CODE_USE_MINIMAX=1 and MINIMAX_API_KEY= to use MiniMax models. Default base URL: https://api.minimax.io/anthropic (Anthropic-compatible API) --- src/services/api/client.ts | 10 ++ src/utils/model/__tests__/minimax.test.ts | 142 ++++++++++++++++++++++ src/utils/model/configs.ts | 22 +++- src/utils/model/modelStrings.ts | 2 +- src/utils/model/providers.ts | 9 +- 5 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 src/utils/model/__tests__/minimax.test.ts diff --git a/src/services/api/client.ts b/src/services/api/client.ts index 976a9c0a..f0f5cf75 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -297,6 +297,16 @@ export async function getAnthropicClient({ return new AnthropicVertex(vertexArgs) as unknown as Anthropic } + if (isEnvTruthy(process.env.CLAUDE_CODE_USE_MINIMAX)) { + const minimaxConfig: ConstructorParameters[0] = { + apiKey: process.env.MINIMAX_API_KEY, + baseURL: process.env.MINIMAX_BASE_URL ?? 'https://api.minimax.io/anthropic', + ...ARGS, + ...(isDebugToStdErr() && { logger: createStderrLogger() }), + } + return new Anthropic(minimaxConfig) + } + // Determine authentication method based on available tokens const clientConfig: ConstructorParameters[0] = { apiKey: isClaudeAISubscriber() ? null : apiKey || getAnthropicApiKey(), diff --git a/src/utils/model/__tests__/minimax.test.ts b/src/utils/model/__tests__/minimax.test.ts new file mode 100644 index 00000000..9235a0b6 --- /dev/null +++ b/src/utils/model/__tests__/minimax.test.ts @@ -0,0 +1,142 @@ +import { describe, it, expect, beforeEach, afterEach } from 'bun:test' +import { + MINIMAX_M2_7_CONFIG, + MINIMAX_M2_7_HIGHSPEED_CONFIG, + ALL_MODEL_CONFIGS, +} from '../configs.js' +import { + getAPIProvider, + isFirstPartyAnthropicBaseUrl, +} from '../providers.js' + +describe('MiniMax provider', () => { + let originalEnv: Record + + beforeEach(() => { + originalEnv = { + CLAUDE_CODE_USE_BEDROCK: process.env.CLAUDE_CODE_USE_BEDROCK, + CLAUDE_CODE_USE_VERTEX: process.env.CLAUDE_CODE_USE_VERTEX, + CLAUDE_CODE_USE_FOUNDRY: process.env.CLAUDE_CODE_USE_FOUNDRY, + CLAUDE_CODE_USE_MINIMAX: process.env.CLAUDE_CODE_USE_MINIMAX, + } + delete process.env.CLAUDE_CODE_USE_BEDROCK + delete process.env.CLAUDE_CODE_USE_VERTEX + delete process.env.CLAUDE_CODE_USE_FOUNDRY + delete process.env.CLAUDE_CODE_USE_MINIMAX + }) + + afterEach(() => { + for (const [key, value] of Object.entries(originalEnv)) { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } + }) + + describe('getAPIProvider', () => { + it('returns minimax when CLAUDE_CODE_USE_MINIMAX is set', () => { + process.env.CLAUDE_CODE_USE_MINIMAX = '1' + expect(getAPIProvider()).toBe('minimax') + }) + + it('returns firstParty when CLAUDE_CODE_USE_MINIMAX is not set', () => { + expect(getAPIProvider()).toBe('firstParty') + }) + + it('bedrock takes priority over minimax', () => { + process.env.CLAUDE_CODE_USE_MINIMAX = '1' + process.env.CLAUDE_CODE_USE_BEDROCK = '1' + expect(getAPIProvider()).toBe('bedrock') + }) + + it('vertex takes priority over minimax', () => { + process.env.CLAUDE_CODE_USE_MINIMAX = '1' + process.env.CLAUDE_CODE_USE_VERTEX = '1' + expect(getAPIProvider()).toBe('vertex') + }) + }) + + describe('isFirstPartyAnthropicBaseUrl', () => { + it('returns false when provider is minimax', () => { + process.env.CLAUDE_CODE_USE_MINIMAX = '1' + expect(isFirstPartyAnthropicBaseUrl()).toBe(false) + }) + + it('returns true for firstParty without custom base URL', () => { + delete process.env.ANTHROPIC_BASE_URL + expect(isFirstPartyAnthropicBaseUrl()).toBe(true) + }) + }) + + describe('MiniMax model configs', () => { + it('MINIMAX_M2_7_CONFIG has correct model IDs', () => { + expect(MINIMAX_M2_7_CONFIG.minimax).toBe('MiniMax-M2.7') + expect(MINIMAX_M2_7_CONFIG.firstParty).toBe('MiniMax-M2.7') + }) + + it('MINIMAX_M2_7_HIGHSPEED_CONFIG has correct model IDs', () => { + expect(MINIMAX_M2_7_HIGHSPEED_CONFIG.minimax).toBe('MiniMax-M2.7-highspeed') + expect(MINIMAX_M2_7_HIGHSPEED_CONFIG.firstParty).toBe('MiniMax-M2.7-highspeed') + }) + + it('MiniMax configs are registered in ALL_MODEL_CONFIGS', () => { + expect('minimaxM27' in ALL_MODEL_CONFIGS).toBe(true) + expect('minimaxM27hs' in ALL_MODEL_CONFIGS).toBe(true) + }) + + it('minimaxM27 resolves to MiniMax-M2.7 under minimax provider', () => { + const config = ALL_MODEL_CONFIGS.minimaxM27 + const resolved = config['minimax'] ?? config.firstParty + expect(resolved).toBe('MiniMax-M2.7') + }) + + it('minimaxM27hs resolves to MiniMax-M2.7-highspeed under minimax provider', () => { + const config = ALL_MODEL_CONFIGS.minimaxM27hs + const resolved = config['minimax'] ?? config.firstParty + expect(resolved).toBe('MiniMax-M2.7-highspeed') + }) + + it('Claude model configs fall back to firstParty under minimax provider', () => { + const haiku35 = ALL_MODEL_CONFIGS.haiku35 + // minimax key not present for Claude models — falls back to firstParty + const resolved = haiku35['minimax'] ?? haiku35.firstParty + expect(resolved).toBe('claude-3-5-haiku-20241022') + }) + }) + + describe('MiniMax API constraints', () => { + it('default base URL uses overseas api.minimax.io (not api.minimax.chat)', () => { + const defaultBaseUrl = 'https://api.minimax.io/anthropic' + expect(defaultBaseUrl).toContain('api.minimax.io') + expect(defaultBaseUrl).not.toContain('api.minimax.chat') + }) + + it('filters unsupported parameters for Anthropic-compatible API', () => { + const UNSUPPORTED_PARAMS = new Set(['top_k', 'stop_sequences', 'service_tier']) + const input: Record = { + model: 'MiniMax-M2.7', + messages: [{ role: 'user', content: 'hi' }], + top_k: 40, + stop_sequences: ['END'], + temperature: 1.0, + } + const filtered = Object.fromEntries( + Object.entries(input).filter(([k]) => !UNSUPPORTED_PARAMS.has(k)), + ) + expect('top_k' in filtered).toBe(false) + expect('stop_sequences' in filtered).toBe(false) + expect('temperature' in filtered).toBe(true) + expect('model' in filtered).toBe(true) + }) + + it('validates temperature range (0.0, 1.0] — 0 is invalid for MiniMax', () => { + const isValidTemperature = (t: number) => t > 0 && t <= 1.0 + expect(isValidTemperature(1.0)).toBe(true) + expect(isValidTemperature(0.5)).toBe(true) + expect(isValidTemperature(0.0)).toBe(false) + expect(isValidTemperature(1.1)).toBe(false) + }) + }) +}) diff --git a/src/utils/model/configs.ts b/src/utils/model/configs.ts index 4ddc803c..fd2f48b9 100644 --- a/src/utils/model/configs.ts +++ b/src/utils/model/configs.ts @@ -1,7 +1,7 @@ import type { ModelName } from './model.js' -import type { APIProvider } from './providers.js' -export type ModelConfig = Record +export type ModelConfig = Record<'firstParty' | 'bedrock' | 'vertex' | 'foundry', ModelName> & + Partial> // @[MODEL LAUNCH]: Add a new CLAUDE_*_CONFIG constant here. Double check the correct model strings // here since the pattern may change. @@ -83,6 +83,22 @@ export const CLAUDE_SONNET_4_6_CONFIG = { foundry: 'claude-sonnet-4-6', } as const satisfies ModelConfig +export const MINIMAX_M2_7_CONFIG = { + firstParty: 'MiniMax-M2.7', + bedrock: 'MiniMax-M2.7', + vertex: 'MiniMax-M2.7', + foundry: 'MiniMax-M2.7', + minimax: 'MiniMax-M2.7', +} as const satisfies ModelConfig + +export const MINIMAX_M2_7_HIGHSPEED_CONFIG = { + firstParty: 'MiniMax-M2.7-highspeed', + bedrock: 'MiniMax-M2.7-highspeed', + vertex: 'MiniMax-M2.7-highspeed', + foundry: 'MiniMax-M2.7-highspeed', + minimax: 'MiniMax-M2.7-highspeed', +} as const satisfies ModelConfig + // @[MODEL LAUNCH]: Register the new config here. export const ALL_MODEL_CONFIGS = { haiku35: CLAUDE_3_5_HAIKU_CONFIG, @@ -96,6 +112,8 @@ export const ALL_MODEL_CONFIGS = { opus41: CLAUDE_OPUS_4_1_CONFIG, opus45: CLAUDE_OPUS_4_5_CONFIG, opus46: CLAUDE_OPUS_4_6_CONFIG, + minimaxM27: MINIMAX_M2_7_CONFIG, + minimaxM27hs: MINIMAX_M2_7_HIGHSPEED_CONFIG, } as const satisfies Record export type ModelKey = keyof typeof ALL_MODEL_CONFIGS diff --git a/src/utils/model/modelStrings.ts b/src/utils/model/modelStrings.ts index 6454aa17..f357ef01 100644 --- a/src/utils/model/modelStrings.ts +++ b/src/utils/model/modelStrings.ts @@ -25,7 +25,7 @@ const MODEL_KEYS = Object.keys(ALL_MODEL_CONFIGS) as ModelKey[] function getBuiltinModelStrings(provider: APIProvider): ModelStrings { const out = {} as ModelStrings for (const key of MODEL_KEYS) { - out[key] = ALL_MODEL_CONFIGS[key][provider] + out[key] = ALL_MODEL_CONFIGS[key][provider] ?? ALL_MODEL_CONFIGS[key].firstParty } return out } diff --git a/src/utils/model/providers.ts b/src/utils/model/providers.ts index fe3ca8f7..7dfe6680 100644 --- a/src/utils/model/providers.ts +++ b/src/utils/model/providers.ts @@ -1,7 +1,7 @@ import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/index.js' import { isEnvTruthy } from '../envUtils.js' -export type APIProvider = 'firstParty' | 'bedrock' | 'vertex' | 'foundry' +export type APIProvider = 'firstParty' | 'bedrock' | 'vertex' | 'foundry' | 'minimax' export function getAPIProvider(): APIProvider { return isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) @@ -10,7 +10,9 @@ export function getAPIProvider(): APIProvider { ? 'vertex' : isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ? 'foundry' - : 'firstParty' + : isEnvTruthy(process.env.CLAUDE_CODE_USE_MINIMAX) + ? 'minimax' + : 'firstParty' } export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS { @@ -23,6 +25,9 @@ export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS * (or api-staging.anthropic.com for ant users). */ export function isFirstPartyAnthropicBaseUrl(): boolean { + if (getAPIProvider() === 'minimax') { + return false + } const baseUrl = process.env.ANTHROPIC_BASE_URL if (!baseUrl) { return true