From f166dee3524a04aac43336816d391c8be374c07e Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 13 May 2026 09:14:34 -0700 Subject: [PATCH 01/13] fix: route MiniMax through Anthropic-compatible API Switch MiniMax provider setup away from the OpenAI-compatible shim and onto the Anthropic-compatible endpoint. Update env-only, provider flag, and saved profile paths to use ANTHROPIC_* while preserving legacy OPENAI_MODEL as a migration fallback. Adjust MiniMax M2 context metadata so shared descriptors use the gateway-safe 196608 window and the direct MiniMax catalog overrides to the documented 204800 window. Extend runtime context lookup to anthropic-proxy routes so compact budgeting uses the direct provider metadata. Update MiniMax client, provider profile, provider flag, context, and auto-compact tests for the Anthropic-compatible route and provider-specific compact limits. --- .env.example | 5 +- src/integrations/models/minimax.ts | 14 +- src/integrations/vendors/minimax.ts | 28 ++-- src/services/api/client.test.ts | 153 +++++------------- src/services/api/client.ts | 48 +++--- src/services/compact/autoCompact.test.ts | 36 ++++- src/utils/context.test.ts | 15 +- src/utils/context.ts | 1 + .../model/model.openai-shim-providers.test.ts | 2 + src/utils/model/model.ts | 16 +- src/utils/providerFlag.test.ts | 13 +- src/utils/providerFlag.ts | 19 +++ src/utils/providerProfile.ts | 19 ++- src/utils/providerProfiles.test.ts | 11 +- src/utils/providerProfiles.ts | 33 +++- 15 files changed, 217 insertions(+), 196 deletions(-) diff --git a/.env.example b/.env.example index c8eaafb32..a70d37574 100644 --- a/.env.example +++ b/.env.example @@ -282,10 +282,9 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here # MiniMax API provides text generation models. # Get your API key from https://platform.minimax.io/ # -# CLAUDE_CODE_USE_OPENAI=1 # MINIMAX_API_KEY=your-minimax-key-here -# OPENAI_BASE_URL=https://api.minimax.io/v1 -# OPENAI_MODEL=MiniMax-M2.5 +# ANTHROPIC_BASE_URL=https://api.minimax.io/anthropic +# ANTHROPIC_MODEL=MiniMax-M2.7 # ============================================================================= diff --git a/src/integrations/models/minimax.ts b/src/integrations/models/minimax.ts index 2e53ad206..f143daa39 100644 --- a/src/integrations/models/minimax.ts +++ b/src/integrations/models/minimax.ts @@ -18,7 +18,7 @@ export default [ classification: ['chat', 'reasoning', 'coding'], defaultModel: 'MiniMax-M2', capabilities: minimaxM2Capabilities, - contextWindow: 204_800, + contextWindow: 196_608, maxOutputTokens: 131_072, }), defineModel({ @@ -29,7 +29,7 @@ export default [ classification: ['chat', 'reasoning', 'vision', 'coding'], defaultModel: 'MiniMax-M2.1', capabilities: minimaxM2Capabilities, - contextWindow: 204_800, + contextWindow: 196_608, maxOutputTokens: 131_072, }), defineModel({ @@ -40,7 +40,7 @@ export default [ classification: ['chat', 'reasoning', 'vision', 'coding'], defaultModel: 'MiniMax-M2.1-highspeed', capabilities: minimaxM2Capabilities, - contextWindow: 204_800, + contextWindow: 196_608, maxOutputTokens: 131_072, }), defineModel({ @@ -51,7 +51,7 @@ export default [ classification: ['chat', 'reasoning', 'vision', 'coding'], defaultModel: 'MiniMax-M2.5', capabilities: minimaxM2Capabilities, - contextWindow: 204_800, + contextWindow: 196_608, maxOutputTokens: 131_072, }), defineModel({ @@ -62,7 +62,7 @@ export default [ classification: ['chat', 'reasoning', 'vision', 'coding'], defaultModel: 'MiniMax-M2.5-highspeed', capabilities: minimaxM2Capabilities, - contextWindow: 204_800, + contextWindow: 196_608, maxOutputTokens: 131_072, }), defineModel({ @@ -73,7 +73,7 @@ export default [ classification: ['chat', 'reasoning', 'vision', 'coding'], defaultModel: 'MiniMax-M2.7', capabilities: minimaxM2Capabilities, - contextWindow: 204_800, + contextWindow: 196_608, maxOutputTokens: 131_072, }), defineModel({ @@ -84,7 +84,7 @@ export default [ classification: ['chat', 'reasoning', 'vision', 'coding'], defaultModel: 'MiniMax-M2.7-highspeed', capabilities: minimaxM2Capabilities, - contextWindow: 204_800, + contextWindow: 196_608, maxOutputTokens: 131_072, }), defineModel({ diff --git a/src/integrations/vendors/minimax.ts b/src/integrations/vendors/minimax.ts index e8ffeee9c..922302da6 100644 --- a/src/integrations/vendors/minimax.ts +++ b/src/integrations/vendors/minimax.ts @@ -3,8 +3,8 @@ import { defineVendor } from '../define.js' export default defineVendor({ id: 'minimax', label: 'MiniMax', - classification: 'openai-compatible', - defaultBaseUrl: 'https://api.minimax.io/v1', + classification: 'native', + defaultBaseUrl: 'https://api.minimax.io/anthropic', defaultModel: 'MiniMax-M2.7', requiredEnvVars: ['MINIMAX_API_KEY'], setup: { @@ -13,11 +13,7 @@ export default defineVendor({ credentialEnvVars: ['MINIMAX_API_KEY'], }, transportConfig: { - kind: 'openai-compatible', - openaiShim: { - supportsApiFormatSelection: false, - supportsAuthHeaders: false, - }, + kind: 'anthropic-proxy', }, preset: { id: 'minimax', @@ -30,20 +26,20 @@ export default defineVendor({ matchDefaultBaseUrl: true, matchBaseUrlHosts: ['api.minimax.io', 'api.minimax.chat'], }, - credentialEnvVars: ['MINIMAX_API_KEY', 'OPENAI_API_KEY'], + credentialEnvVars: ['MINIMAX_API_KEY'], missingCredentialMessage: - 'MiniMax auth is required. Set MINIMAX_API_KEY or OPENAI_API_KEY.', + 'MiniMax auth is required. Set MINIMAX_API_KEY.', }, catalog: { source: 'static', models: [ - { id: 'minimax-m2', apiName: 'MiniMax-M2', label: 'MiniMax M2', modelDescriptorId: 'minimax-m2' }, - { id: 'minimax-m2.1', apiName: 'MiniMax-M2.1', label: 'MiniMax M2.1', modelDescriptorId: 'minimax-m2.1' }, - { id: 'minimax-m2.1-highspeed', apiName: 'MiniMax-M2.1-highspeed', label: 'MiniMax M2.1 Highspeed', modelDescriptorId: 'minimax-m2.1-highspeed' }, - { id: 'minimax-m2.5', apiName: 'MiniMax-M2.5', label: 'MiniMax M2.5', modelDescriptorId: 'minimax-m2.5' }, - { id: 'minimax-m2.5-highspeed', apiName: 'MiniMax-M2.5-highspeed', label: 'MiniMax M2.5 Highspeed', modelDescriptorId: 'minimax-m2.5-highspeed' }, - { id: 'minimax-m2.7', apiName: 'MiniMax-M2.7', label: 'MiniMax M2.7', modelDescriptorId: 'minimax-m2.7' }, - { id: 'minimax-m2.7-highspeed', apiName: 'MiniMax-M2.7-highspeed', label: 'MiniMax M2.7 Highspeed', modelDescriptorId: 'minimax-m2.7-highspeed' }, + { id: 'minimax-m2', apiName: 'MiniMax-M2', label: 'MiniMax M2', modelDescriptorId: 'minimax-m2', contextWindow: 204_800 }, + { id: 'minimax-m2.1', apiName: 'MiniMax-M2.1', label: 'MiniMax M2.1', modelDescriptorId: 'minimax-m2.1', contextWindow: 204_800 }, + { id: 'minimax-m2.1-highspeed', apiName: 'MiniMax-M2.1-highspeed', label: 'MiniMax M2.1 Highspeed', modelDescriptorId: 'minimax-m2.1-highspeed', contextWindow: 204_800 }, + { id: 'minimax-m2.5', apiName: 'MiniMax-M2.5', label: 'MiniMax M2.5', modelDescriptorId: 'minimax-m2.5', contextWindow: 204_800 }, + { id: 'minimax-m2.5-highspeed', apiName: 'MiniMax-M2.5-highspeed', label: 'MiniMax M2.5 Highspeed', modelDescriptorId: 'minimax-m2.5-highspeed', contextWindow: 204_800 }, + { id: 'minimax-m2.7', apiName: 'MiniMax-M2.7', label: 'MiniMax M2.7', modelDescriptorId: 'minimax-m2.7', contextWindow: 204_800 }, + { id: 'minimax-m2.7-highspeed', apiName: 'MiniMax-M2.7-highspeed', label: 'MiniMax M2.7 Highspeed', modelDescriptorId: 'minimax-m2.7-highspeed', contextWindow: 204_800 }, { id: 'minimax-text-01', apiName: 'MiniMax-Text-01', label: 'MiniMax Text 01', modelDescriptorId: 'minimax-text-01' }, { id: 'minimax-text-01-preview', apiName: 'MiniMax-Text-01-Preview', label: 'MiniMax Text 01 Preview', modelDescriptorId: 'minimax-text-01-preview' }, { id: 'minimax-vision-01', apiName: 'MiniMax-Vision-01', label: 'MiniMax Vision 01', modelDescriptorId: 'minimax-vision-01' }, diff --git a/src/services/api/client.test.ts b/src/services/api/client.test.ts index 68394b673..b84608717 100644 --- a/src/services/api/client.test.ts +++ b/src/services/api/client.test.ts @@ -38,6 +38,7 @@ const originalEnv = { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL, + ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL, ANTHROPIC_CUSTOM_HEADERS: process.env.ANTHROPIC_CUSTOM_HEADERS, } @@ -75,6 +76,7 @@ beforeEach(() => { delete process.env.NVIDIA_NIM delete process.env.ANTHROPIC_API_KEY delete process.env.ANTHROPIC_AUTH_TOKEN + delete process.env.ANTHROPIC_MODEL delete process.env.ANTHROPIC_CUSTOM_HEADERS }) @@ -104,6 +106,7 @@ afterEach(() => { restoreEnv('ANTHROPIC_API_KEY', originalEnv.ANTHROPIC_API_KEY) restoreEnv('ANTHROPIC_AUTH_TOKEN', originalEnv.ANTHROPIC_AUTH_TOKEN) restoreEnv('ANTHROPIC_BASE_URL', originalEnv.ANTHROPIC_BASE_URL) + restoreEnv('ANTHROPIC_MODEL', originalEnv.ANTHROPIC_MODEL) restoreEnv('ANTHROPIC_CUSTOM_HEADERS', originalEnv.ANTHROPIC_CUSTOM_HEADERS) globalThis.fetch = originalFetch }) @@ -240,7 +243,7 @@ test('routes Gemini provider requests through the OpenAI-compatible shim', async }) }) -test('routes env-only MiniMax requests through the OpenAI-compatible shim', async () => { +test('routes env-only MiniMax requests through the Anthropic-compatible API', async () => { let capturedUrl: string | undefined let capturedHeaders: Headers | undefined let capturedBody: Record | undefined @@ -264,22 +267,18 @@ test('routes env-only MiniMax requests through the OpenAI-compatible shim', asyn return new Response( JSON.stringify({ - id: 'chatcmpl-minimax', + id: 'msg-minimax', + type: 'message', + role: 'assistant', model: 'MiniMax-M2.5', - choices: [ - { - message: { - role: 'assistant', - content: 'minimax ok', - }, - finish_reason: 'stop', - }, - ], + content: [{ type: 'text', text: 'minimax ok' }], usage: { - prompt_tokens: 8, - completion_tokens: 3, - total_tokens: 11, + input_tokens: 8, + output_tokens: 3, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, }, + stop_reason: 'end_turn', }), { headers: { @@ -302,16 +301,19 @@ test('routes env-only MiniMax requests through the OpenAI-compatible shim', asyn stream: false, }) - expect(capturedUrl).toBe('https://api.minimax.io/v1/chat/completions') - expect(capturedHeaders?.get('authorization')).toBe('Bearer minimax-test-key') + expect(capturedUrl).toBe('https://api.minimax.io/anthropic/v1/messages?beta=true') + expect(capturedHeaders?.get('x-api-key')).toBe('minimax-test-key') expect(capturedBody?.model).toBe('MiniMax-M2.5') + expect(process.env.ANTHROPIC_BASE_URL).toBe('https://api.minimax.io/anthropic') + expect(process.env.ANTHROPIC_API_KEY).toBe('minimax-test-key') + expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined() expect(response).toMatchObject({ role: 'assistant', model: 'MiniMax-M2.5', }) }) -test('env-only MiniMax fallback preserves OpenAI-shaped model and base overrides', async () => { +test('env-only MiniMax fallback preserves legacy OPENAI_MODEL as Anthropic model', async () => { let capturedUrl: string | undefined let capturedBody: Record | undefined @@ -321,7 +323,6 @@ test('env-only MiniMax fallback preserves OpenAI-shaped model and base overrides delete process.env.GEMINI_BASE_URL delete process.env.GEMINI_AUTH_MODE process.env.MINIMAX_API_KEY = 'minimax-test-key' - process.env.OPENAI_BASE_URL = 'https://api.minimax.chat/v1' process.env.OPENAI_MODEL = 'MiniMax-M2.7-highspeed' globalThis.fetch = (async (input, init) => { @@ -335,15 +336,13 @@ test('env-only MiniMax fallback preserves OpenAI-shaped model and base overrides return new Response( JSON.stringify({ - id: 'chatcmpl-minimax-override', + id: 'msg-minimax-override', + type: 'message', + role: 'assistant', model: 'MiniMax-M2.7-highspeed', - choices: [ - { - message: { role: 'assistant', content: 'minimax override ok' }, - finish_reason: 'stop', - }, - ], - usage: { prompt_tokens: 8, completion_tokens: 3, total_tokens: 11 }, + content: [{ type: 'text', text: 'minimax override ok' }], + usage: { input_tokens: 8, output_tokens: 3, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }, + stop_reason: 'end_turn', }), { headers: { 'Content-Type': 'application/json' } }, ) @@ -362,84 +361,12 @@ test('env-only MiniMax fallback preserves OpenAI-shaped model and base overrides stream: false, }) - expect(capturedUrl).toBe('https://api.minimax.chat/v1/chat/completions') + expect(capturedUrl).toBe('https://api.minimax.io/anthropic/v1/messages?beta=true') expect(capturedBody?.model).toBe('MiniMax-M2.7-highspeed') - expect(process.env.OPENAI_API_KEY).toBe('minimax-test-key') + expect(process.env.ANTHROPIC_MODEL).toBe('MiniMax-M2.7-highspeed') }) -test('env-only MiniMax fallback ignores stale OPENAI_API_BASE when primary base matches', async () => { - delete process.env.CLAUDE_CODE_USE_GEMINI - delete process.env.GEMINI_API_KEY - delete process.env.GEMINI_MODEL - delete process.env.GEMINI_BASE_URL - delete process.env.GEMINI_AUTH_MODE - process.env.MINIMAX_API_KEY = 'minimax-test-key' - process.env.OPENAI_BASE_URL = 'https://api.minimax.chat/v1' - process.env.OPENAI_API_BASE = 'https://api.openai.com/v1' - - await getAnthropicClient({ - maxRetries: 0, - model: 'MiniMax-M2.7', - }) - - expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1') - expect(process.env.OPENAI_BASE_URL).toBe('https://api.minimax.chat/v1') - expect(process.env.OPENAI_API_KEY).toBe('minimax-test-key') -}) - -test('env-only MiniMax fallback preserves OPENAI_API_BASE host overrides', async () => { - let capturedUrl: string | undefined - - delete process.env.CLAUDE_CODE_USE_GEMINI - delete process.env.GEMINI_API_KEY - delete process.env.GEMINI_MODEL - delete process.env.GEMINI_BASE_URL - delete process.env.GEMINI_AUTH_MODE - process.env.MINIMAX_API_KEY = 'minimax-test-key' - process.env.OPENAI_API_BASE = 'https://api.minimax.chat/v1' - - globalThis.fetch = (async (input) => { - capturedUrl = - typeof input === 'string' - ? input - : input instanceof URL - ? input.toString() - : input.url - - return new Response( - JSON.stringify({ - id: 'chatcmpl-minimax-api-base', - model: 'MiniMax-M2.7', - choices: [ - { - message: { role: 'assistant', content: 'minimax api base ok' }, - finish_reason: 'stop', - }, - ], - usage: { prompt_tokens: 8, completion_tokens: 3, total_tokens: 11 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as FetchType - - const client = (await getAnthropicClient({ - maxRetries: 0, - model: 'MiniMax-M2.7', - })) as unknown as ShimClient - - await client.beta.messages.create({ - model: 'MiniMax-M2.7', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - - expect(capturedUrl).toBe('https://api.minimax.chat/v1/chat/completions') - expect(process.env.OPENAI_BASE_URL).toBe('https://api.minimax.chat/v1') -}) - -test('env-only MiniMax fallback drops unsupported OpenAI shim options', async () => { +test('env-only MiniMax fallback drops stale OpenAI shim options', async () => { let capturedUrl: string | undefined let capturedHeaders: Headers | undefined @@ -465,15 +392,13 @@ test('env-only MiniMax fallback drops unsupported OpenAI shim options', async () return new Response( JSON.stringify({ - id: 'chatcmpl-minimax-clean', + id: 'msg-minimax-clean', + type: 'message', + role: 'assistant', model: 'MiniMax-M2.7', - choices: [ - { - message: { role: 'assistant', content: 'minimax clean ok' }, - finish_reason: 'stop', - }, - ], - usage: { prompt_tokens: 8, completion_tokens: 3, total_tokens: 11 }, + content: [{ type: 'text', text: 'minimax clean ok' }], + usage: { input_tokens: 8, output_tokens: 3, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }, + stop_reason: 'end_turn', }), { headers: { 'Content-Type': 'application/json' } }, ) @@ -492,8 +417,8 @@ test('env-only MiniMax fallback drops unsupported OpenAI shim options', async () stream: false, }) - expect(capturedUrl).toBe('https://api.minimax.io/v1/chat/completions') - expect(capturedHeaders?.get('authorization')).toBe('Bearer minimax-test-key') + expect(capturedUrl).toBe('https://api.minimax.io/anthropic/v1/messages?beta=true') + expect(capturedHeaders?.get('x-api-key')).toBe('minimax-test-key') expect(capturedHeaders?.get('api-key')).toBeNull() expect(process.env.OPENAI_API_FORMAT).toBeUndefined() expect(process.env.OPENAI_AUTH_HEADER).toBeUndefined() @@ -516,9 +441,9 @@ test('env-only MiniMax fallback replaces stale non-MiniMax model env', async () model: 'MiniMax-M2.7', }) - expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1') - expect(process.env.OPENAI_MODEL).toBe('MiniMax-M2.7') - expect(process.env.OPENAI_API_KEY).toBe('minimax-test-key') + expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined() + expect(process.env.ANTHROPIC_MODEL).toBe('MiniMax-M2.7') + expect(process.env.ANTHROPIC_API_KEY).toBe('minimax-test-key') }) test('env-only MiniMax fallback does not override explicit OpenAI credentials', async () => { diff --git a/src/services/api/client.ts b/src/services/api/client.ts index 5bf9b506d..836cb2448 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -35,7 +35,6 @@ import { isEnvTruthy, } from '../../utils/envUtils.js' import { - getMiniMaxBaseUrlOverride, getRouteDefaultBaseUrl, getRouteDefaultModel, getXaiBaseUrlOverride, @@ -124,19 +123,16 @@ function isXaiModelName(value: string | undefined): boolean { } function applyMiniMaxEnvOnlyDefaults(): void { - const baseUrlOverride = getMiniMaxBaseUrlOverride() - const hasMiniMaxBaseOverride = baseUrlOverride !== undefined const modelOverride = process.env.OPENAI_MODEL?.trim() || undefined - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = - baseUrlOverride ?? getRouteDefaultBaseUrl('minimax') - process.env.OPENAI_MODEL = - (hasMiniMaxBaseOverride || isMiniMaxModelName(modelOverride) + process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic' + process.env.ANTHROPIC_API_KEY = process.env.MINIMAX_API_KEY + process.env.ANTHROPIC_MODEL = + (isMiniMaxModelName(modelOverride) ? modelOverride : undefined) ?? getRouteDefaultModel('minimax') - process.env.OPENAI_API_KEY = process.env.MINIMAX_API_KEY + delete process.env.CLAUDE_CODE_USE_OPENAI delete process.env.OPENAI_API_FORMAT delete process.env.OPENAI_AUTH_HEADER delete process.env.OPENAI_AUTH_SCHEME @@ -216,8 +212,21 @@ export async function getAnthropicClient({ defaultHeaders['x-anthropic-additional-protection'] = 'true' } + const envOnlyProviderRouteId = resolveEnvOnlyProviderRouteId(process.env) + const useXaiEnvOnlyProvider = envOnlyProviderRouteId === 'xai' + const useMiniMaxEnvOnlyProvider = envOnlyProviderRouteId === 'minimax' + if (useMiniMaxEnvOnlyProvider) { + applyMiniMaxEnvOnlyDefaults() + } + if (useXaiEnvOnlyProvider) { + applyXaiEnvOnlyDefaults() + } + const shouldUseFirstPartyAuth = shouldUseFirstPartyAnthropicAuth(providerOverride) + const useMiniMaxNativeProvider = + getAPIProvider() === 'minimax' && + !isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) if (shouldUseFirstPartyAuth) { logForDebugging('[API:auth] OAuth token check starting') @@ -284,18 +293,7 @@ export async function getAnthropicClient({ } return new Anthropic(nativeArgs) } - const envOnlyProviderRouteId = resolveEnvOnlyProviderRouteId(process.env) - const useXaiEnvOnlyProvider = envOnlyProviderRouteId === 'xai' - const useMiniMaxEnvOnlyProvider = envOnlyProviderRouteId === 'minimax' - if (useMiniMaxEnvOnlyProvider) { - applyMiniMaxEnvOnlyDefaults() - } - if (useXaiEnvOnlyProvider) { - applyXaiEnvOnlyDefaults() - } - if ( - useMiniMaxEnvOnlyProvider || useXaiEnvOnlyProvider || isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) || @@ -465,7 +463,11 @@ export async function getAnthropicClient({ // Determine authentication method based on available tokens const clientConfig: ConstructorParameters[0] = { - apiKey: isClaudeAiSubscriber ? null : apiKey || getAnthropicApiKey(), + apiKey: isClaudeAiSubscriber + ? null + : useMiniMaxNativeProvider + ? process.env.MINIMAX_API_KEY || process.env.ANTHROPIC_API_KEY + : apiKey || getAnthropicApiKey(), authToken: isClaudeAiSubscriber ? getClaudeAIOAuthTokens()?.accessToken : undefined, @@ -473,7 +475,9 @@ export async function getAnthropicClient({ ...(process.env.USER_TYPE === 'ant' && isEnvTruthy(process.env.USE_STAGING_OAUTH) ? { baseURL: getOauthConfig().BASE_API_URL } - : {}), + : process.env.ANTHROPIC_BASE_URL + ? { baseURL: process.env.ANTHROPIC_BASE_URL } + : {}), ...ARGS, ...(isDebugToStdErr() && { logger: createStderrLogger() }), } diff --git a/src/services/compact/autoCompact.test.ts b/src/services/compact/autoCompact.test.ts index b0decd3ae..05f1139cd 100644 --- a/src/services/compact/autoCompact.test.ts +++ b/src/services/compact/autoCompact.test.ts @@ -4,6 +4,23 @@ import { getAutoCompactThreshold, } from './autoCompact.ts' +const SAVED_ENV = { + CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, + MINIMAX_API_KEY: process.env.MINIMAX_API_KEY, + OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, + OPENAI_MODEL: process.env.OPENAI_MODEL, +} + +function restoreEnv(): void { + for (const [key, value] of Object.entries(SAVED_ENV)) { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } +} + describe('getEffectiveContextWindowSize', () => { test('returns positive value for known models with large context windows', () => { // claude-sonnet-4 has 200k context @@ -32,7 +49,20 @@ describe('getEffectiveContextWindowSize', () => { // the GrowthBook flag state. expect(effective).toBeGreaterThanOrEqual(21_000) } finally { - delete process.env.CLAUDE_CODE_USE_OPENAI + restoreEnv() + } + }) + + test('uses MiniMax M2 context and output metadata for compact budget', () => { + process.env.MINIMAX_API_KEY = 'minimax-test' + process.env.OPENAI_MODEL = 'MiniMax-M2.7' + + try { + // MiniMax's recommended Anthropic-compatible endpoint supports the full + // M2 window. Compact reserves at most 20k summary output tokens. + expect(getEffectiveContextWindowSize('MiniMax-M2.7')).toBe(184_800) + } finally { + restoreEnv() } }) }) @@ -49,7 +79,7 @@ describe('getAutoCompactThreshold', () => { const threshold = getAutoCompactThreshold('some-unknown-3p-model') expect(threshold).toBeGreaterThan(0) } finally { - delete process.env.CLAUDE_CODE_USE_OPENAI + restoreEnv() } }) -}) \ No newline at end of file +}) diff --git a/src/utils/context.test.ts b/src/utils/context.test.ts index 9fa6aacc3..6b1b55eaa 100644 --- a/src/utils/context.test.ts +++ b/src/utils/context.test.ts @@ -245,12 +245,12 @@ test('gpt-5.4 family keeps large max output overrides within provider limits', ( expect(getMaxOutputTokensForModel('gpt-5.4-nano')).toBe(128_000) }) -test('MiniMax-M2.7 uses explicit provider-specific context and output caps', () => { +test('MiniMax-M2.7 uses the shared gateway-safe context cap by default', () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS delete process.env.OPENAI_MODEL - expect(getContextWindowForModel('MiniMax-M2.7')).toBe(204_800) + expect(getContextWindowForModel('MiniMax-M2.7')).toBe(196_608) expect(getModelMaxOutputTokens('MiniMax-M2.7')).toEqual({ default: 131_072, upperLimit: 131_072, @@ -265,6 +265,7 @@ test('env-only MiniMax key uses provider-specific context and output caps before delete process.env.OPENAI_MODEL expect(getContextWindowForModel('MiniMax-M2.7')).toBe(204_800) + expect(getContextWindowForModel('MiniMax-M2.5')).toBe(204_800) expect(getModelMaxOutputTokens('MiniMax-M2.7')).toEqual({ default: 131_072, upperLimit: 131_072, @@ -410,15 +411,15 @@ test('OpenAI-compatible legacy aliases keep their migrated limits', () => { }) }) -test('MiniMax-M2.5 and M2.1 use explicit provider-specific context and output caps', () => { +test('MiniMax-M2.5 and M2.1 use shared gateway-safe context caps by default', () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS delete process.env.OPENAI_MODEL - expect(getContextWindowForModel('MiniMax-M2.5')).toBe(204_800) - expect(getContextWindowForModel('MiniMax-M2.5-highspeed')).toBe(204_800) - expect(getContextWindowForModel('MiniMax-M2.1')).toBe(204_800) - expect(getContextWindowForModel('MiniMax-M2.1-highspeed')).toBe(204_800) + expect(getContextWindowForModel('MiniMax-M2.5')).toBe(196_608) + expect(getContextWindowForModel('MiniMax-M2.5-highspeed')).toBe(196_608) + expect(getContextWindowForModel('MiniMax-M2.1')).toBe(196_608) + expect(getContextWindowForModel('MiniMax-M2.1-highspeed')).toBe(196_608) expect(getModelMaxOutputTokens('MiniMax-M2.5')).toEqual({ default: 131_072, upperLimit: 131_072, diff --git a/src/utils/context.ts b/src/utils/context.ts index 6c3e1f422..bd6db003f 100644 --- a/src/utils/context.ts +++ b/src/utils/context.ts @@ -71,6 +71,7 @@ function shouldUseIntegrationRuntimeLimits( return ( transportKind === 'openai-compatible' || + transportKind === 'anthropic-proxy' || transportKind === 'local' || transportKind === 'gemini-native' ) diff --git a/src/utils/model/model.openai-shim-providers.test.ts b/src/utils/model/model.openai-shim-providers.test.ts index d85242b85..3017669d7 100644 --- a/src/utils/model/model.openai-shim-providers.test.ts +++ b/src/utils/model/model.openai-shim-providers.test.ts @@ -45,6 +45,7 @@ const SAVED_ENV = { CLAUDE_CODE_USE_FOUNDRY: process.env.CLAUDE_CODE_USE_FOUNDRY, NVIDIA_NIM: process.env.NVIDIA_NIM, MINIMAX_API_KEY: process.env.MINIMAX_API_KEY, + ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL, OPENAI_MODEL: process.env.OPENAI_MODEL, OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, CODEX_API_KEY: process.env.CODEX_API_KEY, @@ -74,6 +75,7 @@ beforeEach(() => { delete process.env.CLAUDE_CODE_USE_FOUNDRY delete process.env.NVIDIA_NIM delete process.env.MINIMAX_API_KEY + delete process.env.ANTHROPIC_MODEL delete process.env.OPENAI_MODEL delete process.env.OPENAI_BASE_URL delete process.env.CODEX_API_KEY diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 630401b58..dac1190e5 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -34,6 +34,10 @@ export type ModelShortName = string export type ModelName = string export type ModelSetting = ModelName | ModelAlias | null +function getMiniMaxModelEnv(): string | undefined { + return process.env.ANTHROPIC_MODEL || process.env.OPENAI_MODEL +} + function normalizeModelSetting(value: unknown): ModelName | ModelAlias | undefined { if (typeof value !== 'string') return undefined const trimmed = value.trim() @@ -70,7 +74,7 @@ export function getSmallFastModel(): ModelName { // MiniMax — OPENAI_MODEL carries the active MiniMax model; fall back to // the fastest tier (M2.5-highspeed) when missing. if (getAPIProvider() === 'minimax') { - return process.env.OPENAI_MODEL || 'MiniMax-M2.5-highspeed' + return getMiniMaxModelEnv() || 'MiniMax-M2.5-highspeed' } // xAI — OPENAI_MODEL carries the active Grok model; fall back to grok-3. if (getAPIProvider() === 'xai') { @@ -125,11 +129,11 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined { provider === 'codex' || provider === 'github' || provider === 'nvidia-nim' || - provider === 'minimax' || provider === 'xai' specifiedModel = (provider === 'gemini' ? process.env.GEMINI_MODEL : undefined) || (provider === 'mistral' ? process.env.MISTRAL_MODEL : undefined) || + (provider === 'minimax' ? getMiniMaxModelEnv() : undefined) || (isOpenAIShimProvider ? process.env.OPENAI_MODEL : undefined) || (provider === 'firstParty' ? process.env.ANTHROPIC_MODEL : undefined) || setting || @@ -199,7 +203,7 @@ export function getDefaultOpusModel(): ModelName { } // MiniMax — flagship tier for "opus"-equivalent. if (getAPIProvider() === 'minimax') { - return process.env.OPENAI_MODEL || 'MiniMax-M2.7' + return getMiniMaxModelEnv() || 'MiniMax-M2.7' } // xAI — flagship Grok model for "opus"-equivalent. if (getAPIProvider() === 'xai') { @@ -245,7 +249,7 @@ export function getDefaultSonnetModel(): ModelName { } // MiniMax — mid tier for "sonnet"-equivalent. if (getAPIProvider() === 'minimax') { - return process.env.OPENAI_MODEL || 'MiniMax-M2.5' + return getMiniMaxModelEnv() || 'MiniMax-M2.5' } // xAI — flagship Grok model for "sonnet"-equivalent. if (getAPIProvider() === 'xai') { @@ -289,7 +293,7 @@ export function getDefaultHaikuModel(): ModelName { } // MiniMax — fastest tier for "haiku"-equivalent. if (getAPIProvider() === 'minimax') { - return process.env.OPENAI_MODEL || 'MiniMax-M2.5-highspeed' + return getMiniMaxModelEnv() || 'MiniMax-M2.5-highspeed' } // xAI — faster Grok model for "haiku"-equivalent. if (getAPIProvider() === 'xai') { @@ -369,7 +373,7 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias { } // MiniMax provider: always use the configured MiniMax model if (getAPIProvider() === 'minimax') { - return process.env.OPENAI_MODEL || 'MiniMax-M2.7' + return getMiniMaxModelEnv() || 'MiniMax-M2.7' } // Ants default to defaultModel from flag config, or Opus 1M if not configured diff --git a/src/utils/providerFlag.test.ts b/src/utils/providerFlag.test.ts index f39a3e964..869b0dae6 100644 --- a/src/utils/providerFlag.test.ts +++ b/src/utils/providerFlag.test.ts @@ -299,12 +299,19 @@ describe('applyProviderFlag - descriptor-backed openai-compatible routes', () => describe('applyProviderFlag - minimax', () => { test('preserves MiniMax default base URL and model semantics', () => { + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-4o' + process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com' + process.env.ANTHROPIC_MODEL = 'claude-sonnet-4-5' + const result = applyProviderFlag('minimax', []) expect(result.error).toBeUndefined() - expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1') - expect(process.env.OPENAI_BASE_URL).toBe('https://api.minimax.io/v1') - expect(process.env.OPENAI_MODEL).toBe('MiniMax-M2.7') + expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined() + expect(process.env.ANTHROPIC_BASE_URL).toBe('https://api.minimax.io/anthropic') + expect(process.env.ANTHROPIC_MODEL).toBe('MiniMax-M2.7') + expect(process.env.OPENAI_BASE_URL).toBeUndefined() + expect(process.env.OPENAI_MODEL).toBeUndefined() }) }) diff --git a/src/utils/providerFlag.ts b/src/utils/providerFlag.ts index f7c1bb549..f57b798f4 100644 --- a/src/utils/providerFlag.ts +++ b/src/utils/providerFlag.ts @@ -278,6 +278,25 @@ export function applyProviderFlag( } break + case 'minimax': + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_API_BASE + delete process.env.OPENAI_MODEL + delete process.env.OPENAI_API_FORMAT + delete process.env.OPENAI_AUTH_HEADER + delete process.env.OPENAI_AUTH_SCHEME + delete process.env.OPENAI_AUTH_HEADER_VALUE + process.env.ANTHROPIC_BASE_URL = defaultBaseUrl ?? 'https://api.minimax.io/anthropic' + process.env.ANTHROPIC_MODEL = defaultModel ?? 'MiniMax-M2.7' + if (model) process.env.ANTHROPIC_MODEL = model + if (process.env.MINIMAX_API_KEY && !process.env.ANTHROPIC_API_KEY) { + process.env.ANTHROPIC_API_KEY = process.env.MINIMAX_API_KEY + } + if (copiedOpenAIKeyProvider === 'minimax') { + delete process.env.OPENAI_API_KEY + } + break + default: process.env.CLAUDE_CODE_USE_OPENAI = '1' if (defaultBaseUrl) { diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index fc6fc785c..95c09fed1 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -440,22 +440,29 @@ export function buildMiniMaxProfileEnv(options: { if (!defaultBaseUrl || !defaultModel) { throw new Error('MiniMax route defaults are missing from integration metadata.') } - const secretSource: SecretValueSource = { OPENAI_API_KEY: key } + const secretSource: SecretValueSource = { + ANTHROPIC_API_KEY: key, + MINIMAX_API_KEY: key, + OPENAI_API_KEY: key, + } return { - OPENAI_BASE_URL: + ANTHROPIC_BASE_URL: sanitizeProviderConfigValue(options.baseUrl, secretSource) || - sanitizeProviderConfigValue(processEnv.OPENAI_BASE_URL, secretSource) || + sanitizeProviderConfigValue(processEnv.ANTHROPIC_BASE_URL, secretSource) || defaultBaseUrl, - OPENAI_MODEL: + ANTHROPIC_MODEL: normalizeProfileModel( sanitizeProviderConfigValue(options.model, secretSource), ) || normalizeProfileModel( - sanitizeProviderConfigValue(processEnv.OPENAI_MODEL, secretSource), + sanitizeProviderConfigValue( + processEnv.ANTHROPIC_MODEL ?? processEnv.OPENAI_MODEL, + secretSource, + ), ) || defaultModel, - OPENAI_API_KEY: key, + ANTHROPIC_API_KEY: key, MINIMAX_API_KEY: key, MINIMAX_BASE_URL: defaultBaseUrl, MINIMAX_MODEL: defaultModel, diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index d9e8c397c..67b27688d 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -452,7 +452,7 @@ describe('applyProviderProfileToProcessEnv', () => { applyProviderProfileToProcessEnv( buildProfile({ provider: 'minimax', - baseUrl: 'https://api.minimax.io/v1', + baseUrl: 'https://api.minimax.io/anthropic', model: 'MiniMax-M2.7', apiKey: 'minimax-live-key', apiFormat: 'responses', @@ -465,10 +465,11 @@ describe('applyProviderProfileToProcessEnv', () => { }), ) - expect(process.env.OPENAI_BASE_URL).toBe('https://api.minimax.io/v1') - expect(process.env.OPENAI_MODEL).toBe('MiniMax-M2.7') - expect(process.env.OPENAI_API_KEY).toBe('minimax-live-key') + expect(process.env.ANTHROPIC_BASE_URL).toBe('https://api.minimax.io/anthropic') + expect(process.env.ANTHROPIC_MODEL).toBe('MiniMax-M2.7') + expect(process.env.ANTHROPIC_API_KEY).toBe('minimax-live-key') expect(process.env.MINIMAX_API_KEY).toBe('minimax-live-key') + expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined() expect(process.env.OPENAI_API_FORMAT).toBeUndefined() expect(process.env.OPENAI_AUTH_HEADER).toBeUndefined() expect(process.env.OPENAI_AUTH_SCHEME).toBeUndefined() @@ -1079,7 +1080,7 @@ describe('getProviderPresetDefaults', () => { expect(defaults.provider).toBe('minimax') expect(defaults.name).toBe('MiniMax') - expect(defaults.baseUrl).toBe('https://api.minimax.io/v1') + expect(defaults.baseUrl).toBe('https://api.minimax.io/anthropic') expect(defaults.model).toBe('MiniMax-M2.7') expect(defaults.requiresApiKey).toBe(true) }) diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index 75b66990a..f07c8b402 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -94,6 +94,9 @@ function resolveProfileCompatibility(provider: string): { if (route.vendorId === 'anthropic') { return { route, compatibilityMode: 'anthropic' } } + if (route.vendorId === 'minimax') { + return { route, compatibilityMode: 'anthropic' } + } if (route.vendorId === 'gemini') { return { route, compatibilityMode: 'gemini' } } @@ -572,10 +575,20 @@ export function applyProviderProfileToProcessEnv(profile: ProviderProfile): void } if (compatibilityMode === 'anthropic') { - profileEnv = { - ANTHROPIC_BASE_URL: profile.baseUrl, - ANTHROPIC_MODEL: primaryModel, - ...(profile.apiKey ? { ANTHROPIC_API_KEY: profile.apiKey } : {}), + if (route.vendorId === 'minimax') { + profileEnv = + buildMiniMaxProfileEnv({ + model: primaryModel, + baseUrl: profile.baseUrl, + apiKey: profile.apiKey, + processEnv: process.env, + }) ?? {} + } else { + profileEnv = { + ANTHROPIC_BASE_URL: profile.baseUrl, + ANTHROPIC_MODEL: primaryModel, + ...(profile.apiKey ? { ANTHROPIC_API_KEY: profile.apiKey } : {}), + } } } else if (compatibilityMode === 'mistral') { profileEnv = { @@ -968,6 +981,18 @@ function buildStartupProfileFromActiveProfile( switch (compatibilityMode) { case 'anthropic': + if (route.vendorId === 'minimax') { + const env = + buildMiniMaxProfileEnv({ + model: getPrimaryModel(activeProfile.model), + baseUrl: activeProfile.baseUrl, + apiKey: activeProfile.apiKey, + processEnv: process.env, + }) ?? null + return env + ? { profile: 'minimax', env: applySupportedProfileCustomHeaders(activeProfile, env) } + : null + } return { profile: 'anthropic', env: applySupportedProfileCustomHeaders(activeProfile, { From ecb5c23d5b572742ad0656d4e638b44ff79aab30 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 13 May 2026 09:37:17 -0700 Subject: [PATCH 02/13] test: cover MiniMax provider manager paths Update ProviderManager test fixtures so MiniMax uses the Anthropic-compatible endpoint instead of the old OpenAI-compatible /v1 endpoint. Add coverage for the /provider add flow to assert MiniMax saves provider=minimax, endpoint https://api.minimax.io/anthropic, and displays the Anthropic-compatible API provider type. Add edit-flow coverage to ensure existing MiniMax profiles remain on the Anthropic-compatible provider path and continue hiding OpenAI-only advanced fields. --- src/components/ProviderManager.test.tsx | 115 +++++++++++++++++++++++- 1 file changed, 112 insertions(+), 3 deletions(-) diff --git a/src/components/ProviderManager.test.tsx b/src/components/ProviderManager.test.tsx index ba49c8c1f..52d663199 100644 --- a/src/components/ProviderManager.test.tsx +++ b/src/components/ProviderManager.test.tsx @@ -227,7 +227,7 @@ function mockProviderProfilesModule(options?: { return { provider: 'minimax', name: 'MiniMax', - baseUrl: 'https://api.minimax.io/v1', + baseUrl: 'https://api.minimax.io/anthropic', model: 'MiniMax-M2.7', apiKey: '', requiresApiKey: true, @@ -790,8 +790,15 @@ test('ProviderManager saves OpenAI preset GPT-5 models with Responses API', asyn } }) -test('ProviderManager skips advanced setup fields when adding MiniMax preset', async () => { - mockProviderManagerDependencies(() => undefined, async () => undefined) +test('ProviderManager saves MiniMax preset with Anthropic-compatible endpoint and type', async () => { + const addProviderProfile = mock((payload: any) => ({ + id: 'minimax_profile', + ...payload, + })) + + mockProviderManagerDependencies(() => undefined, async () => undefined, { + addProviderProfile, + }) const nonce = `${Date.now()}-${Math.random()}` const { ProviderManager } = await import(`./ProviderManager.js?ts=${nonce}`) @@ -816,6 +823,7 @@ test('ProviderManager skips advanced setup fields when adding MiniMax preset', a expect(modelOutput).toContain('MiniMax') expect(modelOutput).toContain('MiniMax-M2.7') + expect(modelOutput).toContain('Provider type: Anthropic-compatible API') expect(modelOutput).not.toContain('Provider name') expect(modelOutput).not.toContain('Base URL') expect(modelOutput).not.toContain('API mode') @@ -831,6 +839,107 @@ test('ProviderManager skips advanced setup fields when adding MiniMax preset', a expect(keyOutput).not.toContain('API mode') expect(keyOutput).not.toContain('Auth header') expect(keyOutput).not.toContain('Custom headers') + + mounted.stdin.write('minimax-test-key') + await Bun.sleep(25) + mounted.stdin.write('\r') + + await waitForCondition(() => addProviderProfile.mock.calls.length > 0) + expect(addProviderProfile).toHaveBeenCalledWith( + expect.objectContaining({ + provider: 'minimax', + baseUrl: 'https://api.minimax.io/anthropic', + model: 'MiniMax-M2.7', + apiFormat: 'chat_completions', + }), + expect.objectContaining({ makeActive: true }), + ) + } finally { + await mounted.dispose() + } +}) + +test('ProviderManager edit flow keeps MiniMax on Anthropic-compatible provider path', async () => { + const minimaxProfile = { + id: 'provider_minimax', + provider: 'minimax', + name: 'MiniMax', + baseUrl: 'https://api.minimax.io/anthropic', + model: 'MiniMax-M2.7', + apiKey: 'minimax-key', + } + const updateProviderProfile = mock((id: string, payload: any) => ({ + ...minimaxProfile, + id, + ...payload, + })) + + mockProviderManagerDependencies( + () => undefined, + async () => undefined, + { + getProviderProfiles: () => [minimaxProfile], + getActiveProviderProfile: () => minimaxProfile, + updateProviderProfile, + }, + ) + + const nonce = `${Date.now()}-${Math.random()}` + const { ProviderManager } = await import(`./ProviderManager.js?ts=${nonce}`) + const mounted = await mountProviderManager(ProviderManager) + + try { + await waitForFrameOutput(mounted.getOutput, frame => + frame.includes('Provider manager') && + frame.includes('Edit provider'), + ) + + mounted.stdin.write('j') + await Bun.sleep(25) + mounted.stdin.write('j') + await Bun.sleep(25) + mounted.stdin.write('\r') + + await waitForFrameOutput(mounted.getOutput, frame => + frame.includes('Edit provider') && + frame.includes('MiniMax') && + !frame.includes('Provider manager'), + ) + + mounted.stdin.write('\r') + const editOutput = await waitForFrameOutput(mounted.getOutput, frame => + frame.includes('Edit provider profile') && + frame.includes('Provider type: Anthropic-compatible API'), + ) + + expect(editOutput).toContain('Provider type: Anthropic-compatible API') + expect(editOutput).not.toContain('API mode') + expect(editOutput).not.toContain('Auth header') + expect(editOutput).not.toContain('Custom headers') + + for (let step = 2; step <= 4; step++) { + mounted.stdin.write('\r') + await waitForFrameOutput(mounted.getOutput, frame => + frame.includes(`Step ${step} of 4`), + ) + } + mounted.stdin.write('\r') + + await waitForCondition(() => updateProviderProfile.mock.calls.length > 0) + expect(updateProviderProfile).toHaveBeenCalledWith( + 'provider_minimax', + expect.objectContaining({ + provider: 'minimax', + baseUrl: 'https://api.minimax.io/anthropic', + model: 'MiniMax-M2.7', + }), + ) + expect(updateProviderProfile.mock.calls[0]?.[1]).toMatchObject({ + authHeader: undefined, + authScheme: undefined, + authHeaderValue: undefined, + customHeaders: undefined, + }) } finally { await mounted.dispose() } From 5fbf6ac218ebfc23c9fde22325175a0c35441794 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 13 May 2026 09:55:12 -0700 Subject: [PATCH 03/13] test: isolate MiniMax env-only coverage Harden MiniMax client and compact tests against ambient CI provider env such as OPENAI_API_KEY, ANTHROPIC_BASE_URL, and provider-profile markers. The compact budget test now explicitly clears competing provider flags before asserting direct MiniMax metadata, preventing CI-level OpenAI credentials from masking env-only MiniMax route detection. --- src/services/api/client.test.ts | 15 +++++++++++ src/services/compact/autoCompact.test.ts | 32 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/services/api/client.test.ts b/src/services/api/client.test.ts index b84608717..e7bc16670 100644 --- a/src/services/api/client.test.ts +++ b/src/services/api/client.test.ts @@ -40,6 +40,10 @@ const originalEnv = { ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL, ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL, ANTHROPIC_CUSTOM_HEADERS: process.env.ANTHROPIC_CUSTOM_HEADERS, + CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED: + process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED, + CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID: + process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID, } function restoreEnv(key: string, value: string | undefined): void { @@ -76,8 +80,11 @@ beforeEach(() => { delete process.env.NVIDIA_NIM delete process.env.ANTHROPIC_API_KEY delete process.env.ANTHROPIC_AUTH_TOKEN + delete process.env.ANTHROPIC_BASE_URL delete process.env.ANTHROPIC_MODEL delete process.env.ANTHROPIC_CUSTOM_HEADERS + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID }) afterEach(() => { @@ -108,6 +115,14 @@ afterEach(() => { restoreEnv('ANTHROPIC_BASE_URL', originalEnv.ANTHROPIC_BASE_URL) restoreEnv('ANTHROPIC_MODEL', originalEnv.ANTHROPIC_MODEL) restoreEnv('ANTHROPIC_CUSTOM_HEADERS', originalEnv.ANTHROPIC_CUSTOM_HEADERS) + restoreEnv( + 'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED', + originalEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED, + ) + restoreEnv( + 'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID', + originalEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID, + ) globalThis.fetch = originalFetch }) diff --git a/src/services/compact/autoCompact.test.ts b/src/services/compact/autoCompact.test.ts index 05f1139cd..20ebcaf8a 100644 --- a/src/services/compact/autoCompact.test.ts +++ b/src/services/compact/autoCompact.test.ts @@ -6,9 +6,25 @@ import { const SAVED_ENV = { CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, + CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + CLAUDE_CODE_USE_MISTRAL: process.env.CLAUDE_CODE_USE_MISTRAL, + CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB, + 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_PROVIDER_PROFILE_ENV_APPLIED: + process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED, + CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID: + process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID, MINIMAX_API_KEY: process.env.MINIMAX_API_KEY, + XAI_API_KEY: process.env.XAI_API_KEY, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, + OPENAI_API_BASE: process.env.OPENAI_API_BASE, OPENAI_MODEL: process.env.OPENAI_MODEL, + ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, + ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL, + ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL, } function restoreEnv(): void { @@ -54,6 +70,22 @@ describe('getEffectiveContextWindowSize', () => { }) test('uses MiniMax M2 context and output metadata for compact budget', () => { + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_USE_MISTRAL + delete process.env.CLAUDE_CODE_USE_GITHUB + 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_PROVIDER_PROFILE_ENV_APPLIED + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID + delete process.env.XAI_API_KEY + delete process.env.OPENAI_API_KEY + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_API_BASE + delete process.env.ANTHROPIC_API_KEY + delete process.env.ANTHROPIC_BASE_URL + delete process.env.ANTHROPIC_MODEL process.env.MINIMAX_API_KEY = 'minimax-test' process.env.OPENAI_MODEL = 'MiniMax-M2.7' From 3c6dbeae45c42e978fd63e0319a173763148a570 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 13 May 2026 10:00:04 -0700 Subject: [PATCH 04/13] test: reset provider env inside MiniMax client cases Make each env-only MiniMax client test clear competing provider flags, OpenAI/XAI keys, Anthropic env, and saved-profile markers before setting MINIMAX_API_KEY. This keeps the Anthropic-compatible MiniMax route assertions independent of CI-level process env that can otherwise mask env-only provider detection in the full serial suite. --- src/services/api/client.test.ts | 47 ++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/src/services/api/client.test.ts b/src/services/api/client.test.ts index e7bc16670..acb5c504b 100644 --- a/src/services/api/client.test.ts +++ b/src/services/api/client.test.ts @@ -54,6 +54,35 @@ function restoreEnv(key: string, value: string | undefined): void { } } +function clearEnvForMiniMaxOnlyTest(): void { + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.CLAUDE_CODE_USE_BEDROCK + delete process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH + delete process.env.CLAUDE_CODE_USE_VERTEX + delete process.env.CLAUDE_CODE_USE_FOUNDRY + delete process.env.CLAUDE_CODE_USE_GITHUB + delete process.env.CLAUDE_CODE_USE_MISTRAL + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID + delete process.env.GEMINI_API_KEY + delete process.env.GEMINI_MODEL + delete process.env.GEMINI_BASE_URL + delete process.env.GEMINI_AUTH_MODE + delete process.env.GOOGLE_API_KEY + delete process.env.OPENAI_API_KEY + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_API_BASE + delete process.env.OPENAI_MODEL + delete process.env.XAI_API_KEY + delete process.env.NVIDIA_NIM + delete process.env.ANTHROPIC_API_KEY + delete process.env.ANTHROPIC_AUTH_TOKEN + delete process.env.ANTHROPIC_BASE_URL + delete process.env.ANTHROPIC_MODEL + delete process.env.ANTHROPIC_CUSTOM_HEADERS +} + beforeEach(() => { ;(globalThis as Record).MACRO = { VERSION: 'test-version' } process.env.CLAUDE_CODE_USE_GEMINI = '1' @@ -263,11 +292,7 @@ test('routes env-only MiniMax requests through the Anthropic-compatible API', as let capturedHeaders: Headers | undefined let capturedBody: Record | undefined - delete process.env.CLAUDE_CODE_USE_GEMINI - delete process.env.GEMINI_API_KEY - delete process.env.GEMINI_MODEL - delete process.env.GEMINI_BASE_URL - delete process.env.GEMINI_AUTH_MODE + clearEnvForMiniMaxOnlyTest() process.env.MINIMAX_API_KEY = 'minimax-test-key' globalThis.fetch = (async (input, init) => { @@ -332,11 +357,7 @@ test('env-only MiniMax fallback preserves legacy OPENAI_MODEL as Anthropic model let capturedUrl: string | undefined let capturedBody: Record | undefined - delete process.env.CLAUDE_CODE_USE_GEMINI - delete process.env.GEMINI_API_KEY - delete process.env.GEMINI_MODEL - delete process.env.GEMINI_BASE_URL - delete process.env.GEMINI_AUTH_MODE + clearEnvForMiniMaxOnlyTest() process.env.MINIMAX_API_KEY = 'minimax-test-key' process.env.OPENAI_MODEL = 'MiniMax-M2.7-highspeed' @@ -385,11 +406,7 @@ test('env-only MiniMax fallback drops stale OpenAI shim options', async () => { let capturedUrl: string | undefined let capturedHeaders: Headers | undefined - delete process.env.CLAUDE_CODE_USE_GEMINI - delete process.env.GEMINI_API_KEY - delete process.env.GEMINI_MODEL - delete process.env.GEMINI_BASE_URL - delete process.env.GEMINI_AUTH_MODE + clearEnvForMiniMaxOnlyTest() process.env.MINIMAX_API_KEY = 'minimax-test-key' process.env.OPENAI_API_FORMAT = 'responses' process.env.OPENAI_AUTH_HEADER = 'api-key' From caf38560df606b869f4d9dfb14e95b9eb1bf6010 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 13 May 2026 10:20:39 -0700 Subject: [PATCH 05/13] fix: honor explicit MiniMax routing intent Route MiniMax env-only requests by explicit MiniMax model/base intent even when generic OpenAI-compatible environment variables are present, while preserving non-MiniMax base URL conflicts. Use the resolved MiniMax env-only path for Anthropic SDK key selection so stale provider classification or Bun module mocks cannot fall back to an Anthropic test key. Harden MiniMax compact coverage against leaked env overrides and prior autoCompact module mocks, and cover the ambient OpenAI/XAI env regression. --- src/integrations/routeMetadata.test.ts | 24 ++++++++++ src/integrations/routeMetadata.ts | 37 +++++++++++++--- src/services/api/client.test.ts | 3 ++ src/services/api/client.ts | 56 +++++++++++++++++++++--- src/services/compact/autoCompact.test.ts | 42 +++++++++++++----- 5 files changed, 137 insertions(+), 25 deletions(-) diff --git a/src/integrations/routeMetadata.test.ts b/src/integrations/routeMetadata.test.ts index 92ddfafbf..e522becbe 100644 --- a/src/integrations/routeMetadata.test.ts +++ b/src/integrations/routeMetadata.test.ts @@ -102,6 +102,30 @@ test('resolveActiveRouteIdFromEnv prefers xAI when env-only keys compete', () => ).toBe('xai') }) +test('resolveActiveRouteIdFromEnv lets explicit MiniMax model beat ambient OpenAI-compatible env', () => { + expect( + resolveActiveRouteIdFromEnv({ + CLAUDE_CODE_USE_OPENAI: '1', + OPENAI_API_KEY: 'openai-key', + XAI_API_KEY: 'xai-key', + MINIMAX_API_KEY: 'minimax-key', + OPENAI_MODEL: 'MiniMax-M2.7', + }), + ).toBe('minimax') +}) + +test('resolveActiveRouteIdFromEnv does not use MiniMax when OpenAI base conflicts', () => { + expect( + resolveActiveRouteIdFromEnv({ + CLAUDE_CODE_USE_OPENAI: '1', + OPENAI_API_KEY: 'openai-key', + MINIMAX_API_KEY: 'minimax-key', + OPENAI_BASE_URL: 'https://api.openai.com/v1', + OPENAI_MODEL: 'MiniMax-M2.7', + }), + ).toBe('openai') +}) + test('resolveActiveRouteIdFromEnv keeps xAI primary base over stale API base', () => { expect( resolveActiveRouteIdFromEnv({ diff --git a/src/integrations/routeMetadata.ts b/src/integrations/routeMetadata.ts index 1544d539f..f226cd770 100644 --- a/src/integrations/routeMetadata.ts +++ b/src/integrations/routeMetadata.ts @@ -231,6 +231,22 @@ export function getMiniMaxBaseUrlOverride( return undefined } +function isMiniMaxModelName(value: string | undefined): boolean { + const normalized = value?.trim().toLowerCase() + return Boolean( + normalized && + (normalized.startsWith('minimax-') || normalized.startsWith('minimax/')), + ) +} + +function hasMiniMaxRouteIntent(processEnv: NodeJS.ProcessEnv): boolean { + return ( + getMiniMaxBaseUrlOverride(processEnv) !== undefined || + isMiniMaxModelName(processEnv.OPENAI_MODEL) || + isMiniMaxModelName(processEnv.ANTHROPIC_MODEL) + ) +} + export function getXaiBaseUrlOverride( processEnv: NodeJS.ProcessEnv = process.env, ): string | undefined { @@ -288,12 +304,14 @@ export function hasXaiEnvOnlyProviderIntent( export function hasMiniMaxEnvOnlyProviderIntent( processEnv: NodeJS.ProcessEnv = process.env, ): boolean { + const hasExplicitMiniMaxIntent = hasMiniMaxRouteIntent(processEnv) return ( hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) && - !hasNonEmptyEnvValue(processEnv.OPENAI_API_KEY) && - !hasNonEmptyEnvValue(processEnv.XAI_API_KEY) && !hasConflictingOpenAIBaseUrlForRoute(processEnv, isMiniMaxBaseUrl) && - hasNoExplicitNonOpenAICompatibleProvider(processEnv) + (hasExplicitMiniMaxIntent || + (!hasNonEmptyEnvValue(processEnv.OPENAI_API_KEY) && + !hasNonEmptyEnvValue(processEnv.XAI_API_KEY) && + hasNoExplicitNonOpenAICompatibleProvider(processEnv))) ) } @@ -313,6 +331,13 @@ export function hasVeniceEnvOnlyProviderIntent( export function resolveEnvOnlyProviderRouteId( processEnv: NodeJS.ProcessEnv = process.env, ): 'xai' | 'minimax' | 'venice' | null { + if ( + hasMiniMaxRouteIntent(processEnv) && + hasMiniMaxEnvOnlyProviderIntent(processEnv) + ) { + return 'minimax' + } + if (hasXaiEnvOnlyProviderIntent(processEnv)) { return 'xai' } @@ -529,6 +554,9 @@ export function resolveActiveRouteIdFromEnv( return 'vertex' } + const envOnlyRouteId = resolveEnvOnlyProviderRouteId(processEnv) + if (envOnlyRouteId) return envOnlyRouteId + if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)) { const baseUrl = processEnv.OPENAI_BASE_URL ?? processEnv.OPENAI_API_BASE @@ -564,9 +592,6 @@ export function resolveActiveRouteIdFromEnv( return 'custom' } - const envOnlyRouteId = resolveEnvOnlyProviderRouteId(processEnv) - if (envOnlyRouteId) return envOnlyRouteId - return 'anthropic' } diff --git a/src/services/api/client.test.ts b/src/services/api/client.test.ts index acb5c504b..7ba88634a 100644 --- a/src/services/api/client.test.ts +++ b/src/services/api/client.test.ts @@ -293,6 +293,9 @@ test('routes env-only MiniMax requests through the Anthropic-compatible API', as let capturedBody: Record | undefined clearEnvForMiniMaxOnlyTest() + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_API_KEY = 'ambient-openai-key' + process.env.XAI_API_KEY = 'ambient-xai-key' process.env.MINIMAX_API_KEY = 'minimax-test-key' globalThis.fetch = (async (input, init) => { diff --git a/src/services/api/client.ts b/src/services/api/client.ts index 836cb2448..dcfb6ed1a 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -35,6 +35,7 @@ import { isEnvTruthy, } from '../../utils/envUtils.js' import { + getMiniMaxBaseUrlOverride, getRouteDefaultBaseUrl, getRouteDefaultModel, getXaiBaseUrlOverride, @@ -114,6 +115,38 @@ function isMiniMaxModelName(value: string | undefined): boolean { ) } +function hasMiniMaxModelIntent(model: string | undefined): boolean { + return ( + isMiniMaxModelName(model) || + isMiniMaxModelName(process.env.OPENAI_MODEL) || + isMiniMaxModelName(process.env.ANTHROPIC_MODEL) + ) +} + +function hasConflictingOpenAIBaseUrlForMiniMax(): boolean { + const openAIBaseUrl = + process.env.OPENAI_BASE_URL?.trim() || process.env.OPENAI_API_BASE?.trim() + return Boolean(openAIBaseUrl && getMiniMaxBaseUrlOverride() === undefined) +} + +function shouldUseMiniMaxEnvOnlyProvider( + model: string | undefined, + envOnlyProviderRouteId: string | null, +): boolean { + if (!process.env.MINIMAX_API_KEY?.trim()) { + return false + } + + if (envOnlyProviderRouteId === 'minimax') { + return true + } + + return ( + (hasMiniMaxModelIntent(model) || getMiniMaxBaseUrlOverride() !== undefined) && + !hasConflictingOpenAIBaseUrlForMiniMax() + ) +} + function isXaiModelName(value: string | undefined): boolean { const normalized = value?.trim().toLowerCase() return Boolean( @@ -122,8 +155,12 @@ function isXaiModelName(value: string | undefined): boolean { ) } -function applyMiniMaxEnvOnlyDefaults(): void { - const modelOverride = process.env.OPENAI_MODEL?.trim() || undefined +function applyMiniMaxEnvOnlyDefaults(model: string | undefined): void { + const modelOverride = + (isMiniMaxModelName(model) ? model?.trim() : undefined) ?? + process.env.OPENAI_MODEL?.trim() ?? + process.env.ANTHROPIC_MODEL?.trim() ?? + undefined process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic' process.env.ANTHROPIC_API_KEY = process.env.MINIMAX_API_KEY @@ -213,10 +250,14 @@ export async function getAnthropicClient({ } const envOnlyProviderRouteId = resolveEnvOnlyProviderRouteId(process.env) - const useXaiEnvOnlyProvider = envOnlyProviderRouteId === 'xai' - const useMiniMaxEnvOnlyProvider = envOnlyProviderRouteId === 'minimax' + const useMiniMaxEnvOnlyProvider = shouldUseMiniMaxEnvOnlyProvider( + model, + envOnlyProviderRouteId, + ) + const useXaiEnvOnlyProvider = + envOnlyProviderRouteId === 'xai' && !useMiniMaxEnvOnlyProvider if (useMiniMaxEnvOnlyProvider) { - applyMiniMaxEnvOnlyDefaults() + applyMiniMaxEnvOnlyDefaults(model) } if (useXaiEnvOnlyProvider) { applyXaiEnvOnlyDefaults() @@ -225,8 +266,9 @@ export async function getAnthropicClient({ const shouldUseFirstPartyAuth = shouldUseFirstPartyAnthropicAuth(providerOverride) const useMiniMaxNativeProvider = - getAPIProvider() === 'minimax' && - !isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) + useMiniMaxEnvOnlyProvider || + (getAPIProvider() === 'minimax' && + !isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)) if (shouldUseFirstPartyAuth) { logForDebugging('[API:auth] OAuth token check starting') diff --git a/src/services/compact/autoCompact.test.ts b/src/services/compact/autoCompact.test.ts index 20ebcaf8a..1a126b922 100644 --- a/src/services/compact/autoCompact.test.ts +++ b/src/services/compact/autoCompact.test.ts @@ -1,8 +1,10 @@ -import { describe, expect, test } from 'bun:test' -import { - getEffectiveContextWindowSize, - getAutoCompactThreshold, -} from './autoCompact.ts' +import { describe, expect, mock, test } from 'bun:test' + +async function importAutoCompact() { + mock.restore() + const nonce = `${Date.now()}-${Math.random()}` + return import(`./autoCompact.ts?test=${nonce}`) +} const SAVED_ENV = { CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, @@ -25,6 +27,13 @@ const SAVED_ENV = { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL, ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL, + USER_TYPE: process.env.USER_TYPE, + CLAUDE_CODE_MAX_CONTEXT_TOKENS: + process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS, + CLAUDE_CODE_AUTO_COMPACT_WINDOW: + process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW, + CLAUDE_CODE_MAX_OUTPUT_TOKENS: + process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, } function restoreEnv(): void { @@ -38,13 +47,15 @@ function restoreEnv(): void { } describe('getEffectiveContextWindowSize', () => { - test('returns positive value for known models with large context windows', () => { + test('returns positive value for known models with large context windows', async () => { + const { getEffectiveContextWindowSize } = await importAutoCompact() // claude-sonnet-4 has 200k context const effective = getEffectiveContextWindowSize('claude-sonnet-4') expect(effective).toBeGreaterThan(0) }) - test('never returns negative even for unknown 3P models (issue #635)', () => { + test('never returns negative even for unknown 3P models (issue #635)', async () => { + const { getEffectiveContextWindowSize } = await importAutoCompact() // Previously, unknown 3P models got 8k context → effective context was // 8k minus 20k summary reservation = -12k, causing infinite auto-compact. // Now the fallback is 128k and there's a floor, so effective is always @@ -69,8 +80,8 @@ describe('getEffectiveContextWindowSize', () => { } }) - test('uses MiniMax M2 context and output metadata for compact budget', () => { - delete process.env.CLAUDE_CODE_USE_OPENAI + test('uses MiniMax M2 context and output metadata for compact budget', async () => { + const { getEffectiveContextWindowSize } = await importAutoCompact() delete process.env.CLAUDE_CODE_USE_GEMINI delete process.env.CLAUDE_CODE_USE_MISTRAL delete process.env.CLAUDE_CODE_USE_GITHUB @@ -80,12 +91,17 @@ describe('getEffectiveContextWindowSize', () => { delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID delete process.env.XAI_API_KEY - delete process.env.OPENAI_API_KEY delete process.env.OPENAI_BASE_URL delete process.env.OPENAI_API_BASE delete process.env.ANTHROPIC_API_KEY delete process.env.ANTHROPIC_BASE_URL delete process.env.ANTHROPIC_MODEL + delete process.env.USER_TYPE + delete process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS + delete process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW + delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_API_KEY = 'ambient-openai-key' process.env.MINIMAX_API_KEY = 'minimax-test' process.env.OPENAI_MODEL = 'MiniMax-M2.7' @@ -100,12 +116,14 @@ describe('getEffectiveContextWindowSize', () => { }) describe('getAutoCompactThreshold', () => { - test('returns positive threshold for known models', () => { + test('returns positive threshold for known models', async () => { + const { getAutoCompactThreshold } = await importAutoCompact() const threshold = getAutoCompactThreshold('claude-sonnet-4') expect(threshold).toBeGreaterThan(0) }) - test('never returns negative threshold even for unknown 3P models (issue #635)', () => { + test('never returns negative threshold even for unknown 3P models (issue #635)', async () => { + const { getAutoCompactThreshold } = await importAutoCompact() process.env.CLAUDE_CODE_USE_OPENAI = '1' try { const threshold = getAutoCompactThreshold('some-unknown-3p-model') From 2282694b98da05bef46c20326cfb7290373c1b36 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 13 May 2026 10:29:37 -0700 Subject: [PATCH 06/13] test: clean up compression autoCompact mocks Restore Bun module mocks after compression test files so their deterministic autoCompact window does not leak into later compact tests in full-suite order. Verified the MiniMax compact regression now passes after the compression suites and in the full local test log. --- src/services/api/compressToolHistory.test.ts | 6 +++++- src/services/api/openaiShim.compression.test.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/services/api/compressToolHistory.test.ts b/src/services/api/compressToolHistory.test.ts index ed5f136f9..35bada9d5 100644 --- a/src/services/api/compressToolHistory.test.ts +++ b/src/services/api/compressToolHistory.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import { compressToolHistory, getTiers } from './compressToolHistory.js' // Mock the two dependencies so tests are deterministic and don't read disk config. @@ -17,6 +17,10 @@ mock.module('../compact/autoCompact.js', () => ({ getEffectiveContextWindowSize: () => mockState.effectiveWindow, })) +afterAll(() => { + mock.restore() +}) + beforeEach(() => { mockState.enabled = true mockState.effectiveWindow = 100_000 diff --git a/src/services/api/openaiShim.compression.test.ts b/src/services/api/openaiShim.compression.test.ts index b45811ff4..848f01857 100644 --- a/src/services/api/openaiShim.compression.test.ts +++ b/src/services/api/openaiShim.compression.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import { createOpenAIShimClient } from './openaiShim.js' type FetchType = typeof globalThis.fetch @@ -27,6 +27,10 @@ mock.module('../compact/autoCompact.js', () => ({ getEffectiveContextWindowSize: () => mockState.effectiveWindow, })) +afterAll(() => { + mock.restore() +}) + type OpenAIShimClient = { beta: { messages: { From 4031362e1c6f375e6f27cecdfd96660427833a06 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 13 May 2026 10:42:26 -0700 Subject: [PATCH 07/13] test: avoid autoCompact module mocks in compression tests Replace the compression suites' top-level Bun module mocks for autoCompact/config with real test config and env controls. This avoids Bun 1.3.11 leaking a mocked effective context window into the later MiniMax compact test in full-suite order. Verified compression-before-compact and MiniMax focused suites pass locally. --- src/services/api/compressToolHistory.test.ts | 64 ++++++++++++----- .../api/openaiShim.compression.test.ts | 71 +++++++++++++------ 2 files changed, 95 insertions(+), 40 deletions(-) diff --git a/src/services/api/compressToolHistory.test.ts b/src/services/api/compressToolHistory.test.ts index 35bada9d5..61c13433a 100644 --- a/src/services/api/compressToolHistory.test.ts +++ b/src/services/api/compressToolHistory.test.ts @@ -1,34 +1,64 @@ -import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterEach, beforeEach, expect, test } from 'bun:test' +import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' import { compressToolHistory, getTiers } from './compressToolHistory.js' -// Mock the two dependencies so tests are deterministic and don't read disk config. +const originalEnv = { + CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, + CLAUDE_CODE_AUTO_COMPACT_WINDOW: + process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW, + CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, +} + +const originalConfig = { + toolHistoryCompressionEnabled: + getGlobalConfig().toolHistoryCompressionEnabled, +} + const mockState = { enabled: true, effectiveWindow: 100_000, } -mock.module('../../utils/config.js', () => ({ - getGlobalConfig: () => ({ - toolHistoryCompressionEnabled: mockState.enabled, - }), -})) +function restoreEnv(key: keyof typeof originalEnv): void { + const value = originalEnv[key] + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } +} -mock.module('../compact/autoCompact.js', () => ({ - getEffectiveContextWindowSize: () => mockState.effectiveWindow, -})) +function setEffectiveWindowForTest(effectiveWindow: number): void { + mockState.effectiveWindow = effectiveWindow + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '8000' + process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(effectiveWindow + 8_000) +} -afterAll(() => { - mock.restore() -}) +function setCompressionEnabledForTest(enabled: boolean): void { + mockState.enabled = enabled + saveGlobalConfig(current => ({ + ...current, + toolHistoryCompressionEnabled: mockState.enabled, + })) +} beforeEach(() => { - mockState.enabled = true - mockState.effectiveWindow = 100_000 + setCompressionEnabledForTest(true) + setEffectiveWindowForTest(100_000) }) afterEach(() => { mockState.enabled = true mockState.effectiveWindow = 100_000 + for (const key of Object.keys(originalEnv) as Array) { + restoreEnv(key) + } + saveGlobalConfig(current => ({ + ...current, + toolHistoryCompressionEnabled: + originalConfig.toolHistoryCompressionEnabled, + })) }) type Block = Record @@ -128,7 +158,7 @@ test('getTiers: ≥ 500k (gpt-4.1 1M) → recent=25, mid=50', () => { // ---------- master switch ---------- test('pass-through when toolHistoryCompressionEnabled is false', () => { - mockState.enabled = false + setCompressionEnabledForTest(false) const messages = buildConversation(20) const result = compressToolHistory(messages, 'gpt-4o') expect(result).toBe(messages) // same reference (no transformation) @@ -394,7 +424,7 @@ test('tier boundaries: 16 exchanges → 1 old + 10 mid + 5 recent', () => { test('large window (1M) with 30 exchanges: all untouched (recent=25 ≥ 30 - 5)', () => { // ≥500k → recent=25, mid=50. 30 exchanges → 5 mid + 25 recent. None old. - mockState.effectiveWindow = 1_000_000 + setEffectiveWindowForTest(1_000_000) const messages = buildConversation(30, 5_000) const result = compressToolHistory(messages, 'gpt-4.1') const resultMsgs = getResultMessages(result) diff --git a/src/services/api/openaiShim.compression.test.ts b/src/services/api/openaiShim.compression.test.ts index 848f01857..4cf1d1c15 100644 --- a/src/services/api/openaiShim.compression.test.ts +++ b/src/services/api/openaiShim.compression.test.ts @@ -1,35 +1,55 @@ -import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterEach, beforeEach, expect, test } from 'bun:test' +import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' import { createOpenAIShimClient } from './openaiShim.js' type FetchType = typeof globalThis.fetch const originalFetch = globalThis.fetch const originalEnv = { + CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, + CLAUDE_CODE_AUTO_COMPACT_WINDOW: + process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW, + CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, OPENAI_API_KEY: process.env.OPENAI_API_KEY, OPENAI_MODEL: process.env.OPENAI_MODEL, } -// Mock config + autoCompact so the shim sees deterministic state. +const originalConfig = { + toolHistoryCompressionEnabled: + getGlobalConfig().toolHistoryCompressionEnabled, + autoCompactEnabled: getGlobalConfig().autoCompactEnabled, +} + const mockState = { enabled: true, effectiveWindow: 100_000, // Copilot gpt-4o tier } -mock.module('../../utils/config.js', () => ({ - getGlobalConfig: () => ({ - toolHistoryCompressionEnabled: mockState.enabled, - autoCompactEnabled: false, - }), -})) +function restoreEnv(key: keyof typeof originalEnv): void { + const value = originalEnv[key] + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } +} -mock.module('../compact/autoCompact.js', () => ({ - getEffectiveContextWindowSize: () => mockState.effectiveWindow, -})) +function setEffectiveWindowForTest(effectiveWindow: number): void { + mockState.effectiveWindow = effectiveWindow + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '8000' + process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(effectiveWindow + 8_000) +} -afterAll(() => { - mock.restore() -}) +function setCompressionEnabledForTest(enabled: boolean): void { + mockState.enabled = enabled + saveGlobalConfig(current => ({ + ...current, + toolHistoryCompressionEnabled: mockState.enabled, + autoCompactEnabled: false, + })) +} type OpenAIShimClient = { beta: { @@ -100,20 +120,23 @@ function makeFakeResponse(): Response { } beforeEach(() => { + setCompressionEnabledForTest(true) + setEffectiveWindowForTest(100_000) process.env.OPENAI_BASE_URL = 'http://example.test/v1' process.env.OPENAI_API_KEY = 'test-key' delete process.env.OPENAI_MODEL - mockState.enabled = true - mockState.effectiveWindow = 100_000 }) afterEach(() => { - if (originalEnv.OPENAI_BASE_URL === undefined) delete process.env.OPENAI_BASE_URL - else process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL - if (originalEnv.OPENAI_API_KEY === undefined) delete process.env.OPENAI_API_KEY - else process.env.OPENAI_API_KEY = originalEnv.OPENAI_API_KEY - if (originalEnv.OPENAI_MODEL === undefined) delete process.env.OPENAI_MODEL - else process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL + for (const key of Object.keys(originalEnv) as Array) { + restoreEnv(key) + } + saveGlobalConfig(current => ({ + ...current, + toolHistoryCompressionEnabled: + originalConfig.toolHistoryCompressionEnabled, + autoCompactEnabled: originalConfig.autoCompactEnabled, + })) globalThis.fetch = originalFetch }) @@ -121,6 +144,8 @@ async function captureRequestBody( messages: Array<{ role: string; content: unknown }>, model: string, ): Promise> { + setCompressionEnabledForTest(mockState.enabled) + setEffectiveWindowForTest(mockState.effectiveWindow) let captured: Record | undefined globalThis.fetch = (async (_input, init) => { @@ -159,7 +184,7 @@ function getAssistantToolCalls(body: Record): unknown[] { // ============================================================================ test('BUG REPRO: without compression, all 30 tool results are sent at full size', async () => { - mockState.enabled = false + setCompressionEnabledForTest(false) const messages = buildLongConversation(30, 5_000) const body = await captureRequestBody(messages, 'gpt-4o') From 1ac6a9556f478b945e8b7462fccfe57f040ffeef Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 13 May 2026 10:46:33 -0700 Subject: [PATCH 08/13] test: allow capped MiniMax compact reservation CI enables the output-token slot-reservation cap, so MiniMax's direct 204,800 context can produce a 196,800 effective compact window instead of the uncapped 184,800. Keep the test focused on direct MiniMax context metadata while accepting either reservation state. --- src/services/compact/autoCompact.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/compact/autoCompact.test.ts b/src/services/compact/autoCompact.test.ts index 1a126b922..04483cceb 100644 --- a/src/services/compact/autoCompact.test.ts +++ b/src/services/compact/autoCompact.test.ts @@ -107,8 +107,11 @@ describe('getEffectiveContextWindowSize', () => { try { // MiniMax's recommended Anthropic-compatible endpoint supports the full - // M2 window. Compact reserves at most 20k summary output tokens. - expect(getEffectiveContextWindowSize('MiniMax-M2.7')).toBe(184_800) + // M2 window. Compact reserves either the default 20k summary output + // tokens or 8k when the slot-reservation cap flag is enabled. + expect([184_800, 196_800]).toContain( + getEffectiveContextWindowSize('MiniMax-M2.7'), + ) } finally { restoreEnv() } From b5b1ca85325a56ef47ce37ade264a2009cbd0045 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 13 May 2026 15:27:02 -0700 Subject: [PATCH 09/13] fix: address MiniMax review findings Treat env-only provider routes such as direct MiniMax as complete startup provider selections so saved profiles do not override explicit MINIMAX_API_KEY/ANTHROPIC_* env. Stop advertising direct MiniMax benchmark support through the OpenAI-compatible benchmark path, and add regression coverage for the unsupported direct MiniMax benchmark env. --- src/commands/benchmark.ts | 4 +-- src/utils/model/benchmark.test.ts | 43 ++++++++++++++++++++++++++++++ src/utils/model/benchmark.ts | 4 +-- src/utils/providerProfiles.test.ts | 28 +++++++++++++++++++ src/utils/providerProfiles.ts | 2 ++ 5 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 src/utils/model/benchmark.test.ts diff --git a/src/commands/benchmark.ts b/src/commands/benchmark.ts index 84bf690fe..d9bd018bf 100644 --- a/src/commands/benchmark.ts +++ b/src/commands/benchmark.ts @@ -15,7 +15,7 @@ async function runBenchmark( if (!isBenchmarkSupported()) { context?.stdout?.write( 'Benchmark not supported for this provider.\n' + - 'Supported: OpenAI-compatible endpoints (Ollama, NVIDIA NIM, MiniMax)\n', + 'Supported: OpenAI-compatible endpoints (Ollama, NVIDIA NIM)\n', ) return } @@ -53,4 +53,4 @@ export const benchmark: Command = { await runBenchmark(model, context) }, -} \ No newline at end of file +} diff --git a/src/utils/model/benchmark.test.ts b/src/utils/model/benchmark.test.ts new file mode 100644 index 000000000..028bfb4d2 --- /dev/null +++ b/src/utils/model/benchmark.test.ts @@ -0,0 +1,43 @@ +import { afterEach, expect, test } from 'bun:test' + +const ORIGINAL_ENV = { + CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, + OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, + OPENAI_API_BASE: process.env.OPENAI_API_BASE, + OPENAI_MODEL: process.env.OPENAI_MODEL, + MINIMAX_API_KEY: process.env.MINIMAX_API_KEY, + ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL, + ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL, +} + +function restoreEnv(): void { + for (const [key, value] of Object.entries(ORIGINAL_ENV)) { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } +} + +afterEach(() => { + restoreEnv() +}) + +test('benchmark support does not advertise direct MiniMax Anthropic-compatible env', async () => { + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.OPENAI_API_KEY + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_API_BASE + delete process.env.OPENAI_MODEL + process.env.MINIMAX_API_KEY = 'minimax-test-key' + process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic' + process.env.ANTHROPIC_MODEL = 'MiniMax-M2.7' + + const { isBenchmarkSupported } = await import( + `./benchmark.js?ts=${Date.now()}-${Math.random()}` + ) + + expect(isBenchmarkSupported()).toBe(false) +}) diff --git a/src/utils/model/benchmark.ts b/src/utils/model/benchmark.ts index 4c203c1b4..4decdbb02 100644 --- a/src/utils/model/benchmark.ts +++ b/src/utils/model/benchmark.ts @@ -34,8 +34,8 @@ function getBenchmarkEndpoint(): string | null { if (provider === 'openai' || provider === 'firstParty') { return `${baseUrl || 'https://api.openai.com/v1'}/chat/completions` } - // NVIDIA NIM or MiniMax via OPENAI_BASE_URL - if (baseUrl?.includes('nvidia') || baseUrl?.includes('minimax')) { + // NVIDIA NIM via OPENAI_BASE_URL + if (baseUrl?.includes('nvidia')) { return `${baseUrl}/chat/completions` } return null diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index 67b27688d..993949ad9 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -781,6 +781,34 @@ describe('applyActiveProviderProfileFromConfig', () => { expect(process.env.OPENAI_MODEL).toBe('gpt-4o-mini') }) + test('does not override explicit env-only MiniMax selection with saved profile', async () => { + const { applyActiveProviderProfileFromConfig } = + await importFreshProviderProfileModules() + process.env.MINIMAX_API_KEY = 'minimax-live-key' + process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic' + process.env.ANTHROPIC_MODEL = 'MiniMax-M2.7' + + const applied = applyActiveProviderProfileFromConfig({ + providerProfiles: [ + buildProfile({ + id: 'saved_openai', + baseUrl: 'https://api.openai.com/v1', + model: 'gpt-4o', + }), + ], + activeProviderProfileId: 'saved_openai', + } as any) + + expect(applied).toBeUndefined() + expect(process.env.MINIMAX_API_KEY).toBe('minimax-live-key') + expect(process.env.ANTHROPIC_BASE_URL).toBe( + 'https://api.minimax.io/anthropic', + ) + expect(process.env.ANTHROPIC_MODEL).toBe('MiniMax-M2.7') + expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined() + expect(process.env.OPENAI_BASE_URL).toBeUndefined() + }) + test('does not override explicit startup selection when profile marker is stale', async () => { const { applyActiveProviderProfileFromConfig } = await importFreshProviderProfileModules() diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index f07c8b402..24a4ead91 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -38,6 +38,7 @@ import { type ResolvedProfileRoute, type ProviderPreset, } from '../integrations/index.js' +import { resolveEnvOnlyProviderRouteId } from '../integrations/routeMetadata.js' import { logForDebugging } from './debug.js' import { sanitizeProfileCustomHeaders, @@ -344,6 +345,7 @@ function hasProviderSelectionFlags( function hasCompleteProviderSelection( processEnv: NodeJS.ProcessEnv = process.env, ): boolean { + if (resolveEnvOnlyProviderRouteId(processEnv) !== null) return true if (!hasProviderSelectionFlags(processEnv)) return false if (processEnv.CLAUDE_CODE_USE_OPENAI !== undefined) { return ( From eec70329ff6d0b9b2a3a7b5edf780b74261b3f59 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 13 May 2026 17:56:44 -0700 Subject: [PATCH 10/13] fix: classify MiniMax profile startup correctly Recognize MiniMax when /provider loads it through the Anthropic-compatible env shape using ANTHROPIC_BASE_URL, ANTHROPIC_MODEL, and ANTHROPIC_API_KEY. Label MiniMax correctly on the startup screen and skip the Anthropic custom-key approval prompt when the resolved provider is not using the Anthropic account flow. Add regressions for route metadata, legacy provider classification, account-flow bypass, and startup display for Anthropic-compatible MiniMax profiles. --- src/components/StartupScreen.test.ts | 10 ++++++++++ src/components/StartupScreen.ts | 4 +++- src/integrations/routeMetadata.test.ts | 10 ++++++++++ src/integrations/routeMetadata.ts | 12 +++++++++++- src/interactiveHelpers.tsx | 2 +- src/utils/model/providers.test.ts | 21 +++++++++++++++++++++ 6 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/components/StartupScreen.test.ts b/src/components/StartupScreen.test.ts index 3c4aef3b9..e10021943 100644 --- a/src/components/StartupScreen.test.ts +++ b/src/components/StartupScreen.test.ts @@ -43,6 +43,7 @@ const ENV_KEYS = [ 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_BASE_URL', + 'ANTHROPIC_API_KEY', ] const originalEnv: Record = {} @@ -255,6 +256,15 @@ describe('detectProvider — explicit dedicated-provider env flags', () => { process.env.MINIMAX_API_KEY = 'test-key' expect(detectProvider().name).toBe('MiniMax') }) + + test('Anthropic-compatible MiniMax profile is labeled MiniMax', () => { + process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic' + process.env.ANTHROPIC_API_KEY = 'test-key' + process.env.ANTHROPIC_MODEL = 'MiniMax-M2.7' + + expect(detectProvider().name).toBe('MiniMax') + expect(detectProvider().baseUrl).toBe('https://api.minimax.io/anthropic') + }) }) // --- modelOverride from --model flag --- diff --git a/src/components/StartupScreen.ts b/src/components/StartupScreen.ts index ccf8ee437..39051bb17 100644 --- a/src/components/StartupScreen.ts +++ b/src/components/StartupScreen.ts @@ -8,6 +8,7 @@ import { isLocalProviderUrl, resolveProviderRequest } from '../services/api/providerConfig.js' import { getRouteLabel, + isMiniMaxBaseUrl, resolveRouteIdFromBaseUrl, } from '../integrations/routeMetadata.js' import { getLocalOpenAICompatibleProviderLabel } from '../utils/providerDiscovery.js' @@ -161,7 +162,8 @@ export function detectProvider(modelOverride?: string): { name: string; model: s const resolvedModel = parseUserSpecifiedModel(modelSetting) const baseUrl = process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com' const isLocal = isLocalProviderUrl(baseUrl) - return { name: 'Anthropic', model: resolvedModel, baseUrl, isLocal } + const name = isMiniMaxBaseUrl(baseUrl) ? 'MiniMax' : 'Anthropic' + return { name, model: resolvedModel, baseUrl, isLocal } } // ─── Box drawing ────────────────────────────────────────────────────────────── diff --git a/src/integrations/routeMetadata.test.ts b/src/integrations/routeMetadata.test.ts index e522becbe..55966a7a0 100644 --- a/src/integrations/routeMetadata.test.ts +++ b/src/integrations/routeMetadata.test.ts @@ -78,6 +78,16 @@ test('resolveActiveRouteIdFromEnv treats MiniMax credential-only env as MiniMax' ).toBe('minimax') }) +test('resolveActiveRouteIdFromEnv treats Anthropic-compatible MiniMax profile env as MiniMax', () => { + expect( + resolveActiveRouteIdFromEnv({ + ANTHROPIC_BASE_URL: 'https://api.minimax.io/anthropic', + ANTHROPIC_API_KEY: 'minimax-key', + ANTHROPIC_MODEL: 'MiniMax-M2.7', + }), + ).toBe('minimax') +}) + test('resolveActiveRouteIdFromEnv treats Venice credential-only env as Venice', () => { expect( resolveActiveRouteIdFromEnv({ diff --git a/src/integrations/routeMetadata.ts b/src/integrations/routeMetadata.ts index f226cd770..927b5477a 100644 --- a/src/integrations/routeMetadata.ts +++ b/src/integrations/routeMetadata.ts @@ -218,6 +218,11 @@ export function isVeniceBaseUrl(value: string | undefined): boolean { export function getMiniMaxBaseUrlOverride( processEnv: NodeJS.ProcessEnv = process.env, ): string | undefined { + const anthropicBaseUrl = processEnv.ANTHROPIC_BASE_URL?.trim() + if (isMiniMaxBaseUrl(anthropicBaseUrl)) { + return anthropicBaseUrl + } + const openAIBaseUrl = processEnv.OPENAI_BASE_URL?.trim() if (isMiniMaxBaseUrl(openAIBaseUrl)) { return openAIBaseUrl @@ -305,8 +310,13 @@ export function hasMiniMaxEnvOnlyProviderIntent( processEnv: NodeJS.ProcessEnv = process.env, ): boolean { const hasExplicitMiniMaxIntent = hasMiniMaxRouteIntent(processEnv) + const hasMiniMaxCredential = + hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) || + (isMiniMaxBaseUrl(processEnv.ANTHROPIC_BASE_URL) && + hasNonEmptyEnvValue(processEnv.ANTHROPIC_API_KEY)) + return ( - hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) && + hasMiniMaxCredential && !hasConflictingOpenAIBaseUrlForRoute(processEnv, isMiniMaxBaseUrl) && (hasExplicitMiniMaxIntent || (!hasNonEmptyEnvValue(processEnv.OPENAI_API_KEY) && diff --git a/src/interactiveHelpers.tsx b/src/interactiveHelpers.tsx index c655f38ec..68749f3bf 100644 --- a/src/interactiveHelpers.tsx +++ b/src/interactiveHelpers.tsx @@ -213,7 +213,7 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod // Check for custom API key // On homespace, ANTHROPIC_API_KEY is preserved in process.env for child // processes but ignored by Claude Code itself (see auth.ts). - if (process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace()) { + if (usesAnthropicSetup && process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace()) { const customApiKeyTruncated = normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY); const keyStatus = getCustomApiKeyStatus(customApiKeyTruncated); if (keyStatus === 'new') { diff --git a/src/utils/model/providers.test.ts b/src/utils/model/providers.test.ts index bda01ee5c..2a55d96f0 100644 --- a/src/utils/model/providers.test.ts +++ b/src/utils/model/providers.test.ts @@ -9,6 +9,9 @@ const originalEnv = { CLAUDE_CODE_USE_FOUNDRY: process.env.CLAUDE_CODE_USE_FOUNDRY, NVIDIA_NIM: process.env.NVIDIA_NIM, MINIMAX_API_KEY: process.env.MINIMAX_API_KEY, + ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL, + ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, + ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL, OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, OPENAI_API_BASE: process.env.OPENAI_API_BASE, OPENAI_MODEL: process.env.OPENAI_MODEL, @@ -24,6 +27,9 @@ afterEach(() => { process.env.CLAUDE_CODE_USE_FOUNDRY = originalEnv.CLAUDE_CODE_USE_FOUNDRY process.env.NVIDIA_NIM = originalEnv.NVIDIA_NIM process.env.MINIMAX_API_KEY = originalEnv.MINIMAX_API_KEY + process.env.ANTHROPIC_BASE_URL = originalEnv.ANTHROPIC_BASE_URL + process.env.ANTHROPIC_API_KEY = originalEnv.ANTHROPIC_API_KEY + process.env.ANTHROPIC_MODEL = originalEnv.ANTHROPIC_MODEL process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL process.env.OPENAI_API_BASE = originalEnv.OPENAI_API_BASE process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL @@ -43,6 +49,9 @@ function clearProviderEnv(): void { delete process.env.CLAUDE_CODE_USE_FOUNDRY delete process.env.NVIDIA_NIM delete process.env.MINIMAX_API_KEY + delete process.env.ANTHROPIC_BASE_URL + delete process.env.ANTHROPIC_API_KEY + delete process.env.ANTHROPIC_MODEL delete process.env.OPENAI_BASE_URL delete process.env.OPENAI_API_BASE delete process.env.OPENAI_MODEL @@ -201,6 +210,18 @@ test('env-only MiniMax API key resolves to the minimax provider', async () => { expect(getAPIProvider()).toBe('minimax') }) +test('Anthropic-compatible MiniMax profile resolves to the minimax provider', async () => { + clearProviderEnv() + process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic' + process.env.ANTHROPIC_API_KEY = 'minimax-key' + process.env.ANTHROPIC_MODEL = 'MiniMax-M2.7' + + const { getAPIProvider, usesAnthropicAccountFlow } = + await importFreshProvidersModule() + expect(getAPIProvider()).toBe('minimax') + expect(usesAnthropicAccountFlow()).toBe(false) +}) + test('conflicting OpenAI base prevents env-only MiniMax provider label', async () => { clearProviderEnv() process.env.MINIMAX_API_KEY = 'minimax-key' From fcec9738bfb6874b850bdb36f3204a314bfb6101 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 15 May 2026 09:42:41 -0700 Subject: [PATCH 11/13] fix: include Anthropic key in provider secret source Allow MiniMax profile redaction to include ANTHROPIC_API_KEY in the SecretValueSource type used by sanitizeProviderConfigValue. This fixes the PR-specific TS2353 reported by review while keeping the MiniMax Anthropic-compatible key alias redacted alongside MINIMAX_API_KEY. Validation: bun test --max-concurrency=1 src\utils\providerProfiles.test.ts src\utils\providerFlag.test.ts src\utils\model\providers.test.ts src\integrations\routeMetadata.test.ts; bun run typecheck still has existing repo-wide errors, with no providerProfile.ts matches. --- src/utils/providerProfile.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 993f4998f..be728228e 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -184,6 +184,7 @@ export type ProfileFile = { type SecretValueSource = Partial< Record< | 'OPENAI_API_KEY' + | 'ANTHROPIC_API_KEY' | 'OPENAI_AUTH_HEADER_VALUE' | 'CODEX_API_KEY' | 'GEMINI_API_KEY' From b6991886d98da4fde5738982b05bba5f2310b061 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 15 May 2026 10:06:47 -0700 Subject: [PATCH 12/13] test: stabilize tool history compression smoke Add a narrow compression-enabled override for tests so the compression suites do not depend on shared global config state from the full Bun runner. Pass explicit effective context windows in direct compression tests and use catalog-backed models in shim compression tests to avoid env-capped tier drift. Verified with focused compression tests, full bun test --max-concurrency=1, and bun run build. --- src/services/api/compressToolHistory.test.ts | 94 ++++++++++++------- src/services/api/compressToolHistory.ts | 18 +++- .../api/openaiShim.compression.test.ts | 27 ++++-- 3 files changed, 91 insertions(+), 48 deletions(-) diff --git a/src/services/api/compressToolHistory.test.ts b/src/services/api/compressToolHistory.test.ts index 61c13433a..012ec8f24 100644 --- a/src/services/api/compressToolHistory.test.ts +++ b/src/services/api/compressToolHistory.test.ts @@ -1,6 +1,14 @@ import { afterEach, beforeEach, expect, test } from 'bun:test' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' -import { compressToolHistory, getTiers } from './compressToolHistory.js' +import { + acquireSharedMutationLock, + releaseSharedMutationLock, +} from '../../test/sharedMutationLock.js' +import { + compressToolHistory, + getTiers, + setToolHistoryCompressionEnabledOverrideForTest, +} from './compressToolHistory.js' const originalEnv = { CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, @@ -37,28 +45,35 @@ function setEffectiveWindowForTest(effectiveWindow: number): void { function setCompressionEnabledForTest(enabled: boolean): void { mockState.enabled = enabled + setToolHistoryCompressionEnabledOverrideForTest(enabled) saveGlobalConfig(current => ({ ...current, toolHistoryCompressionEnabled: mockState.enabled, })) } -beforeEach(() => { +beforeEach(async () => { + await acquireSharedMutationLock('compressToolHistory.test.ts') setCompressionEnabledForTest(true) setEffectiveWindowForTest(100_000) }) afterEach(() => { - mockState.enabled = true - mockState.effectiveWindow = 100_000 - for (const key of Object.keys(originalEnv) as Array) { - restoreEnv(key) + try { + mockState.enabled = true + mockState.effectiveWindow = 100_000 + for (const key of Object.keys(originalEnv) as Array) { + restoreEnv(key) + } + saveGlobalConfig(current => ({ + ...current, + toolHistoryCompressionEnabled: + originalConfig.toolHistoryCompressionEnabled, + })) + } finally { + setToolHistoryCompressionEnabledOverrideForTest(undefined) + releaseSharedMutationLock() } - saveGlobalConfig(current => ({ - ...current, - toolHistoryCompressionEnabled: - originalConfig.toolHistoryCompressionEnabled, - })) }) type Block = Record @@ -125,6 +140,14 @@ function getResultText(msg: Msg): string { return '' } +function compressToolHistoryForTest( + messages: T[], + model = 'gpt-4o', + effectiveContextWindowSize = 100_000, +): T[] { + return compressToolHistory(messages, model, { effectiveContextWindowSize }) +} + // ---------- getTiers ---------- test('getTiers: < 16k window → recent=2, mid=3', () => { @@ -160,14 +183,14 @@ test('getTiers: ≥ 500k (gpt-4.1 1M) → recent=25, mid=50', () => { test('pass-through when toolHistoryCompressionEnabled is false', () => { setCompressionEnabledForTest(false) const messages = buildConversation(20) - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') expect(result).toBe(messages) // same reference (no transformation) }) test('pass-through when total tool_results <= recent tier', () => { // 100k effective → recent=5; only 4 exchanges → no compression const messages = buildConversation(4) - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') expect(result).toBe(messages) }) @@ -176,7 +199,7 @@ test('pass-through when total tool_results <= recent tier', () => { test('recent tier: tool_result content untouched', () => { // 100k effective → recent=5, mid=10. With 6 exchanges, only the oldest is touched. const messages = buildConversation(6, 5_000) - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) // Last 5 should be untouched (full 5000 chars) @@ -188,7 +211,7 @@ test('recent tier: tool_result content untouched', () => { test('mid tier: long content truncated to MID_MAX_CHARS with marker', () => { // 100k → recent=5, mid=10. 10 exchanges: 5 recent + 5 mid (none old). const messages = buildConversation(10, 5_000) - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) // First 5 are mid tier — should be truncated to ~2000 chars + marker @@ -204,7 +227,7 @@ test('mid tier: long content truncated to MID_MAX_CHARS with marker', () => { test('mid tier: short content (< MID_MAX_CHARS) untouched', () => { const messages = buildConversation(10, 500) // 500 < MID_MAX_CHARS - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) for (let i = 0; i < 5; i++) { @@ -215,7 +238,7 @@ test('mid tier: short content (< MID_MAX_CHARS) untouched', () => { test('old tier: content replaced with stub [name args={...} → N chars omitted]', () => { // 100k → recent=5, mid=10, old=rest. 20 exchanges → 5 old + 10 mid + 5 recent. const messages = buildConversation(20, 5_000) - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) // First 5 are old tier — should be stubs @@ -249,7 +272,7 @@ test('old tier: stub args truncated to 200 chars', () => { // Pad with enough recent exchanges to push the above into old tier ...buildConversation(20, 100).slice(1), ] - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) const text = getResultText(resultMsgs[0]) @@ -272,7 +295,7 @@ test('old tier: orphan tool_result (no matching tool_use) falls back to "tool"', }, ...buildConversation(20, 100).slice(1), ] - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) const text = getResultText(resultMsgs[0]) @@ -283,7 +306,7 @@ test('old tier: orphan tool_result (no matching tool_use) falls back to "tool"', test('tool_use blocks always preserved', () => { const messages = buildConversation(20, 5_000) - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const useCount = (msgs: Msg[]) => msgs.reduce((sum, m) => { @@ -310,7 +333,7 @@ test('text blocks always preserved', () => { }, ...buildConversation(20, 5_000).slice(1), ] - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const assistantMsg = (result as Msg[])[1] const textBlock = (assistantMsg.content as Block[]).find((b: any) => b.type === 'text') @@ -333,7 +356,7 @@ test('thinking blocks always preserved', () => { }, ...buildConversation(20, 5_000).slice(1), ] - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const assistantMsg = (result as Msg[])[1] const thinking = (assistantMsg.content as Block[]).find((b: any) => b.type === 'thinking') @@ -349,7 +372,7 @@ test('non-array content (string) handled gracefully', () => { { role: 'user', content: 'plain string content' }, ...buildConversation(20, 100).slice(1), ] - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') expect((result as Msg[])[0].content).toBe('plain string content') }) @@ -358,7 +381,7 @@ test('empty content array handled gracefully', () => { { role: 'user', content: [] }, ...buildConversation(20, 100).slice(1), ] - expect(() => compressToolHistory(messages, 'gpt-4o')).not.toThrow() + expect(() => compressToolHistoryForTest(messages, 'gpt-4o')).not.toThrow() }) // ---------- message shape compatibility ---------- @@ -367,7 +390,7 @@ test('wrapped shape ({ message: { role, content } }) handled', () => { type WrappedMsg = { message: { role: string; content: Block[] | string } } const wrap = (m: Msg): WrappedMsg => ({ message: { role: m.role, content: m.content } }) const messages = buildConversation(20, 5_000).map(wrap) - const result = compressToolHistory(messages as any, 'gpt-4o') + const result = compressToolHistoryForTest(messages as any, 'gpt-4o') // First wrapped tool-result message should have stub content (old tier) const firstResultMsg = (result as WrappedMsg[]).find( @@ -384,7 +407,7 @@ test('wrapped shape ({ message: { role, content } }) handled', () => { test('flat shape ({ role, content }) handled', () => { const messages = buildConversation(20, 5_000) - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) expect(getResultText(resultMsgs[0])).toMatch(/^\[Read args=.*→ 5000 chars omitted\]$/) @@ -394,7 +417,7 @@ test('flat shape ({ role, content }) handled', () => { test('tier boundaries: 6 exchanges → 1 mid + 5 recent (recent=5)', () => { const messages = buildConversation(6, 5_000) - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) // Oldest: mid (truncated) @@ -407,7 +430,7 @@ test('tier boundaries: 6 exchanges → 1 mid + 5 recent (recent=5)', () => { test('tier boundaries: 16 exchanges → 1 old + 10 mid + 5 recent', () => { const messages = buildConversation(16, 5_000) - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) // Oldest 1: stub (old tier) @@ -424,9 +447,8 @@ test('tier boundaries: 16 exchanges → 1 old + 10 mid + 5 recent', () => { test('large window (1M) with 30 exchanges: all untouched (recent=25 ≥ 30 - 5)', () => { // ≥500k → recent=25, mid=50. 30 exchanges → 5 mid + 25 recent. None old. - setEffectiveWindowForTest(1_000_000) const messages = buildConversation(30, 5_000) - const result = compressToolHistory(messages, 'gpt-4.1') + const result = compressToolHistoryForTest(messages, 'gpt-4.1', 1_000_000) const resultMsgs = getResultMessages(result) // Last 25: untouched @@ -458,7 +480,7 @@ test('is_error flag preserved in mid tier', () => { // Pad with enough recent exchanges to push the above into MID tier ...buildConversation(10, 100).slice(1), ] - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) const block = getResultBlock(resultMsgs[0]) as { is_error?: boolean; content: unknown } @@ -486,7 +508,7 @@ test('is_error flag preserved in old tier (stub)', () => { }, ...buildConversation(20, 100).slice(1), ] - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) const block = getResultBlock(resultMsgs[0]) as { is_error?: boolean; content: unknown } @@ -515,7 +537,7 @@ test('non-compactable tool (e.g. Task/Agent) is NEVER compressed', () => { // Pad with 20 compactable exchanges to push Task into old tier ...buildConversation(20, 100).slice(1), ] - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) // First tool_result is for Task (non-compactable) → must remain full @@ -541,7 +563,7 @@ test('mcp__ prefixed tools ARE compactable (matches microCompact behavior)', () }, ...buildConversation(20, 100).slice(1), ] - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) // MCP tool result is compressed (gets stub since it's in old tier) @@ -569,7 +591,7 @@ test('blocks already cleared by microCompact are NOT re-compressed', () => { }, ...buildConversation(20, 100).slice(1), ] - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) // Already-cleared marker survives untouched (no double processing) @@ -597,7 +619,7 @@ test('extra block attributes (e.g. cache_control) preserved across rewrites', () }, ...buildConversation(20, 100).slice(1), ] - const result = compressToolHistory(messages, 'gpt-4o') + const result = compressToolHistoryForTest(messages, 'gpt-4o') const resultMsgs = getResultMessages(result) const block = getResultBlock(resultMsgs[0]) as { cache_control?: unknown } diff --git a/src/services/api/compressToolHistory.ts b/src/services/api/compressToolHistory.ts index 465036f0a..7eeab99c6 100644 --- a/src/services/api/compressToolHistory.ts +++ b/src/services/api/compressToolHistory.ts @@ -75,6 +75,14 @@ export function getTiers(effectiveWindow: number): Tiers { return { recent: 25, mid: 50 } } +let toolHistoryCompressionEnabledOverrideForTest: boolean | undefined + +export function setToolHistoryCompressionEnabledOverrideForTest( + enabled: boolean | undefined, +): void { + toolHistoryCompressionEnabledOverrideForTest = enabled +} + function extractText(content: unknown): string { if (typeof content === 'string') return content if (Array.isArray(content)) { @@ -209,12 +217,18 @@ function shouldCompressBlock( export function compressToolHistory( messages: T[], model: string, + options: { effectiveContextWindowSize?: number } = {}, ): T[] { // Master kill-switch. Returns the original reference so callers skip a // defensive copy when the feature is disabled. - if (!getGlobalConfig().toolHistoryCompressionEnabled) return messages + const compressionEnabled = + toolHistoryCompressionEnabledOverrideForTest ?? + getGlobalConfig().toolHistoryCompressionEnabled + if (!compressionEnabled) return messages - const tiers = getTiers(getEffectiveContextWindowSize(model)) + const tiers = getTiers( + options.effectiveContextWindowSize ?? getEffectiveContextWindowSize(model), + ) const toolResultIndices = indexToolResultMessages(messages) const total = toolResultIndices.length diff --git a/src/services/api/openaiShim.compression.test.ts b/src/services/api/openaiShim.compression.test.ts index 1f5923d4c..95139c559 100644 --- a/src/services/api/openaiShim.compression.test.ts +++ b/src/services/api/openaiShim.compression.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, expect, test } from 'bun:test' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js' +import { setToolHistoryCompressionEnabledOverrideForTest } from './compressToolHistory.js' import { createOpenAIShimClient } from './openaiShim.js' type FetchType = typeof globalThis.fetch @@ -27,6 +28,10 @@ const mockState = { effectiveWindow: 100_000, // Copilot gpt-4o tier } +const MID_TIER_MODEL = 'llama-3.1-8b-instant' +const LARGE_CONTEXT_MODEL = 'deepseek-v4-flash' +const SMALL_CONTEXT_MODEL = 'minimax-vision-01' + function restoreEnv(key: keyof typeof originalEnv): void { const value = originalEnv[key] if (value === undefined) { @@ -45,6 +50,7 @@ function setEffectiveWindowForTest(effectiveWindow: number): void { function setCompressionEnabledForTest(enabled: boolean): void { mockState.enabled = enabled + setToolHistoryCompressionEnabledOverrideForTest(enabled) saveGlobalConfig(current => ({ ...current, toolHistoryCompressionEnabled: mockState.enabled, @@ -142,6 +148,7 @@ afterEach(() => { })) globalThis.fetch = originalFetch } finally { + setToolHistoryCompressionEnabledOverrideForTest(undefined) releaseSharedMutationLock() } }) @@ -213,12 +220,12 @@ test('BUG REPRO: without compression, all 30 tool results are sent at full size' // FIX: with compression, recent kept full, mid truncated, old stubbed // ============================================================================ -test('FIX: with compression on Copilot gpt-4o (tier 5/10/rest), 30 turns shrinks dramatically', async () => { +test('FIX: with compression on a 128k model (tier 5/10/rest), 30 turns shrinks dramatically', async () => { mockState.enabled = true mockState.effectiveWindow = 100_000 // 64–128k → recent=5, mid=10 const messages = buildLongConversation(30, 5_000) - const body = await captureRequestBody(messages, 'gpt-4o') + const body = await captureRequestBody(messages, MID_TIER_MODEL) const toolMessages = getToolMessages(body) const payloadSize = JSON.stringify(body).length @@ -250,12 +257,12 @@ test('FIX: with compression on Copilot gpt-4o (tier 5/10/rest), 30 turns shrinks // FIX: large-context model gets generous tiers — compression effectively inert // ============================================================================ -test('FIX: gpt-4.1 (1M context) with 25 exchanges keeps all full (recent tier=25)', async () => { +test('FIX: 1M context model with 25 exchanges keeps all full (recent tier=25)', async () => { mockState.enabled = true mockState.effectiveWindow = 1_000_000 // ≥500k → recent=25, mid=50 const messages = buildLongConversation(25, 5_000) - const body = await captureRequestBody(messages, 'gpt-4.1') + const body = await captureRequestBody(messages, LARGE_CONTEXT_MODEL) const toolMessages = getToolMessages(body) expect(toolMessages.length).toBe(25) @@ -266,12 +273,12 @@ test('FIX: gpt-4.1 (1M context) with 25 exchanges keeps all full (recent tier=25 } }) -test('FIX: gpt-4.1 (1M context) with 30 exchanges → only first 5 mid-truncated', async () => { +test('FIX: 1M context model with 30 exchanges → only first 5 mid-truncated', async () => { mockState.enabled = true mockState.effectiveWindow = 1_000_000 // recent=25, mid=50 const messages = buildLongConversation(30, 5_000) - const body = await captureRequestBody(messages, 'gpt-4.1') + const body = await captureRequestBody(messages, LARGE_CONTEXT_MODEL) const toolMessages = getToolMessages(body) // 30 total: indices 0..4 mid, indices 5..29 recent @@ -292,7 +299,7 @@ test('FIX: stub format includes original tool name and arguments', async () => { mockState.effectiveWindow = 100_000 const messages = buildLongConversation(30, 5_000) - const body = await captureRequestBody(messages, 'gpt-4o') + const body = await captureRequestBody(messages, MID_TIER_MODEL) const toolMessages = getToolMessages(body) const oldestStub = toolMessages[0].content @@ -311,7 +318,7 @@ test('FIX: every tool_call retains its full id, name, and arguments', async () = mockState.effectiveWindow = 100_000 const messages = buildLongConversation(30, 5_000) - const body = await captureRequestBody(messages, 'gpt-4o') + const body = await captureRequestBody(messages, MID_TIER_MODEL) const toolCalls = getAssistantToolCalls(body) as Array<{ id: string function: { name: string; arguments: string } @@ -331,12 +338,12 @@ test('FIX: every tool_call retains its full id, name, and arguments', async () = // FIX: small-context provider (Mistral 32k) gets aggressive compression // ============================================================================ -test('FIX: 32k window (Mistral tier) → recent=3 keeps last 3 only', async () => { +test('FIX: 32k window tier → recent=3 keeps last 3 only', async () => { mockState.enabled = true mockState.effectiveWindow = 24_000 // 16–32k → recent=3, mid=5 const messages = buildLongConversation(15, 3_000) - const body = await captureRequestBody(messages, 'mistral-large-latest') + const body = await captureRequestBody(messages, SMALL_CONTEXT_MODEL) const toolMessages = getToolMessages(body) // 15 total: indices 0..6 old, 7..11 mid, 12..14 recent From a811aeced356a35ba1edc8f4c5c7b0f3e51ad27b Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 15 May 2026 10:20:43 -0700 Subject: [PATCH 13/13] test: stabilize Orama corruption recovery assertion Verify the quarantined corrupted Orama file from the actual persistence directory returned by getOramaPersistencePath, instead of assuming the config-dir projects root used by the full CI runner. Verified with the failing KnowledgeGraph stress test, compression smoke suites, full bun test --max-concurrency=1, and bun run build. --- src/utils/knowledgeGraph.stress.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/knowledgeGraph.stress.test.ts b/src/utils/knowledgeGraph.stress.test.ts index b98508fd4..b1e92f3bf 100644 --- a/src/utils/knowledgeGraph.stress.test.ts +++ b/src/utils/knowledgeGraph.stress.test.ts @@ -10,7 +10,7 @@ import { } from './knowledgeGraph.js' import { mkdtempSync, rmSync, existsSync } from 'fs' import { tmpdir } from 'os' -import { join } from 'path' +import { dirname, join } from 'path' import { acquireEnvMutex, releaseEnvMutex } from '../entrypoints/sdk/shared.js' import { setClaudeConfigHomeDirForTesting } from './envUtils.js' import { getFsImplementation } from './fsOperations.js' @@ -126,9 +126,9 @@ describe('KnowledgeGraph Phase 1 Stress & Edge Cases', () => { // 5. Verify the corrupted file was moved const { readdirSync } = await import('fs') - const projectsBaseDir = join(configDir, 'projects') - expect(existsSync(projectsBaseDir)).toBe(true) - // Search recursively for the corrupted file + const oramaDir = dirname(oramaPath) + expect(existsSync(oramaDir)).toBe(true) + // Search from the actual persistence directory for the corrupted file. const findCorrupted = (dir: string): boolean => { const entries = readdirSync(dir, { withFileTypes: true }) for (const entry of entries) { @@ -140,7 +140,7 @@ describe('KnowledgeGraph Phase 1 Stress & Edge Cases', () => { } return false } - expect(findCorrupted(projectsBaseDir)).toBe(true) + expect(findCorrupted(oramaDir)).toBe(true) }) it('maintains consistency between JSON and Orama', async () => {