diff --git a/src/__tests__/unit/minimax-provider.test.ts b/src/__tests__/unit/minimax-provider.test.ts new file mode 100644 index 00000000..9cdcbc27 --- /dev/null +++ b/src/__tests__/unit/minimax-provider.test.ts @@ -0,0 +1,88 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { generateText } from 'ai'; +import { createOpenAI } from '@ai-sdk/openai'; +import { ClaudeCodeCompatModel } from '../../lib/claude-code-compat'; + +const MODEL_ID = 'MiniMax-M3'; +const API_KEY = 'test-key-not-real'; + +function requestUrl(input: RequestInfo | URL): string { + return typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url; +} + +function anthropicResponse(): Response { + return new Response(JSON.stringify({ + id: 'msg_test', + type: 'message', + role: 'assistant', + model: MODEL_ID, + content: [{ type: 'text', text: 'ok' }], + stop_reason: 'end_turn', + usage: { input_tokens: 1, output_tokens: 1 }, + }), { status: 200, headers: { 'content-type': 'application/json' } }); +} + +function openAiResponse(): Response { + return new Response(JSON.stringify({ + id: 'chatcmpl_test', + object: 'chat.completion', + created: 0, + model: MODEL_ID, + choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), { status: 200, headers: { 'content-type': 'application/json' } }); +} + +describe('MiniMax endpoint request capture', () => { + it('routes regional Anthropic bases to /anthropic/v1/messages', async () => { + const originalFetch = globalThis.fetch; + const urls: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + urls.push(requestUrl(input)); + return anthropicResponse(); + }) as typeof fetch; + + try { + for (const baseUrl of [ + 'https://api.minimaxi.com/anthropic', + 'https://api.minimax.io/anthropic', + ]) { + const model = new ClaudeCodeCompatModel({ + baseUrl, + modelId: MODEL_ID, + authToken: API_KEY, + }); + await generateText({ model, prompt: 'ping' }); + } + } finally { + globalThis.fetch = originalFetch; + } + + assert.deepEqual(urls, [ + 'https://api.minimaxi.com/anthropic/v1/messages', + 'https://api.minimax.io/anthropic/v1/messages', + ]); + }); + + it('routes regional OpenAI bases to /v1/chat/completions', async () => { + const urls: string[] = []; + const captureFetch = (async (input: RequestInfo | URL) => { + urls.push(requestUrl(input)); + return openAiResponse(); + }) as typeof fetch; + + for (const baseURL of [ + 'https://api.minimaxi.com/v1', + 'https://api.minimax.io/v1', + ]) { + const openai = createOpenAI({ apiKey: API_KEY, baseURL, fetch: captureFetch }); + await generateText({ model: openai.chat(MODEL_ID), prompt: 'ping' }); + } + + assert.deepEqual(urls, [ + 'https://api.minimaxi.com/v1/chat/completions', + 'https://api.minimax.io/v1/chat/completions', + ]); + }); +}); diff --git a/src/__tests__/unit/provider-preset.test.ts b/src/__tests__/unit/provider-preset.test.ts index 10c4135f..de9df190 100644 --- a/src/__tests__/unit/provider-preset.test.ts +++ b/src/__tests__/unit/provider-preset.test.ts @@ -73,6 +73,48 @@ describe('Preset Schema Validation', () => { assert.equal(p.authStyle, 'auth_token'); }); + it('MiniMax presets expose M3 and M2.7 capabilities in both regions', () => { + const expectedBaseUrls = new Map([ + ['minimax-cn', 'https://api.minimaxi.com/anthropic'], + ['minimax-global', 'https://api.minimax.io/anthropic'], + ]); + + for (const [key, baseUrl] of expectedBaseUrls) { + const preset = VENDOR_PRESETS.find(v => v.key === key); + assert.ok(preset, `${key} preset must exist`); + assert.equal(preset.baseUrl, baseUrl); + assert.equal(preset.protocol, 'anthropic'); + + const m3 = preset.defaultModels.find(model => model.modelId === 'MiniMax-M3'); + assert.deepEqual(m3, { + modelId: 'MiniMax-M3', + upstreamModelId: 'MiniMax-M3', + displayName: 'MiniMax-M3', + capabilities: { + contextWindow: 1000000, + reasoning: true, + toolUse: true, + vision: true, + supportsAdaptiveThinking: true, + }, + }); + + const m27 = preset.defaultModels.find(model => model.upstreamModelId === 'MiniMax-M2.7'); + assert.deepEqual(m27, { + modelId: 'sonnet', + upstreamModelId: 'MiniMax-M2.7', + displayName: 'MiniMax-M2.7', + role: 'default', + capabilities: { + contextWindow: 204800, + reasoning: true, + toolUse: true, + }, + }); + assert.equal(preset.defaultRoleModels?.default, 'MiniMax-M2.7'); + } + }); + it('openai-compatible preset exists: openai-compatible protocol, api_key auth, no fixed URL, no fabricated catalog', () => { const p = VENDOR_PRESETS.find(v => v.key === 'openai-compatible'); assert.ok(p, 'generic openai-compatible preset must exist'); diff --git a/src/lib/provider-catalog.ts b/src/lib/provider-catalog.ts index 2a9e3300..5fd96eb2 100644 --- a/src/lib/provider-catalog.ts +++ b/src/lib/provider-catalog.ts @@ -673,7 +673,8 @@ export const VENDOR_PRESETS: VendorPreset[] = [ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', }, defaultModels: [ - { modelId: 'sonnet', upstreamModelId: 'MiniMax-M2.7', displayName: 'MiniMax-M2.7', role: 'default' }, + { modelId: 'sonnet', upstreamModelId: 'MiniMax-M2.7', displayName: 'MiniMax-M2.7', role: 'default', capabilities: { contextWindow: 204800, reasoning: true, toolUse: true } }, + { modelId: 'MiniMax-M3', upstreamModelId: 'MiniMax-M3', displayName: 'MiniMax-M3', capabilities: { contextWindow: 1000000, reasoning: true, toolUse: true, vision: true, supportsAdaptiveThinking: true } }, ], defaultRoleModels: { default: 'MiniMax-M2.7', @@ -706,7 +707,8 @@ export const VENDOR_PRESETS: VendorPreset[] = [ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', }, defaultModels: [ - { modelId: 'sonnet', upstreamModelId: 'MiniMax-M2.7', displayName: 'MiniMax-M2.7', role: 'default' }, + { modelId: 'sonnet', upstreamModelId: 'MiniMax-M2.7', displayName: 'MiniMax-M2.7', role: 'default', capabilities: { contextWindow: 204800, reasoning: true, toolUse: true } }, + { modelId: 'MiniMax-M3', upstreamModelId: 'MiniMax-M3', displayName: 'MiniMax-M3', capabilities: { contextWindow: 1000000, reasoning: true, toolUse: true, vision: true, supportsAdaptiveThinking: true } }, ], defaultRoleModels: { default: 'MiniMax-M2.7',