mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-11-06 12:50:54 -04:00
feat(minimax): default to M3.1 Flash and refresh text suggestions (#2297)
* feat(minimax): default to M3.1 Flash and refresh text suggestions * fix(minimax): preserve explicit selections and retire stale Zen suggestions * fix(minimax): preserve Ollama discovery and validate cached options * fix(minimax): preserve local discovery and validate picker caches * fix(minimax): preserve gateway-owned model availability --------- Co-authored-by: octo-patch <266937838+octo-patch@users.noreply.github.com> Co-authored-by: jatmn <the@jat.mn>
This commit is contained in:
co-authored by
octo-patch
jatmn
parent
e3c9a873dc
commit
8be7274b90
@@ -366,7 +366,7 @@ Advanced and source-build guides:
|
||||
| Codex OAuth | `/provider` | Opens ChatGPT sign-in in your browser and stores Codex credentials securely |
|
||||
| Codex | `/provider` | Uses existing Codex CLI auth, OpenClaude secure storage, or env credentials |
|
||||
| Twigpine Opengateway | Startup default, `/provider`, or env vars | Smart gateway at `https://opengateway.gitlawb.com/v1`; requires an API key from https://gitlawb.com/opengateway/keys and routes Xiaomi MiMo and other partner models by `OPENAI_MODEL` |
|
||||
| OpenCode Zen | `/provider` or env vars | Pay-as-you-go AI gateway (48 models); uses `OPENCODE_API_KEY` via `https://opencode.ai/zen/v1`; shared key with OpenCode Go |
|
||||
| OpenCode Zen | `/provider` or env vars | Pay-as-you-go AI gateway; uses `OPENCODE_API_KEY` via `https://opencode.ai/zen/v1`; shared key with OpenCode Go |
|
||||
| OpenCode Go | `/provider` or env vars | $10/mo subscription for open models (13 models); uses `OPENCODE_API_KEY` via `https://opencode.ai/zen/go/v1`; shared key with OpenCode Zen |
|
||||
| Xiaomi MiMo | `/provider` or env vars | OpenAI-compatible API at `https://mimo.mi.com`; uses `MIMO_API_KEY` and defaults to `mimo-v2.5-pro` |
|
||||
| NEAR AI | `/provider` or env vars | Unified gateway (Claude, GPT, Gemini + TEE open models); uses `NEARAI_API_KEY` at `https://cloud-api.near.ai/v1` |
|
||||
|
||||
@@ -256,8 +256,8 @@ export OPENAI_MODEL=gpt-5.4
|
||||
openclaude
|
||||
```
|
||||
|
||||
OpenCode Zen is a pay-as-you-go AI gateway with 48 models (GPT, Claude, Gemini,
|
||||
Qwen, MiniMax, GLM, Kimi, Grok, Big Pickle, DeepSeek, Nemotron). Uses the same
|
||||
OpenCode Zen is a pay-as-you-go AI gateway with models including GPT, Claude, Gemini,
|
||||
Qwen, MiniMax, GLM, Kimi, Grok, Big Pickle, DeepSeek, and Nemotron. Uses the same
|
||||
`OPENCODE_API_KEY` as OpenCode Go. Get your key from https://opencode.ai.
|
||||
|
||||
### OpenCode Go (subscription)
|
||||
@@ -853,6 +853,12 @@ source provider's dedicated key into the new launch.
|
||||
|
||||
When a saved provider profile is active, `/model` can either show the provider's
|
||||
catalog/discovered models or only the models explicitly listed in the profile.
|
||||
MiniMax text-model retirement applies to the direct MiniMax route; other
|
||||
providers retain the MiniMax IDs their own catalogs or discovery advertise.
|
||||
OpenCode Zen excludes its obsolete `minimax-m3-free` and
|
||||
`minimax-m3.1-flash-free` suggestions. Saved or explicitly configured selections
|
||||
remain available.
|
||||
|
||||
Configure this in `~/.openclaude.json`:
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { snapshotEnv } from '../../test/envSnapshot.js'
|
||||
|
||||
import { afterAll, afterEach, beforeEach, expect, mock, setSystemTime, test } from 'bun:test'
|
||||
import React from 'react'
|
||||
@@ -698,6 +699,70 @@ test('/model still applies a Claude alias on a non-Command-Code OpenAI route', a
|
||||
expect(messages[0]).not.toContain('requires the Anthropic Messages protocol')
|
||||
})
|
||||
|
||||
test.each([
|
||||
['MiniMax-M2.5', true],
|
||||
['MiniMax-M2.7', true],
|
||||
['MiniMax-Text-01', true],
|
||||
['abab6.5s-chat', true],
|
||||
['abab6.5-chat', true],
|
||||
['abab6.5g-chat', true],
|
||||
['abab6-chat', true],
|
||||
['MiniMax-M3', true],
|
||||
['MiniMax-M3.1-Flash', true],
|
||||
['custom-minimax-deployment', false],
|
||||
])('/model preserves explicit MiniMax selection validity for %s', async (model, valid) => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://api.minimax.io/v1'
|
||||
process.env.OPENAI_API_KEY = 'minimax-test-key'
|
||||
process.env.OPENAI_MODEL = 'MiniMax-M3.1-Flash'
|
||||
for (const key of [
|
||||
'OPENAI_API_BASE', 'CLAUDE_CODE_USE_GEMINI', 'CLAUDE_CODE_USE_GITHUB',
|
||||
'CLAUDE_CODE_USE_MISTRAL', 'CLAUDE_CODE_USE_BEDROCK',
|
||||
'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY',
|
||||
]) delete process.env[key]
|
||||
mockProviderProfiles()
|
||||
|
||||
const { call } = await importFreshModelModule(`minimax-explicit-${model}`)
|
||||
const { AppStateProvider, getDefaultAppState, useAppState } = await import('../../state/AppState.js')
|
||||
const { render } = await import('../../ink.js')
|
||||
const messages: string[] = []
|
||||
let selectedModel: ModelSetting | undefined
|
||||
function CaptureModel() {
|
||||
selectedModel = useAppState(state => state.mainLoopModel)
|
||||
return null
|
||||
}
|
||||
const stdout = new PassThrough()
|
||||
;(stdout as unknown as { columns: number }).columns = 120
|
||||
const originalFetch = globalThis.fetch
|
||||
const fetch = mock(async () => { throw new Error('Known model validation must not call the API') })
|
||||
globalThis.fetch = fetch as unknown as typeof globalThis.fetch
|
||||
let instance: Awaited<ReturnType<typeof render>> | undefined
|
||||
try {
|
||||
const element = await call(result => { if (result) messages.push(result) }, {} as never, model)
|
||||
instance = await render(
|
||||
<AppStateProvider initialState={{ ...getDefaultAppState(), mainLoopModel: 'MiniMax-M3.1-Flash' }}>
|
||||
{element}
|
||||
<CaptureModel />
|
||||
</AppStateProvider>,
|
||||
stdout as unknown as NodeJS.WriteStream,
|
||||
)
|
||||
await waitForCondition(() => messages.length > 0)
|
||||
if (valid) {
|
||||
expect(messages[0]).toContain('Set model to')
|
||||
await waitForCondition(() => selectedModel === model)
|
||||
expect(selectedModel).toBe(model)
|
||||
} else {
|
||||
expect(messages[0]).toContain(`Model '${model}' not found in MiniMax catalog`)
|
||||
expect(selectedModel).toBe('MiniMax-M3.1-Flash')
|
||||
}
|
||||
expect(fetch).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
instance?.unmount()
|
||||
stdout.end()
|
||||
globalThis.fetch = originalFetch
|
||||
}
|
||||
})
|
||||
|
||||
test('opens the model picker without awaiting descriptor-backed route refresh', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1'
|
||||
@@ -3302,6 +3367,54 @@ test('/model refresh treats empty legacy OpenAI-compatible discovery as failed n
|
||||
)
|
||||
})
|
||||
|
||||
test.each([
|
||||
['ollama', 'http://localhost:11434/v1'],
|
||||
['lmstudio', 'http://localhost:1234/v1'],
|
||||
['atomic-chat', 'http://127.0.0.1:1337/v1'],
|
||||
['openrouter', 'https://openrouter.ai/api/v1'],
|
||||
])('%s picker retains advertised MiniMax models before and after refresh', async (provider, baseUrl) => {
|
||||
const restoreEnv = snapshotEnv([
|
||||
'CLAUDE_CODE_USE_OPENAI', 'OPENAI_BASE_URL', 'OPENAI_API_BASE',
|
||||
'OPENAI_API_KEY', 'OPENAI_MODEL', 'CLAUDE_CODE_USE_GEMINI',
|
||||
'CLAUDE_CODE_USE_GITHUB', 'CLAUDE_CODE_USE_MISTRAL',
|
||||
'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY',
|
||||
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED', 'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID',
|
||||
], { clear: true })
|
||||
let rendered: Awaited<ReturnType<typeof renderModelCommandWithCapturedPicker>> | undefined
|
||||
try {
|
||||
const activeProfile = {
|
||||
id: 'local-profile', name: provider, provider,
|
||||
baseUrl, model: 'qwen3:latest', apiKey: '',
|
||||
}
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = activeProfile.baseUrl
|
||||
process.env.OPENAI_MODEL = activeProfile.model
|
||||
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED = '1'
|
||||
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID = activeProfile.id
|
||||
mockProviderProfiles({ getActiveProviderProfile: () => activeProfile })
|
||||
const models = provider === 'openrouter'
|
||||
? ['minimax/minimax-m2.5', 'minimax/minimax-m2.7']
|
||||
: ['minimax-m3.1-flash:cloud', 'minimax-m3:cloud', 'minimax-m2.7:cloud', 'minimaxai/minimax-m2.7', 'minimax-m3.1-flash-q4_k_m']
|
||||
mockDescriptorDiscovery({
|
||||
routeId: provider,
|
||||
cachedModels: [{ id: models[0]!, apiName: models[0]! }],
|
||||
discoveredModels: models.map(id => ({ id, apiName: id })),
|
||||
})
|
||||
rendered = await renderModelCommandWithCapturedPicker(`${provider}-minimax-discovery`)
|
||||
const pickerValues = () => (rendered!.getCapturedProps().optionsOverride as ModelOption[]).map(option => option.value)
|
||||
expect(pickerValues()).toContain(models[0])
|
||||
expect(pickerValues()).not.toContain(models[1])
|
||||
expect(rendered.getCapturedProps().onRefresh).toEqual(expect.any(Function))
|
||||
rendered.getCapturedProps().onRefresh!()
|
||||
await waitForCondition(() => pickerValues().includes(models[1]!))
|
||||
for (const model of models) expect(pickerValues()).toContain(model)
|
||||
} finally {
|
||||
rendered?.instance.unmount()
|
||||
rendered?.stdout.end()
|
||||
restoreEnv()
|
||||
}
|
||||
})
|
||||
|
||||
test('interactive model picker refresh keeps descriptor options allowlist-filtered', async () => {
|
||||
useSettings({ availableModels: ['allowed-route'] } as SettingsJson)
|
||||
const activeProfile = {
|
||||
|
||||
@@ -497,6 +497,7 @@ async function loadDescriptorDiscoveryContext(
|
||||
routeLabel,
|
||||
staticEntries,
|
||||
routeDefaultModel,
|
||||
routeId,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -553,6 +554,7 @@ async function loadDescriptorDiscoveryContext(
|
||||
routeLabel,
|
||||
mergedEntries,
|
||||
routeDefaultModel,
|
||||
routeId,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -933,6 +935,7 @@ function ModelPickerWrapper({
|
||||
discoveryContext.routeLabel,
|
||||
result?.models ?? [],
|
||||
discoveryContext.routeDefaultModel,
|
||||
discoveryContext.routeId,
|
||||
),
|
||||
{
|
||||
profileModelSurface: discoveryContext.profileModelSurface,
|
||||
@@ -1285,6 +1288,7 @@ async function refreshModelsAndSummarize(): Promise<string> {
|
||||
discoveryContext.routeLabel,
|
||||
result?.models ?? [],
|
||||
discoveryContext.routeDefaultModel,
|
||||
discoveryContext.routeId,
|
||||
),
|
||||
{
|
||||
profileModelSurface: discoveryContext.profileModelSurface,
|
||||
|
||||
@@ -13,6 +13,7 @@ export default defineBrand({
|
||||
supportsPreciseTokenCount: false,
|
||||
},
|
||||
modelIds: [
|
||||
'minimax-m3.1-flash',
|
||||
'minimax-m3',
|
||||
'minimax-m2',
|
||||
'minimax-m2.1',
|
||||
|
||||
@@ -75,6 +75,9 @@ const zenModels: OpenCodeCatalogSpec[] = [
|
||||
{ id: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash' },
|
||||
{ id: 'glm-5.1', label: 'GLM 5.1', zaiGlm: true },
|
||||
{ id: 'glm-5', label: 'GLM 5', zaiGlm: true },
|
||||
// Retain historical identity for saved selections; suggestion policy hides it.
|
||||
{ id: 'minimax-m3.1-flash-free', label: 'MiniMax M3.1 Flash Free' },
|
||||
{ id: 'minimax-m3', label: 'MiniMax M3' },
|
||||
{ id: 'minimax-m2.7', label: 'MiniMax M2.7' },
|
||||
{ id: 'minimax-m2.5', label: 'MiniMax M2.5' },
|
||||
{ id: 'kimi-k2.6', label: 'Kimi K2.6' },
|
||||
@@ -110,7 +113,7 @@ export default defineGateway({
|
||||
preset: {
|
||||
id: 'opencode',
|
||||
vendorId: 'openai',
|
||||
description: 'OpenCode Zen - pay-as-you-go AI gateway (48 models)',
|
||||
description: 'OpenCode Zen - pay-as-you-go AI gateway',
|
||||
apiKeyEnvVars: ['OPENCODE_API_KEY'],
|
||||
modelEnvVars: ['OPENAI_MODEL'],
|
||||
},
|
||||
|
||||
@@ -474,7 +474,7 @@ export const PROVIDER_PRESET_MANIFEST = [
|
||||
"routeId": "opencode",
|
||||
"vendorId": "openai",
|
||||
"gatewayId": "opencode",
|
||||
"description": "OpenCode Zen - pay-as-you-go AI gateway (48 models)",
|
||||
"description": "OpenCode Zen - pay-as-you-go AI gateway",
|
||||
"apiKeyEnvVars": [
|
||||
"OPENCODE_API_KEY"
|
||||
],
|
||||
|
||||
@@ -10,6 +10,17 @@ const minimaxM2Capabilities = {
|
||||
}
|
||||
|
||||
export default [
|
||||
defineModel({
|
||||
id: 'minimax-m3.1-flash',
|
||||
label: 'MiniMax M3.1 Flash',
|
||||
brandId: 'minimax',
|
||||
vendorId: 'minimax',
|
||||
classification: ['chat', 'reasoning', 'vision', 'coding'],
|
||||
defaultModel: 'MiniMax-M3.1-Flash',
|
||||
capabilities: minimaxM2Capabilities,
|
||||
contextWindow: 1_000_000,
|
||||
maxOutputTokens: 524_288,
|
||||
}),
|
||||
defineModel({
|
||||
id: 'minimax-m2',
|
||||
label: 'MiniMax M2',
|
||||
@@ -99,7 +110,7 @@ export default [
|
||||
classification: ['chat', 'reasoning', 'coding'],
|
||||
defaultModel: 'MiniMax-M3',
|
||||
capabilities: minimaxM2Capabilities,
|
||||
contextWindow: 1_048_576,
|
||||
contextWindow: 1_000_000,
|
||||
maxOutputTokens: 131_072,
|
||||
}),
|
||||
defineModel({
|
||||
|
||||
@@ -11,8 +11,8 @@ type OpenCodeModelSpec = {
|
||||
id: string
|
||||
label: string
|
||||
defaultModel: string
|
||||
contextWindow: number
|
||||
maxOutputTokens: number
|
||||
contextWindow?: number
|
||||
maxOutputTokens?: number
|
||||
reasoning?: boolean
|
||||
vision?: boolean
|
||||
coding?: boolean
|
||||
@@ -45,6 +45,9 @@ function openCodeModel(spec: OpenCodeModelSpec) {
|
||||
}
|
||||
|
||||
const zenModels: OpenCodeModelSpec[] = [
|
||||
// Zen publishes no token limits. Historical descriptors do not imply availability.
|
||||
{ id: 'opencode-minimax-m3.1-flash-free', label: 'MiniMax M3.1 Flash Free', defaultModel: 'minimax-m3.1-flash-free', reasoning: true, coding: true },
|
||||
{ id: 'opencode-minimax-m3', label: 'MiniMax M3', defaultModel: 'minimax-m3', reasoning: true, coding: true },
|
||||
{ id: 'opencode-claude-fable-5', label: 'Claude Fable 5', defaultModel: 'claude-fable-5', contextWindow: 1_000_000, maxOutputTokens: 128_000, reasoning: true, vision: true },
|
||||
{ id: 'opencode-claude-opus-4-8', label: 'Claude Opus 4.8', defaultModel: 'claude-opus-4-8', contextWindow: 1_000_000, maxOutputTokens: 128_000, reasoning: true, vision: true },
|
||||
{ id: 'opencode-claude-opus-4-7', label: 'Claude Opus 4.7', defaultModel: 'claude-opus-4-7', contextWindow: 1_000_000, maxOutputTokens: 128_000, reasoning: true, vision: true },
|
||||
|
||||
Vendored
+3
-2
@@ -8,7 +8,7 @@ export default defineVendor({
|
||||
label: 'MiniMax',
|
||||
classification: 'native',
|
||||
defaultBaseUrl: MINIMAX_GLOBAL_ANTHROPIC_BASE_URL,
|
||||
defaultModel: 'MiniMax-M3',
|
||||
defaultModel: 'MiniMax-M3.1-Flash',
|
||||
requiredEnvVars: ['MINIMAX_API_KEY'],
|
||||
setup: {
|
||||
requiresAuth: true,
|
||||
@@ -37,6 +37,7 @@ export default defineVendor({
|
||||
catalog: {
|
||||
source: 'static',
|
||||
models: [
|
||||
{ id: 'minimax-m3.1-flash', apiName: 'MiniMax-M3.1-Flash', label: 'MiniMax M3.1 Flash', modelDescriptorId: 'minimax-m3.1-flash', contextWindow: 1_000_000 },
|
||||
{ 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 },
|
||||
@@ -44,7 +45,7 @@ export default defineVendor({
|
||||
{ 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-m3', apiName: 'MiniMax-M3', label: 'MiniMax M3', modelDescriptorId: 'minimax-m3', contextWindow: 1_048_576 },
|
||||
{ id: 'minimax-m3', apiName: 'MiniMax-M3', label: 'MiniMax M3', modelDescriptorId: 'minimax-m3', contextWindow: 1_000_000 },
|
||||
{ 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' },
|
||||
|
||||
@@ -761,6 +761,66 @@ test('routes env-only MiniMax requests through the Anthropic-compatible API', as
|
||||
})
|
||||
})
|
||||
|
||||
for (const host of ['api.minimax.io', 'api.minimaxi.com']) {
|
||||
for (const protocol of ['anthropic', 'openai'] as const) {
|
||||
for (const savedModel of [undefined, 'MiniMax-M3', 'MiniMax-M2.7', 'custom-minimax-deployment']) {
|
||||
test(`MiniMax ${host} ${protocol} serializes ${savedModel ?? 'Flash default'} unchanged`, async () => {
|
||||
clearEnvForMiniMaxOnlyTest()
|
||||
process.env.MINIMAX_API_KEY = 'minimax-test-key'
|
||||
if (protocol === 'anthropic') {
|
||||
process.env.ANTHROPIC_BASE_URL = `https://${host}/anthropic`
|
||||
if (savedModel) process.env.ANTHROPIC_MODEL = savedModel
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = `https://${host}/v1`
|
||||
if (savedModel) process.env.OPENAI_MODEL = savedModel
|
||||
}
|
||||
const { getDefaultMainLoopModel } = await import('../../utils/model/model.js')
|
||||
const model = getDefaultMainLoopModel()
|
||||
expect(model).toBe(savedModel ?? 'MiniMax-M3.1-Flash')
|
||||
let requestURL = ''
|
||||
let requestHeaders = new Headers()
|
||||
let requestBody: Record<string, unknown> = {}
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const request = new Request(input, init)
|
||||
requestURL = request.url
|
||||
requestHeaders = request.headers
|
||||
requestBody = await request.json() as Record<string, unknown>
|
||||
const response = protocol === 'anthropic'
|
||||
? { id: 'msg-minimax', type: 'message', role: 'assistant', model, content: [{ type: 'text', text: 'ok' }], stop_reason: 'end_turn', usage: { input_tokens: 1, output_tokens: 1 } }
|
||||
: { id: 'chat-minimax', object: 'chat.completion', model, choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], usage: { prompt_tokens: 1, completion_tokens: 1 } }
|
||||
return new Response(JSON.stringify(response), { headers: { 'content-type': 'application/json' } })
|
||||
}) as FetchType
|
||||
const client = await getAnthropicClient({ maxRetries: 0, model })
|
||||
await client.beta.messages.create({
|
||||
model, max_tokens: 16,
|
||||
messages: [{ role: 'user', content: savedModel ? 'hello' : [
|
||||
{ type: 'text', text: 'describe this image' },
|
||||
{ type: 'image', source: { type: 'url', url: 'https://example.com/fixture.png' } },
|
||||
] }],
|
||||
})
|
||||
if (!savedModel) {
|
||||
expect(requestBody.messages).toEqual([{
|
||||
role: 'user', content: [
|
||||
{ type: 'text', text: 'describe this image' },
|
||||
protocol === 'anthropic'
|
||||
? { type: 'image', source: { type: 'url', url: 'https://example.com/fixture.png' } }
|
||||
: { type: 'image_url', image_url: { url: 'https://example.com/fixture.png' } },
|
||||
],
|
||||
}])
|
||||
}
|
||||
expect(requestURL).toContain(`https://${host}/${protocol === 'anthropic' ? 'anthropic/v1/messages' : 'v1/chat/completions'}`)
|
||||
expect(requestHeaders.get(protocol === 'anthropic' ? 'x-api-key' : 'authorization')).toBe(protocol === 'anthropic' ? 'minimax-test-key' : 'Bearer minimax-test-key')
|
||||
expect(requestBody.model).toBe(model)
|
||||
expect(requestBody.service_tier).toBeUndefined()
|
||||
expect(requestBody.thinking).not.toEqual({ type: 'disabled' })
|
||||
expect(requestBody.reasoning_effort).not.toBe('none')
|
||||
expect(requestBody.reasoning_split).not.toBe(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const credentialEnv of ['ANTHROPIC_API_KEY', 'MINIMAX_API_KEY']) {
|
||||
for (const dormantBaseEnv of ['OPENAI_BASE_URL', 'OPENAI_API_BASE']) {
|
||||
for (const host of ['api.minimax.io', 'api.minimaxi.com', 'api.minimax.chat']) {
|
||||
|
||||
@@ -2048,6 +2048,30 @@ test('xiaomi mimo token plan uses raw api-key and OpenAI-compatible reasoning_ef
|
||||
expect(capturedBody).not.toHaveProperty('stream_options')
|
||||
})
|
||||
|
||||
test.each([
|
||||
['https://opencode.ai/zen/v1', 'minimax-m3.1-flash-free'],
|
||||
['https://opencode.ai/zen/v1', 'minimax-m3'],
|
||||
['https://openrouter.ai/api/v1', 'minimax/minimax-m3.1-flash:free'],
|
||||
])('MiniMax gateway %s preserves the explicit model ID %s at HTTP boundary', async (baseURL, model) => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = baseURL
|
||||
process.env.OPENAI_MODEL = model
|
||||
process.env.OPENAI_API_KEY = 'gateway-fixture-key'
|
||||
let capturedURL = ''
|
||||
let body: Record<string, unknown> = {}
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
capturedURL = String(input)
|
||||
body = JSON.parse(String(init?.body))
|
||||
return makeChatCompletionResponse(model)
|
||||
}) as unknown as FetchType
|
||||
const client = createOpenAIShimClient({}) as OpenAIShimClient
|
||||
await client.beta.messages.create({ model, messages: [{ role: 'user', content: 'hello' }], max_tokens: 32, stream: false })
|
||||
expect(capturedURL).toBe(`${baseURL}/chat/completions`)
|
||||
expect(body.model).toBe(model)
|
||||
expect(body.thinking).not.toEqual({ type: 'disabled' })
|
||||
expect(body.reasoning_effort).not.toBe('none')
|
||||
})
|
||||
|
||||
test.each([
|
||||
'minimax-m3',
|
||||
'minimax-m2.7',
|
||||
|
||||
@@ -510,7 +510,9 @@ test('MiniMax China uses the M3 context limit before client setup', () => {
|
||||
try {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api.minimaxi.com/anthropic'
|
||||
process.env.ANTHROPIC_API_KEY = 'china-test-key'
|
||||
expect(getContextWindowForModel('MiniMax-M3')).toBe(1_048_576)
|
||||
expect(getContextWindowForModel('MiniMax-M3')).toBe(1_000_000)
|
||||
expect(getContextWindowForModel('MiniMax-M3.1-Flash')).toBe(1_000_000)
|
||||
expect(getMaxOutputTokensForModel('MiniMax-M3.1-Flash')).toBe(524_288)
|
||||
} finally {
|
||||
if (savedBaseUrl === undefined) delete process.env.ANTHROPIC_BASE_URL
|
||||
else process.env.ANTHROPIC_BASE_URL = savedBaseUrl
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
import { getCatalogEntriesForRoute, getModel } from '../../integrations/index.js'
|
||||
import { isCurrentMiniMaxModelOption } from './minimaxCatalog.js'
|
||||
import { buildRouteCatalogModelOptions, mergeRouteCatalogEntries } from './routeCatalogOptions.js'
|
||||
|
||||
for (const model of ['MiniMax-M2', 'MiniMax-M2.7-highspeed', 'minimax/minimax-m2.5:free', 'MiniMaxAI/MiniMax-M1', 'minimax-text-01', 'abab6.5-chat', 'minimax-m3-free', 'minimax-m3.1-flash-free']) {
|
||||
test(`retired text suggestion is filtered: ${model}`, () => {
|
||||
expect(isCurrentMiniMaxModelOption(model)).toBe(false)
|
||||
})
|
||||
}
|
||||
|
||||
for (const model of ['MiniMax-M3.1-Flash', 'minimax/minimax-m3.1-flash:free', 'minimaxai/minimax-m3', 'minimax/minimax-m3:free', 'MiniMax-Vision-01', 'speech-02-hd', 'music-2.6', 'image-01', 'MiniMax-Hailuo-2.3', 'gpt-4o', 'my-custom-model']) {
|
||||
test(`current, custom, or nontext suggestion is preserved: ${model}`, () => {
|
||||
expect(isCurrentMiniMaxModelOption(model)).toBe(true)
|
||||
})
|
||||
}
|
||||
|
||||
test('Zen catalogs retain available gateway models but exclude obsolete free suggestions', () => {
|
||||
const entries = mergeRouteCatalogEntries(getCatalogEntriesForRoute('opencode'), [
|
||||
{ id: 'minimax/minimax-m2.7', apiName: 'minimax/minimax-m2.7' },
|
||||
{ id: 'minimax-m3-free', apiName: 'minimax-m3-free' },
|
||||
{ id: 'minimax-m3.1-flash-free', apiName: 'minimax-m3.1-flash-free' },
|
||||
{ id: 'minimax/minimax-m3.1-flash:free', apiName: 'minimax/minimax-m3.1-flash:free' },
|
||||
])
|
||||
const values = buildRouteCatalogModelOptions('Gateway', entries, undefined, 'opencode').map(option => option.value)
|
||||
expect(values).not.toContain('minimax-m3.1-flash-free')
|
||||
expect(values).not.toContain('minimax-m3-free')
|
||||
expect(values).toContain('minimax-m3')
|
||||
expect(values).toContain('minimax/minimax-m3.1-flash:free')
|
||||
expect(values).toContain('minimax-m2.5')
|
||||
expect(values).toContain('minimax-m2.7')
|
||||
expect(values).toContain('minimax/minimax-m2.7')
|
||||
// Filtering suggestions must not delete metadata used by explicit requests.
|
||||
expect(getModel('minimax-m2.7')?.capabilities.supportsVision).toBe(false)
|
||||
expect(getCatalogEntriesForRoute('opencode').some(entry => entry.apiName === 'minimax-m2.7')).toBe(true)
|
||||
expect(getCatalogEntriesForRoute('opencode').some(entry => entry.apiName === 'minimax-m3.1-flash-free')).toBe(true)
|
||||
expect(getModel('opencode-minimax-m3.1-flash-free')?.defaultModel).toBe('minimax-m3.1-flash-free')
|
||||
})
|
||||
|
||||
test.each(['openrouter', 'opencode-go', 'custom'])('%s discovery owns its MiniMax availability', routeId => {
|
||||
const models = ['minimax-m2.5', 'minimax/minimax-m2.7']
|
||||
expect(buildRouteCatalogModelOptions(
|
||||
'Gateway', models.map(apiName => ({ id: apiName, apiName })), undefined, routeId,
|
||||
).map(option => option.value)).toEqual(models)
|
||||
})
|
||||
|
||||
test.each(['ollama', 'lmstudio', 'atomic-chat'])('%s catalog options retain provider-owned MiniMax model identities', routeId => {
|
||||
const models = ['minimax-m3.1-flash:cloud', 'minimax-m3:cloud', 'minimax-m2.7:cloud', 'minimaxai/minimax-m2.7', 'minimax-m3.1-flash-q4_k_m']
|
||||
const options = buildRouteCatalogModelOptions(
|
||||
'Custom display label',
|
||||
models.map(apiName => ({ id: apiName, apiName })),
|
||||
undefined,
|
||||
routeId,
|
||||
)
|
||||
expect(options.map(option => option.value)).toEqual(models)
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getTransportKindForRoute } from '../../integrations/routeMetadata.js'
|
||||
|
||||
/** Filter ordinary suggestions only; explicit saved/custom model IDs stay valid. */
|
||||
export function isCurrentMiniMaxModelOption(
|
||||
model: unknown,
|
||||
routeId?: string | null,
|
||||
localEndpoint = false,
|
||||
): boolean {
|
||||
if (model === null) return true
|
||||
// Persisted bootstrap options reach this boundary without normalization.
|
||||
if (typeof model !== 'string') return false
|
||||
const name = model.trim().toLowerCase().split('/').at(-1)!.split('?', 1)[0]!
|
||||
if (!/^(minimax-(?:m\d|text-|\d)|abab\d)/.test(name)) return true
|
||||
// Independently hosted servers own their installed/discovered model names.
|
||||
if (localEndpoint || (routeId && getTransportKindForRoute(routeId) === 'local')) return true
|
||||
// Direct MiniMax retirement does not retire independently served gateway models.
|
||||
if (routeId === 'opencode') {
|
||||
return name !== 'minimax-m3-free' && name !== 'minimax-m3.1-flash-free'
|
||||
}
|
||||
if (routeId && routeId !== 'minimax') return true
|
||||
// No route is the conservative precheck used before resolving picker context.
|
||||
return /^(minimax-m3(?:\.1-flash)?)(?::free)?$/.test(name)
|
||||
}
|
||||
@@ -25,9 +25,13 @@ afterEach(() => {
|
||||
}
|
||||
})
|
||||
|
||||
test('MiniMax picker catalog includes current M2.7 models', () => {
|
||||
test('MiniMax picker offers Flash first, M3, and unchanged vision models', () => {
|
||||
const values = getCachedMiniMaxModelOptions().map(option => option.value)
|
||||
|
||||
expect(values).toContain('MiniMax-M2.7')
|
||||
expect(values).toContain('MiniMax-M2.7-highspeed')
|
||||
expect(values).toEqual([
|
||||
'MiniMax-M3.1-Flash',
|
||||
'MiniMax-M3',
|
||||
'MiniMax-Vision-01',
|
||||
'MiniMax-Vision-01-Fast',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -1,40 +1,32 @@
|
||||
/**
|
||||
* MiniMax model list for the /model picker.
|
||||
* Full model catalog from MiniMax API.
|
||||
*/
|
||||
|
||||
/** MiniMax suggestions share the route catalog used by provider profiles. */
|
||||
import { getCatalogEntriesForRoute, getRouteDefaultModel } from '../../integrations/index.js'
|
||||
import type { ModelOption } from './modelOptions.js'
|
||||
import { getAPIProvider } from './providers.js'
|
||||
import { buildRouteCatalogModelOptions } from './routeCatalogOptions.js'
|
||||
|
||||
export function isMiniMaxProvider(): boolean {
|
||||
return getAPIProvider() === 'minimax'
|
||||
}
|
||||
|
||||
function getMiniMaxModels(): ModelOption[] {
|
||||
return [
|
||||
// Latest Generation Models - use correct MiniMax naming with M prefix
|
||||
{ value: 'MiniMax-M2', label: 'MiniMax M2', description: 'MoE model - 131K context - Chat/Code/Reasoning' },
|
||||
{ value: 'MiniMax-M2.1', label: 'MiniMax M2.1', description: 'Enhanced - 200K context - Vision' },
|
||||
{ value: 'MiniMax-M2.5', label: 'MiniMax M2.5', description: 'Flagship - 256K context - Vision/Function-calling' },
|
||||
{ value: 'MiniMax-M2.7', label: 'MiniMax M2.7', description: 'Flagship - 256K context - Chat/Code/Reasoning' },
|
||||
{ value: 'MiniMax-M2.7-highspeed', label: 'MiniMax M2.7 Highspeed', description: 'Fast flagship - 256K context - Chat/Code/Reasoning' },
|
||||
{ value: 'MiniMax-M3', label: 'MiniMax M3', description: 'Next-gen - 1M context - Coding/Agentic/Reasoning' },
|
||||
{ value: 'MiniMax-Text-01', label: 'MiniMax Text 01', description: 'Text-focused - 512K context - FREE' },
|
||||
{ value: 'MiniMax-Text-01-Preview', label: 'MiniMax Text 01 Preview', description: 'Preview - 256K context - FREE' },
|
||||
{ value: 'MiniMax-Vision-01', label: 'MiniMax Vision 01', description: 'Vision model - 32K context' },
|
||||
{ value: 'MiniMax-Vision-01-Fast', label: 'MiniMax Vision 01 Fast', description: 'Fast vision - 16K context - FREE' },
|
||||
// Legacy free tier models
|
||||
{ value: 'abab6.5s-chat', label: 'ABAB 6.5S Chat', description: 'Legacy free - 16K context' },
|
||||
{ value: 'abab6.5-chat', label: 'ABAB 6.5 Chat', description: 'Legacy free - 32K context' },
|
||||
{ value: 'abab6.5g-chat', label: 'ABAB 6.5G Chat', description: 'Generation 6.5 - 32K context' },
|
||||
{ value: 'abab6-chat', label: 'ABAB 6 Chat', description: 'Legacy - 8K context' },
|
||||
]
|
||||
}
|
||||
// These legacy IDs were accepted before the descriptor-backed picker, but
|
||||
// have no shared descriptor. Retirement from suggestions must not revoke them.
|
||||
const legacyMiniMaxModelIds = new Set([
|
||||
'abab6.5s-chat',
|
||||
'abab6.5-chat',
|
||||
'abab6.5g-chat',
|
||||
'abab6-chat',
|
||||
])
|
||||
|
||||
let cachedMiniMaxOptions: ModelOption[] | null = null
|
||||
export function isKnownMiniMaxModel(model: string): boolean {
|
||||
return legacyMiniMaxModelIds.has(model) ||
|
||||
getCatalogEntriesForRoute('minimax').some(entry => entry.apiName === model)
|
||||
}
|
||||
|
||||
export function getCachedMiniMaxModelOptions(): ModelOption[] {
|
||||
if (!cachedMiniMaxOptions) {
|
||||
cachedMiniMaxOptions = getMiniMaxModels()
|
||||
}
|
||||
return cachedMiniMaxOptions
|
||||
return buildRouteCatalogModelOptions(
|
||||
'MiniMax',
|
||||
getCatalogEntriesForRoute('minimax'),
|
||||
getRouteDefaultModel('minimax'),
|
||||
'minimax',
|
||||
)
|
||||
}
|
||||
|
||||
@@ -342,15 +342,23 @@ test('getDefaultOpusModel returns OPENAI_MODEL for MiniMax', async () => {
|
||||
expect(getDefaultOpusModel()).toBe('MiniMax-M2.7')
|
||||
})
|
||||
|
||||
test('getDefaultMainLoopModelSetting defaults MiniMax to M3', async () => {
|
||||
test('MiniMax default tiers share the Flash route default', async () => {
|
||||
process.env.MINIMAX_API_KEY = 'minimax-test'
|
||||
|
||||
const {
|
||||
getDefaultMainLoopModel,
|
||||
getDefaultMainLoopModelSetting,
|
||||
getDefaultOpusModel,
|
||||
getDefaultSonnetModel,
|
||||
getDefaultHaikuModel,
|
||||
getSmallFastModel,
|
||||
} = await importFreshModelModule()
|
||||
expect(getDefaultMainLoopModelSetting()).toBe('MiniMax-M3')
|
||||
expect(getDefaultMainLoopModel()).toBe('MiniMax-M3')
|
||||
expect(getDefaultSonnetModel()).toBe('MiniMax-M3.1-Flash')
|
||||
expect(getDefaultHaikuModel()).toBe('MiniMax-M3.1-Flash')
|
||||
expect(getSmallFastModel()).toBe('MiniMax-M3.1-Flash')
|
||||
expect(getDefaultMainLoopModelSetting()).toBe('MiniMax-M3.1-Flash')
|
||||
expect(getDefaultMainLoopModel()).toBe('MiniMax-M3.1-Flash')
|
||||
expect(getDefaultOpusModel()).toBe('MiniMax-M3.1-Flash')
|
||||
})
|
||||
|
||||
test('Concentrate selects its dedicated model before client normalization', async () => {
|
||||
|
||||
@@ -112,10 +112,9 @@ export function getSmallFastModel(): ModelName {
|
||||
if (getAPIProvider() === 'nvidia-nim') {
|
||||
return process.env.OPENAI_MODEL || 'meta/llama-3.1-8b-instruct'
|
||||
}
|
||||
// MiniMax — OPENAI_MODEL carries the active MiniMax model; fall back to
|
||||
// the fastest tier (M2.5-highspeed) when missing.
|
||||
// MiniMax uses the configured model or the shared Flash route default.
|
||||
if (getAPIProvider() === 'minimax') {
|
||||
return getMiniMaxModelEnv() || 'MiniMax-M2.5-highspeed'
|
||||
return getMiniMaxModelEnv() || getRouteDefaultModel('minimax')!
|
||||
}
|
||||
// Xiaomi MiMo — OPENAI_MODEL carries the active MiMo model; fall back to
|
||||
// the fast tier when missing.
|
||||
@@ -260,7 +259,7 @@ export function getDefaultOpusModel(): ModelName {
|
||||
}
|
||||
// MiniMax — flagship tier for "opus"-equivalent.
|
||||
if (getAPIProvider() === 'minimax') {
|
||||
return getMiniMaxModelEnv() || 'MiniMax-M2.7'
|
||||
return getMiniMaxModelEnv() || getRouteDefaultModel('minimax')!
|
||||
}
|
||||
// Xiaomi MiMo — flagship tier for "opus"-equivalent.
|
||||
if (getAPIProvider() === 'xiaomi-mimo') {
|
||||
@@ -310,7 +309,7 @@ export function getDefaultSonnetModel(): ModelName {
|
||||
}
|
||||
// MiniMax — mid tier for "sonnet"-equivalent.
|
||||
if (getAPIProvider() === 'minimax') {
|
||||
return getMiniMaxModelEnv() || 'MiniMax-M2.5'
|
||||
return getMiniMaxModelEnv() || getRouteDefaultModel('minimax')!
|
||||
}
|
||||
// Xiaomi MiMo — flagship model for "sonnet"-equivalent.
|
||||
if (getAPIProvider() === 'xiaomi-mimo') {
|
||||
@@ -358,7 +357,7 @@ export function getDefaultHaikuModel(): ModelName {
|
||||
}
|
||||
// MiniMax — fastest tier for "haiku"-equivalent.
|
||||
if (getAPIProvider() === 'minimax') {
|
||||
return getMiniMaxModelEnv() || 'MiniMax-M2.5-highspeed'
|
||||
return getMiniMaxModelEnv() || getRouteDefaultModel('minimax')!
|
||||
}
|
||||
// Xiaomi MiMo — fast tier for "haiku"-equivalent.
|
||||
if (getAPIProvider() === 'xiaomi-mimo') {
|
||||
@@ -470,11 +469,9 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
||||
return process.env.OPENAI_MODEL || getRouteDefaultModel('xai') || 'grok-4.6'
|
||||
}
|
||||
// MiniMax provider: always use the configured MiniMax model.
|
||||
// Keep the env-only fallback aligned with the MiniMax descriptor default
|
||||
// (MiniMax-M3) so a session with only MINIMAX_API_KEY / a MiniMax base URL
|
||||
// defaults to the same model as --provider minimax and saved profiles.
|
||||
// Share the route default across env-only sessions, --provider, and profiles.
|
||||
if (getAPIProvider() === 'minimax') {
|
||||
return getMiniMaxModelEnv() || 'MiniMax-M3'
|
||||
return getMiniMaxModelEnv() || getRouteDefaultModel('minimax')!
|
||||
}
|
||||
// Xiaomi MiMo provider: always use the configured MiMo model
|
||||
if (getAPIProvider() === 'xiaomi-mimo') {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterAll, afterEach, beforeEach, expect, test } from 'bun:test'
|
||||
import { mock } from 'bun:test'
|
||||
import { snapshotEnv } from '../../test/envSnapshot.js'
|
||||
|
||||
import { resetModelStringsForTestingOnly } from '../../bootstrap/state.js'
|
||||
import {
|
||||
@@ -498,7 +499,7 @@ test('getModelOptionsBase: local OpenAI-compatible scope still appends inactive
|
||||
}
|
||||
})
|
||||
|
||||
test('getModelOptionsBase: active Ollama profile still surfaces inactive profile options', async () => {
|
||||
test.each(['llama3.2', 'minimax-m3.1-flash:cloud', 'minimax-m3:cloud', 'minimax-m2.7:cloud'])('getModelOptionsBase: active Ollama profile offers discovered %s and inactive profiles', async discoveredModel => {
|
||||
// Regression for #1164: the `isOllamaProvider()` early return ran before the
|
||||
// inactive-profile compute, so a user whose active profile is a local Ollama
|
||||
// endpoint only saw the Ollama models and lost the cross-profile switcher,
|
||||
@@ -508,7 +509,7 @@ test('getModelOptionsBase: active Ollama profile still surfaces inactive profile
|
||||
id: 'profile_ollama',
|
||||
name: 'Local Ollama',
|
||||
baseUrl: 'http://localhost:11434/v1',
|
||||
model: 'llama3.2',
|
||||
model: 'qwen3:latest',
|
||||
})
|
||||
const inactive = buildProviderProfileFixture({
|
||||
id: 'profile_remote',
|
||||
@@ -523,7 +524,7 @@ test('getModelOptionsBase: active Ollama profile still surfaces inactive profile
|
||||
// `[default, ...ollamaModels, ...inactiveProfileOptions]` return path.
|
||||
activeOllamaOverride = {
|
||||
cachedModels: [
|
||||
{ value: 'llama3.2', label: 'llama3.2', description: 'Local Ollama' },
|
||||
{ value: discoveredModel, label: discoveredModel, description: 'Local Ollama' },
|
||||
],
|
||||
}
|
||||
try {
|
||||
@@ -538,7 +539,7 @@ test('getModelOptionsBase: active Ollama profile still surfaces inactive profile
|
||||
|
||||
const options = getModelOptions(false)
|
||||
// The Ollama model is still present...
|
||||
expect(options.some(o => o.value === 'llama3.2')).toBe(true)
|
||||
expect(options.some(o => o.value === discoveredModel)).toBe(true)
|
||||
// ...and the inactive profile now appears as a switch option.
|
||||
const switchOptions = options.filter(o => o.switchToProfileId !== undefined)
|
||||
expect(switchOptions.length).toBeGreaterThan(0)
|
||||
@@ -554,6 +555,75 @@ test('getModelOptionsBase: active Ollama profile still surfaces inactive profile
|
||||
}
|
||||
})
|
||||
|
||||
test.each([
|
||||
['ollama', 'http://localhost:11434/v1'],
|
||||
['lmstudio', 'http://localhost:1234/v1'],
|
||||
['atomic-chat', 'http://127.0.0.1:1337/v1'],
|
||||
['custom', 'http://localhost:8080/v1'],
|
||||
])('getModelOptions appends scoped %s discoveries when the prefetch cache is empty', async (provider, baseUrl) => {
|
||||
const restoreEnv = snapshotEnv(['CLAUDE_CODE_USE_OPENAI', 'OPENAI_BASE_URL', 'OPENAI_API_BASE', 'OPENAI_MODEL', 'OLLAMA_BASE_URL'])
|
||||
const { getGlobalConfig, saveGlobalConfig } = await import('../config.js')
|
||||
const previousOptions = getGlobalConfig().additionalModelOptionsCache
|
||||
const previousScope = getGlobalConfig().additionalModelOptionsCacheScope
|
||||
const models = ['minimax-m3.1-flash:cloud', 'minimax-m3:cloud', 'minimax-m2.7:cloud', 'minimaxai/minimax-m2.7', 'minimax-m3.1-flash-q4_k_m']
|
||||
if (provider === 'ollama') activeOllamaOverride = { cachedModels: [] }
|
||||
activeCacheScopeOverride = `openai:${baseUrl}:fixture`
|
||||
try {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = baseUrl
|
||||
process.env.OPENAI_MODEL = 'qwen3:latest'
|
||||
delete process.env.OPENAI_API_BASE
|
||||
delete process.env.OLLAMA_BASE_URL
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
additionalModelOptionsCache: models.map(value => ({ value, label: value, description: `Discovered from ${provider}` })),
|
||||
additionalModelOptionsCacheScope: activeCacheScopeOverride!,
|
||||
}))
|
||||
const { getModelOptions } = await importFreshModelOptionsModule({
|
||||
getProviderProfiles: () => [],
|
||||
getActiveProviderProfile: () => undefined,
|
||||
})
|
||||
const values = getModelOptions(false).map(option => option.value)
|
||||
for (const model of models) expect(values).toContain(model)
|
||||
} finally {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
additionalModelOptionsCache: previousOptions,
|
||||
additionalModelOptionsCacheScope: previousScope,
|
||||
}))
|
||||
restoreEnv()
|
||||
}
|
||||
})
|
||||
|
||||
test.each(['firstParty', 'openai:http://localhost:11434/v1:fixture'])('getModelOptions ignores malformed bootstrap values in %s scope', async scope => {
|
||||
const { getGlobalConfig, saveGlobalConfig } = await import('../config.js')
|
||||
const previousOptions = getGlobalConfig().additionalModelOptionsCache
|
||||
const previousScope = getGlobalConfig().additionalModelOptionsCacheScope
|
||||
activeCacheScopeOverride = scope
|
||||
activeApiProviderOverride = scope === 'firstParty' ? 'firstParty' : 'openai'
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
additionalModelOptionsCacheScope: scope,
|
||||
additionalModelOptionsCache: JSON.parse('[{},{"value":42},{"value":false},{"value":{}},{"value":[]},{"value":null},{"value":"valid-cache-control","label":"control","description":"control"}]'),
|
||||
}))
|
||||
try {
|
||||
const { getModelOptions } = await importFreshModelOptionsModule({
|
||||
getProviderProfiles: () => [],
|
||||
getActiveProviderProfile: () => undefined,
|
||||
})
|
||||
const values = getModelOptions(false).map(option => option.value)
|
||||
expect(values).toContain(null)
|
||||
expect(values).toContain('valid-cache-control')
|
||||
expect(values.every(value => value === null || typeof value === 'string')).toBe(true)
|
||||
} finally {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
additionalModelOptionsCache: previousOptions,
|
||||
additionalModelOptionsCacheScope: previousScope,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
test('getModelOptionsBase: 3P path omits inactive profile options when env NOT applied', async () => {
|
||||
// If the user hasn't gone through `/provider` yet (profile env not applied),
|
||||
// surfacing cross-profile switching would be confusing — they haven't opted
|
||||
@@ -742,8 +812,11 @@ test('getModelOptionsBase: Claude subscriber branch appends inactive profile opt
|
||||
|
||||
test('getModelOptionsBase: MiniMax catalog branch appends inactive profile options', async () => {
|
||||
const { active, inactive } = activeAndInactivePair()
|
||||
active.provider = 'minimax'
|
||||
active.baseUrl = 'https://api.minimax.io/anthropic'
|
||||
activeMiniMaxOverride = {
|
||||
cachedModels: [
|
||||
{ value: 'MiniMax-M3.1-Flash', label: 'MiniMax-M3.1-Flash', description: 'MiniMax' },
|
||||
{ value: 'MiniMax-M2.7', label: 'MiniMax-M2.7', description: 'MiniMax' },
|
||||
],
|
||||
}
|
||||
@@ -756,7 +829,8 @@ test('getModelOptionsBase: MiniMax catalog branch appends inactive profile optio
|
||||
],
|
||||
})
|
||||
const options = getModelOptions(false)
|
||||
expect(options.some(o => o.value === 'MiniMax-M2.7')).toBe(true)
|
||||
expect(options.some(o => o.value === 'MiniMax-M3.1-Flash')).toBe(true)
|
||||
expect(options.some(o => o.value === 'MiniMax-M2.7')).toBe(false)
|
||||
expect(
|
||||
options
|
||||
.filter(o => o.switchToProfileId !== undefined)
|
||||
@@ -765,6 +839,59 @@ test('getModelOptionsBase: MiniMax catalog branch appends inactive profile optio
|
||||
})
|
||||
})
|
||||
|
||||
test.each(['MiniMax-M2.7', 'minimax-m3-free', 'minimax-m3.1-flash-free'])('MiniMax picker keeps saved %s without restoring other retired suggestions', async savedModel => {
|
||||
const { active, inactive } = activeAndInactivePair()
|
||||
active.provider = 'minimax'
|
||||
active.baseUrl = 'https://api.minimax.io/anthropic'
|
||||
active.model = savedModel
|
||||
activeMiniMaxOverride = {
|
||||
cachedModels: ['MiniMax-M3.1-Flash', 'MiniMax-M3', 'MiniMax-M2.5', 'minimax-m3-free', 'minimax-m3.1-flash-free'].map(value => ({ value, label: value, description: 'MiniMax' })),
|
||||
}
|
||||
await withProfileEnvApplied(async () => {
|
||||
const { getModelOptions } = await importFreshModelOptionsModule({
|
||||
getProviderProfiles: () => [active, inactive],
|
||||
getActiveProviderProfile: () => active,
|
||||
getProfileModelOptions: profile => [
|
||||
{ value: profile.model, label: profile.model, description: profile.name },
|
||||
],
|
||||
})
|
||||
const values = getModelOptions(false).map(option => option.value)
|
||||
expect(values).toContain('MiniMax-M3.1-Flash')
|
||||
expect(values).toContain('MiniMax-M3')
|
||||
expect(values).toContain(savedModel)
|
||||
expect(values).not.toContain('MiniMax-M2.5')
|
||||
for (const retired of ['minimax-m3-free', 'minimax-m3.1-flash-free']) {
|
||||
if (retired !== savedModel) expect(values).not.toContain(retired)
|
||||
}
|
||||
expect(active.model).toBe(savedModel)
|
||||
})
|
||||
})
|
||||
|
||||
test.each([
|
||||
['https://opencode.ai/zen/v1', 'gpt-5.4'],
|
||||
['https://opencode.ai/zen/go/v1', 'glm-5.1'],
|
||||
])('getModelOptions retains available MiniMax gateway choices on %s', async (baseUrl, model) => {
|
||||
const restoreEnv = snapshotEnv([
|
||||
'CLAUDE_CODE_USE_OPENAI', 'OPENAI_BASE_URL', 'OPENAI_API_BASE', 'OPENAI_MODEL',
|
||||
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED', 'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID',
|
||||
], { clear: true })
|
||||
try {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = baseUrl
|
||||
process.env.OPENAI_MODEL = model
|
||||
const { getModelOptions } = await importFreshModelOptionsModule({
|
||||
getProviderProfiles: () => [],
|
||||
getActiveProviderProfile: () => undefined,
|
||||
})
|
||||
const values = getModelOptions(false).map(option => option.value)
|
||||
expect(values).toContain('minimax-m2.7')
|
||||
expect(values).not.toContain('minimax-m3-free')
|
||||
expect(values).not.toContain('minimax-m3.1-flash-free')
|
||||
} finally {
|
||||
restoreEnv()
|
||||
}
|
||||
})
|
||||
|
||||
test('getModelOptionsBase: Xiaomi MiMo catalog branch appends inactive profile options', async () => {
|
||||
const { active, inactive } = activeAndInactivePair()
|
||||
activeXiaomiOverride = {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../../integrations/routeMetadata.js'
|
||||
import {
|
||||
getAdditionalModelOptionsCacheScope,
|
||||
isLocalProviderUrl,
|
||||
resolveProviderRequest,
|
||||
} from '../../services/api/providerConfig.js'
|
||||
import {
|
||||
@@ -45,14 +46,17 @@ import { getGlobalConfig } from '../config.js'
|
||||
import {
|
||||
getActiveOpenAIModelOptionsCache,
|
||||
getActiveProviderProfile,
|
||||
getConfiguredProfileModelOptions,
|
||||
getProfileModelOptions,
|
||||
getProviderProfiles,
|
||||
resolveProfileCapabilityRouteId,
|
||||
} from '../providerProfiles.js'
|
||||
import { getCachedOllamaModelOptions, isOllamaProvider } from './ollamaModels.js'
|
||||
import { getCachedNvidiaNimModelOptions, isNvidiaNimProvider } from './nvidiaNimModels.js'
|
||||
import { getCachedMiniMaxModelOptions, isMiniMaxProvider } from './minimaxModels.js'
|
||||
import { getCachedXiaomiMimoModelOptions, isXiaomiMimoProvider } from './xiaomi-mimoModels.js'
|
||||
import { getAntModels } from './antModels.js'
|
||||
import { isCurrentMiniMaxModelOption } from './minimaxCatalog.js'
|
||||
|
||||
// @[MODEL LAUNCH]: Update all the available and default model option strings below.
|
||||
|
||||
@@ -1071,7 +1075,42 @@ export function getModelOptions(fastMode = false): ModelOption[] {
|
||||
return filterModelOptionsByAllowlist(getModelOptionsBase(fastMode))
|
||||
}
|
||||
|
||||
const options = getModelOptionsBase(fastMode)
|
||||
const activeProfile = process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED === '1'
|
||||
? getActiveProviderProfile()
|
||||
: undefined
|
||||
const configuredOptions = activeProfile
|
||||
? getConfiguredProfileModelOptions(activeProfile)
|
||||
: []
|
||||
// Most models need no retirement decision. Resolve the route only for a
|
||||
// filtered MiniMax name, and share that metadata lookup across this build.
|
||||
let suggestionRouteId: string | null | undefined
|
||||
let suggestionUsesLocalEndpoint = false
|
||||
const isSuggestedModel = (model: ModelSetting): boolean => {
|
||||
if (isCurrentMiniMaxModelOption(model)) return true
|
||||
if (suggestionRouteId === undefined) {
|
||||
const provider = getAPIProvider()
|
||||
suggestionRouteId = provider === 'openai' && isOllamaProvider()
|
||||
? 'ollama'
|
||||
: activeProfile
|
||||
? resolveProfileCapabilityRouteId(activeProfile.provider, activeProfile.baseUrl)
|
||||
: getActiveOpenAIRouteId()
|
||||
suggestionUsesLocalEndpoint = provider === 'openai' &&
|
||||
isLocalProviderUrl(resolveProviderRequest().baseUrl)
|
||||
}
|
||||
return isCurrentMiniMaxModelOption(model, suggestionRouteId, suggestionUsesLocalEndpoint)
|
||||
}
|
||||
const options = getModelOptionsBase(fastMode).filter(option =>
|
||||
option.switchToProfileId ||
|
||||
isSuggestedModel(option.value) ||
|
||||
configuredOptions.some(configured => configured.value === option.value),
|
||||
)
|
||||
|
||||
for (const configured of configuredOptions) {
|
||||
if (!isSuggestedModel(configured.value) &&
|
||||
!options.some(option => option.value === configured.value)) {
|
||||
options.push(configured)
|
||||
}
|
||||
}
|
||||
|
||||
// Route-catalog lookup state is built once per options build (on first use)
|
||||
// and shared by every catalog check below, so repeated lookups — the env
|
||||
@@ -1104,6 +1143,7 @@ export function getModelOptions(fastMode = false): ModelOption[] {
|
||||
|
||||
// Append additional model options fetched during bootstrap
|
||||
for (const opt of getScopedAdditionalModelOptions()) {
|
||||
if (!isSuggestedModel(opt.value)) continue
|
||||
const catalogOption = findRouteCatalogOption(
|
||||
getSharedRouteCatalogContext(),
|
||||
opt.value,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ModelCatalogEntry } from '../../integrations/descriptors.js'
|
||||
import type { ModelOption } from './modelOptions.js'
|
||||
import { isCurrentMiniMaxModelOption } from './minimaxCatalog.js'
|
||||
|
||||
function toDescription(
|
||||
entry: ModelCatalogEntry,
|
||||
@@ -45,7 +46,11 @@ export function buildRouteCatalogModelOptions(
|
||||
routeLabel: string,
|
||||
entries: ModelCatalogEntry[],
|
||||
routeDefaultModel?: string,
|
||||
routeId?: string,
|
||||
): ModelOption[] {
|
||||
// Preserve raw catalog metadata for explicit legacy IDs, but do not offer
|
||||
// retired MiniMax text models as ordinary suggestions (including discovery).
|
||||
entries = entries.filter(entry => isCurrentMiniMaxModelOption(entry.apiName, routeId))
|
||||
const seen = new Set<string>()
|
||||
const options: ModelOption[] = []
|
||||
const apiNameCounts = new Map<string, number>()
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
getDiscoveredNvidiaNimModelIds,
|
||||
isNvidiaNimProvider,
|
||||
} from './nvidiaNimModels.js'
|
||||
import { getCachedMiniMaxModelOptions, isMiniMaxProvider } from './minimaxModels.js'
|
||||
import { getCachedMiniMaxModelOptions, isKnownMiniMaxModel, isMiniMaxProvider } from './minimaxModels.js'
|
||||
|
||||
// Cache valid models to avoid repeated API calls
|
||||
const validModelCache = new Map<string, boolean>()
|
||||
@@ -86,10 +86,10 @@ export async function validateModel(
|
||||
}
|
||||
}
|
||||
|
||||
// For MiniMax provider, validate against cached model list
|
||||
// Known explicit IDs are independent of the filtered picker suggestions.
|
||||
if (isMiniMaxProvider()) {
|
||||
const minimaxModels = getCachedMiniMaxModelOptions()
|
||||
const found = minimaxModels.some(m => m.value === normalizedModel)
|
||||
const found = isKnownMiniMaxModel(normalizedModel)
|
||||
if (found) {
|
||||
validModelCache.set(normalizedModel, true)
|
||||
return { valid: true }
|
||||
|
||||
@@ -1006,7 +1006,7 @@ describe('applyProviderFlag - minimax', () => {
|
||||
expect(result.error).toBeUndefined()
|
||||
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-M3')
|
||||
expect(process.env.ANTHROPIC_MODEL).toBe('MiniMax-M3.1-Flash')
|
||||
expect(process.env.OPENAI_BASE_URL).toBeUndefined()
|
||||
expect(process.env.OPENAI_MODEL).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -610,7 +610,7 @@ export function applyProviderFlag(
|
||||
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-M3'
|
||||
process.env.ANTHROPIC_MODEL = defaultModel ?? 'MiniMax-M3.1-Flash'
|
||||
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
|
||||
|
||||
@@ -3448,6 +3448,30 @@ describe('applyActiveProviderProfileFromConfig', () => {
|
||||
expect(process.env.OPENAI_MODEL).toBe('gpt-4o')
|
||||
})
|
||||
|
||||
test.each(['minimax-m3-free', 'minimax-m3.1-flash-free'])('applies saved retired Zen model %s without rewriting the configured model', async savedModel => {
|
||||
const { _setSavedModelOverrideForTesting, applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderProfileModules()
|
||||
const profile = buildProfile({
|
||||
id: 'saved_zen',
|
||||
provider: 'opencode',
|
||||
baseUrl: 'https://opencode.ai/zen/v1',
|
||||
model: 'gpt-5.4',
|
||||
})
|
||||
_setSavedModelOverrideForTesting(savedModel)
|
||||
try {
|
||||
const applied = applyActiveProviderProfileFromConfig({
|
||||
...createMockConfigState(),
|
||||
providerProfiles: [profile],
|
||||
activeProviderProfileId: profile.id,
|
||||
})
|
||||
expect(applied?.id).toBe(profile.id)
|
||||
expect(process.env.OPENAI_MODEL).toBe(savedModel)
|
||||
expect(profile.model).toBe('gpt-5.4')
|
||||
} finally {
|
||||
_setSavedModelOverrideForTesting(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
test.each(['', '?reasoning=high', '?thinking=disabled&reasoning=high'])(
|
||||
'preserves context limits for configured and saved models across profile rehydration (%j)',
|
||||
async query => {
|
||||
@@ -3941,7 +3965,7 @@ describe('getProviderPresetDefaults', () => {
|
||||
expect(defaults.requiresApiKey).toBe(true)
|
||||
})
|
||||
|
||||
test('minimax preset defaults to MiniMax M3', async () => {
|
||||
test('minimax preset defaults to MiniMax M3.1 Flash', async () => {
|
||||
const { getProviderPresetDefaults } = await importFreshProviderProfileModules()
|
||||
|
||||
const defaults = getProviderPresetDefaults('minimax')
|
||||
@@ -3949,7 +3973,7 @@ describe('getProviderPresetDefaults', () => {
|
||||
expect(defaults.provider).toBe('minimax')
|
||||
expect(defaults.name).toBe('MiniMax')
|
||||
expect(defaults.baseUrl).toBe('https://api.minimax.io/anthropic')
|
||||
expect(defaults.model).toBe('MiniMax-M3')
|
||||
expect(defaults.model).toBe('MiniMax-M3.1-Flash')
|
||||
expect(defaults.requiresApiKey).toBe(true)
|
||||
})
|
||||
|
||||
@@ -6124,6 +6148,31 @@ describe('getProfileModelOptions', () => {
|
||||
])
|
||||
})
|
||||
|
||||
test.each([
|
||||
['minimax', 'https://api.minimax.io/anthropic', 'MiniMax-M2.7'],
|
||||
['opencode', 'https://opencode.ai/zen/v1', 'minimax-m3-free'],
|
||||
['opencode', 'https://opencode.ai/zen/v1', 'minimax-m3.1-flash-free'],
|
||||
])('%s at %s keeps saved %s while filtering its retired suggestions', async (provider, baseUrl, savedModel) => {
|
||||
const { getProfileModelOptions } = await importFreshProviderProfileModules()
|
||||
mockConfigState = {
|
||||
...createMockConfigState(),
|
||||
openaiAdditionalModelOptionsCacheByProfile: {
|
||||
provider_test: ['MiniMax-M2.5', 'MiniMax-M2.7', 'minimax-m3-free', 'minimax-m3.1-flash-free', 'MiniMax-M3.1-Flash', 'MiniMax-M3', 'MiniMax-Vision-01', 'gpt-4o'].map(value => ({ value, label: value, description: 'Discovered from API' })),
|
||||
},
|
||||
}
|
||||
const profile = buildProfile({
|
||||
id: 'provider_test', provider, baseUrl,
|
||||
model: `${savedModel}, custom-minimax`,
|
||||
})
|
||||
expect(getProfileModelOptions(profile).map(option => option.value)).toEqual([
|
||||
savedModel, 'custom-minimax',
|
||||
...(provider === 'opencode' ? ['MiniMax-M2.5', 'MiniMax-M2.7'] : []),
|
||||
'MiniMax-M3.1-Flash', 'MiniMax-M3', 'MiniMax-Vision-01', 'gpt-4o',
|
||||
])
|
||||
expect(profile.model).toBe(`${savedModel}, custom-minimax`)
|
||||
expect(mockConfigState.openaiAdditionalModelOptionsCacheByProfile?.provider_test?.[0]).toMatchObject({ value: 'MiniMax-M2.5' })
|
||||
})
|
||||
|
||||
test('returns empty array for empty model field', async () => {
|
||||
const { getProfileModelOptions } =
|
||||
await importFreshProviderProfileModules()
|
||||
@@ -6140,6 +6189,50 @@ describe('getProfileModelOptions', () => {
|
||||
})
|
||||
|
||||
describe('setActiveProviderProfile model cache', () => {
|
||||
test.each([
|
||||
['ollama', 'http://localhost:11434/v1'],
|
||||
['ollama', 'https://ollama-proxy.example/v1'],
|
||||
['lmstudio', 'http://localhost:1234/v1'],
|
||||
['lmstudio', 'https://lmstudio-proxy.example/v1'],
|
||||
['atomic-chat', 'http://127.0.0.1:1337/v1'],
|
||||
['custom', 'http://localhost:8080/v1'],
|
||||
['opencode', 'https://opencode.ai/zen/v1'],
|
||||
['opencode-go', 'https://opencode.ai/zen/go/v1'],
|
||||
['openrouter', 'https://openrouter.ai/api/v1'],
|
||||
['custom', 'https://models.example/v1'],
|
||||
] as const)('activation preserves advertised models for %s at %s', async (provider, baseUrl) => {
|
||||
const { setActiveProviderProfile, getActiveOpenAIModelOptionsCache } = await importFreshProviderProfileModules()
|
||||
const models = ['minimax-m3.1-flash:cloud', 'minimax-m3:cloud', 'minimax-m2.7:cloud', 'minimaxai/minimax-m2.7', 'minimax-m3.1-flash-q4_k_m', 'minimax-m2.5', 'minimax-m2.7']
|
||||
const profile = buildProfile({ id: 'local_models', provider, baseUrl, model: 'qwen3:latest' })
|
||||
mockConfigState = {
|
||||
...createMockConfigState(),
|
||||
providerProfiles: [profile],
|
||||
openaiAdditionalModelOptionsCacheByProfile: {
|
||||
[profile.id]: models.map(value => ({ value, label: value, description: 'Discovered from Ollama' })),
|
||||
},
|
||||
}
|
||||
expect(setActiveProviderProfile(profile.id, { configDir: testConfigDir ?? undefined })?.id).toBe(profile.id)
|
||||
expect(getActiveOpenAIModelOptionsCache().map(option => option.value)).toEqual(['qwen3:latest', ...models])
|
||||
expect(mockConfigState.openaiAdditionalModelOptionsCacheByProfile[profile.id]?.map(option => (option as { value: string }).value)).toEqual(['qwen3:latest', ...models])
|
||||
expect(profile.model).toBe('qwen3:latest')
|
||||
})
|
||||
|
||||
test('activation ignores malformed persisted option values before filtering', async () => {
|
||||
const { setActiveProviderProfile, getActiveOpenAIModelOptionsCache } = await importFreshProviderProfileModules()
|
||||
const profile = buildProfile({ id: 'malformed_options', model: 'gpt-4o' })
|
||||
mockConfigState = {
|
||||
...createMockConfigState(),
|
||||
providerProfiles: [profile],
|
||||
// Persisted JSON bypasses the ModelOption type; retain the existing
|
||||
// non-string guard, including records whose value is absent.
|
||||
openaiAdditionalModelOptionsCacheByProfile: {
|
||||
[profile.id]: JSON.parse('[{},{"value":42},{"value":true},{"value":{}},{"value":[]},{"value":null},{"value":" "},{"value":"gpt-4.1","label":"gpt-4.1","description":"Discovered"}]'),
|
||||
},
|
||||
}
|
||||
expect(setActiveProviderProfile(profile.id, { configDir: testConfigDir ?? undefined })?.id).toBe(profile.id)
|
||||
expect(getActiveOpenAIModelOptionsCache().map(option => option.value)).toEqual(['gpt-4o', 'gpt-4.1'])
|
||||
})
|
||||
|
||||
test('populates model cache with all models from multi-model profile on activation', async () => {
|
||||
const {
|
||||
setActiveProviderProfile,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
import {
|
||||
getAdditionalModelOptionsCacheScope,
|
||||
isLocalProviderUrl,
|
||||
isCodexAlias,
|
||||
isCodexBaseUrl,
|
||||
isCodexEligibleGpt5Model,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
type ProviderProfile,
|
||||
} from './config.js'
|
||||
import type { ModelOption } from './model/modelOptions.js'
|
||||
import { isCurrentMiniMaxModelOption } from './model/minimaxCatalog.js'
|
||||
import { getPrimaryModel, parseModelList } from './providerModels.js'
|
||||
import {
|
||||
buildCompatibilityProcessEnv,
|
||||
@@ -1786,9 +1788,14 @@ export function getProfileModelOptions(
|
||||
config = getGlobalConfig(),
|
||||
): ModelOption[] {
|
||||
const configuredOptions = getConfiguredProfileModelOptions(profile)
|
||||
const routeId = resolveProfileCapabilityRouteId(profile.provider, profile.baseUrl)
|
||||
const localEndpoint = isLocalProviderUrl(profile.baseUrl)
|
||||
return mergeModelOptionsByValue(
|
||||
configuredOptions,
|
||||
getModelCacheByProfile(profile.id, config),
|
||||
getModelCacheByProfile(profile.id, config).filter(option =>
|
||||
typeof option.value === 'string' &&
|
||||
isCurrentMiniMaxModelOption(option.value, routeId, localEndpoint),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ export const providers: Provider[] = [
|
||||
group: 'gateways',
|
||||
setup: '/provider or env vars',
|
||||
envVars: ['OPENCODE_API_KEY'],
|
||||
notes: 'Pay-as-you-go gateway (Zen, 48 models) and a $10/mo subscription for open models (Go); one key for both.',
|
||||
notes: 'Pay-as-you-go gateway (Zen) and a $10/mo subscription for open models (Go); one key for both.',
|
||||
},
|
||||
{
|
||||
id: 'clinepass',
|
||||
@@ -292,7 +292,7 @@ export const providers: Provider[] = [
|
||||
group: 'vendors',
|
||||
setup: '/provider or env vars',
|
||||
envVars: ['MINIMAX_API_KEY'],
|
||||
notes: 'Anthropic-compatible MiniMax endpoint; defaults to MiniMax-M3.',
|
||||
notes: 'Anthropic-compatible MiniMax endpoint; defaults to MiniMax-M3.1-Flash.',
|
||||
},
|
||||
{
|
||||
id: 'longcat',
|
||||
|
||||
Reference in New Issue
Block a user