mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat: refresh MiniMax model and endpoint configuration (#2780)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
This commit is contained in:
parent
5f9648744c
commit
0fa547ccdc
20 changed files with 902 additions and 74 deletions
|
|
@ -60,7 +60,7 @@ Generates repository documentation from the knowledge graph using an LLM. Requir
|
|||
| Flag | Effect |
|
||||
| ------------------- | ----------------------------------------- |
|
||||
| `--force` | Force full regeneration |
|
||||
| `--model <model>` | LLM model (default: minimax/minimax-m2.5) |
|
||||
| `--model <model>` | LLM model (default: MiniMax-M3) |
|
||||
| `--base-url <url>` | LLM API base URL |
|
||||
| `--api-key <key>` | LLM API key |
|
||||
| `--concurrency <n>` | Parallel LLM calls (default: 3) |
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ Generates repository documentation from the knowledge graph using an LLM. Requir
|
|||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `--force` | Force full regeneration, also required to re-gerenate an existing wiki in a different language |
|
||||
| `--model <model>` | LLM model (default: minimax/minimax-m2.5) |
|
||||
| `--model <model>` | LLM model (default: MiniMax-M3) |
|
||||
| `--base-url <url>` | LLM API base URL |
|
||||
| `--api-key <key>` | LLM API key |
|
||||
| `--concurrency <n>` | Parallel LLM calls (default: 3) |
|
||||
|
|
|
|||
|
|
@ -21,7 +21,13 @@ import {
|
|||
fetchOpenRouterModels,
|
||||
} from '../core/llm/settings-service';
|
||||
import { getAuthToken, setAuthToken } from '../services/backend-client';
|
||||
import type { LLMSettings, LLMProvider } from '../core/llm/types';
|
||||
import type { LLMSettings, LLMProvider, MiniMaxThinkingMode } from '../core/llm/types';
|
||||
import {
|
||||
getMiniMaxModelCapabilities,
|
||||
MINIMAX_ANTHROPIC_BASE_URLS,
|
||||
MINIMAX_DOCS_ROOTS,
|
||||
MINIMAX_MODEL_IDS,
|
||||
} from '../core/llm/types';
|
||||
import { DEFAULT_OLLAMA_BASE_URL } from '../config/ui-constants';
|
||||
import { ProviderConfigCard } from './settings/ProviderConfigCard';
|
||||
import { SecretInput } from './settings/SecretInput';
|
||||
|
|
@ -341,6 +347,20 @@ export const SettingsPanel = ({
|
|||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const miniMaxModel = settings.minimax?.model ?? MINIMAX_MODEL_IDS[0];
|
||||
const miniMaxCapabilities = getMiniMaxModelCapabilities(miniMaxModel);
|
||||
const configuredMiniMaxThinkingMode = settings.minimax?.thinkingMode;
|
||||
const miniMaxThinkingMode =
|
||||
configuredMiniMaxThinkingMode &&
|
||||
miniMaxCapabilities?.thinkingModes.includes(configuredMiniMaxThinkingMode)
|
||||
? configuredMiniMaxThinkingMode
|
||||
: (miniMaxCapabilities?.thinkingModes[0] ?? configuredMiniMaxThinkingMode ?? 'adaptive');
|
||||
const miniMaxBaseUrl = settings.minimax?.baseUrl ?? MINIMAX_ANTHROPIC_BASE_URLS.global_en;
|
||||
const miniMaxDocsRoot =
|
||||
miniMaxBaseUrl === MINIMAX_ANTHROPIC_BASE_URLS.cn_zh
|
||||
? MINIMAX_DOCS_ROOTS.cn_zh
|
||||
: MINIMAX_DOCS_ROOTS.global_en;
|
||||
|
||||
const providers: LLMProvider[] = [
|
||||
'openai',
|
||||
'gemini',
|
||||
|
|
@ -864,7 +884,7 @@ export const SettingsPanel = ({
|
|||
value: settings.minimax?.apiKey ?? '',
|
||||
placeholder: t('settings:providers.minimax.apiKeyPlaceholder'),
|
||||
helperText: t('settings:providers.minimax.helperText'),
|
||||
helperLink: 'https://platform.minimax.io',
|
||||
helperLink: miniMaxDocsRoot,
|
||||
helperLinkLabel: t('settings:providers.minimax.helperLinkLabel'),
|
||||
isVisible: !!showApiKey['minimax'],
|
||||
onChange: (value) =>
|
||||
|
|
@ -875,16 +895,79 @@ export const SettingsPanel = ({
|
|||
onToggleVisibility: () => toggleApiKeyVisibility('minimax'),
|
||||
}}
|
||||
model={{
|
||||
value: settings.minimax?.model ?? 'MiniMax-M2.5',
|
||||
value: miniMaxModel,
|
||||
placeholder: t('settings:providers.minimax.modelPlaceholder'),
|
||||
onChange: (value) =>
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
minimax: { ...prev.minimax!, model: value },
|
||||
minimax: {
|
||||
...prev.minimax!,
|
||||
model: value,
|
||||
thinkingMode:
|
||||
getMiniMaxModelCapabilities(value)?.thinkingModes[0] ??
|
||||
prev.minimax?.thinkingMode,
|
||||
},
|
||||
})),
|
||||
helperText: t('settings:providers.minimax.helperModel'),
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-secondary">
|
||||
{t('settings:providers.minimax.endpoint')}
|
||||
</label>
|
||||
<select
|
||||
value={miniMaxBaseUrl}
|
||||
onChange={(event) =>
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
minimax: { ...prev.minimax!, baseUrl: event.target.value },
|
||||
}))
|
||||
}
|
||||
className="w-full rounded-xl border border-border-subtle bg-elevated px-4 py-3 font-mono text-sm text-text-primary transition-all outline-none focus:border-accent focus:ring-2 focus:ring-accent/20"
|
||||
>
|
||||
<option value={MINIMAX_ANTHROPIC_BASE_URLS.global_en}>
|
||||
{t('settings:providers.minimax.endpoints.global')}
|
||||
</option>
|
||||
<option value={MINIMAX_ANTHROPIC_BASE_URLS.cn_zh}>
|
||||
{t('settings:providers.minimax.endpoints.china')}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-secondary">
|
||||
{t('settings:providers.minimax.thinking')}
|
||||
</label>
|
||||
<select
|
||||
value={miniMaxThinkingMode}
|
||||
disabled={miniMaxCapabilities?.thinkingModes.length === 1}
|
||||
onChange={(event) =>
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
minimax: {
|
||||
...prev.minimax!,
|
||||
thinkingMode: event.target.value as MiniMaxThinkingMode,
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full rounded-xl border border-border-subtle bg-elevated px-4 py-3 text-sm text-text-primary transition-all outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{(miniMaxCapabilities?.thinkingModes ?? ['adaptive', 'disabled']).map((mode) => (
|
||||
<option key={mode} value={mode}>
|
||||
{t(`settings:providers.minimax.thinkingModes.${mode}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{miniMaxCapabilities && (
|
||||
<p className="text-xs text-text-muted">
|
||||
{t('settings:providers.minimax.capabilities', {
|
||||
contextWindow: miniMaxCapabilities.contextWindow.toLocaleString(),
|
||||
modalities: miniMaxCapabilities.inputModalities.join(', '),
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</ProviderConfigCard>
|
||||
)}
|
||||
|
||||
{/* DeepSeek Settings */}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { ChatOllama } from '@langchain/ollama';
|
|||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { createGraphRAGTools, type GraphRAGBackend } from './tools';
|
||||
import type {
|
||||
AgentUserContent,
|
||||
ProviderConfig,
|
||||
OpenAIConfig,
|
||||
AzureOpenAIConfig,
|
||||
|
|
@ -32,7 +33,9 @@ import type {
|
|||
DeepSeekConfig,
|
||||
AgentStreamChunk,
|
||||
AgentHistoryMessage,
|
||||
MiniMaxThinkingMode,
|
||||
} from './types';
|
||||
import { getMiniMaxModelCapabilities, MINIMAX_ANTHROPIC_BASE_URLS } from './types';
|
||||
import {
|
||||
type CodebaseContext,
|
||||
buildDynamicSystemPrompt,
|
||||
|
|
@ -275,14 +278,28 @@ export const createChatModel = (config: ProviderConfig): BaseChatModel => {
|
|||
throw new Error('MiniMax API key is required but was not provided');
|
||||
}
|
||||
|
||||
const capabilities = getMiniMaxModelCapabilities(minimaxConfig.model);
|
||||
const requestedThinkingMode = minimaxConfig.thinkingMode;
|
||||
const thinkingMode: MiniMaxThinkingMode | undefined =
|
||||
requestedThinkingMode && capabilities?.thinkingModes.includes(requestedThinkingMode)
|
||||
? requestedThinkingMode
|
||||
: (capabilities?.thinkingModes[0] ?? requestedThinkingMode);
|
||||
const thinking =
|
||||
thinkingMode && thinkingMode !== 'always_on' ? { type: thinkingMode } : undefined;
|
||||
const temperature =
|
||||
thinkingMode === 'adaptive' || thinkingMode === 'always_on'
|
||||
? undefined
|
||||
: (minimaxConfig.temperature ?? 0.1);
|
||||
|
||||
return new ChatAnthropic({
|
||||
anthropicApiKey: minimaxConfig.apiKey,
|
||||
model: minimaxConfig.model,
|
||||
temperature: minimaxConfig.temperature ?? 0.1,
|
||||
...(temperature !== undefined ? { temperature } : {}),
|
||||
maxTokens: minimaxConfig.maxTokens ?? 8192,
|
||||
streaming: true,
|
||||
...(thinking ? { thinking } : {}),
|
||||
clientOptions: {
|
||||
baseURL: 'https://api.minimax.io/anthropic',
|
||||
baseURL: minimaxConfig.baseUrl ?? MINIMAX_ANTHROPIC_BASE_URLS.global_en,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -393,7 +410,7 @@ export const createGraphRAGAgent = (
|
|||
/**
|
||||
* Message type for agent conversation
|
||||
*/
|
||||
export type AgentMessage = { role: 'user'; content: string } | AgentHistoryMessage;
|
||||
export type AgentMessage = { role: 'user'; content: AgentUserContent } | AgentHistoryMessage;
|
||||
|
||||
export interface AgentRuntimeOptions {
|
||||
/** Capture assistant/tool messages for providers that require exact transcript replay. */
|
||||
|
|
@ -412,7 +429,9 @@ const isAbortError = (error: unknown, signal?: AbortSignal): boolean => {
|
|||
export const buildLangChainMessages = (messages: AgentMessage[]): BaseMessage[] =>
|
||||
messages.map((message) => {
|
||||
if (message.role === 'user') {
|
||||
return new HumanMessage(message.content);
|
||||
return typeof message.content === 'string'
|
||||
? new HumanMessage(message.content)
|
||||
: new HumanMessage({ content: message.content as any });
|
||||
}
|
||||
if (message.role === 'tool') {
|
||||
return new ToolMessage({
|
||||
|
|
@ -542,6 +561,7 @@ export async function* streamAgentResponse(
|
|||
|
||||
// Handle content that can be string or array of content blocks
|
||||
let content: string = '';
|
||||
let thinkingContent: string = '';
|
||||
if (typeof rawContent === 'string') {
|
||||
content = rawContent;
|
||||
} else if (Array.isArray(rawContent)) {
|
||||
|
|
@ -550,6 +570,14 @@ export async function* streamAgentResponse(
|
|||
.filter((block: any) => block.type === 'text' || typeof block === 'string')
|
||||
.map((block: any) => (typeof block === 'string' ? block : block.text || ''))
|
||||
.join('');
|
||||
thinkingContent = rawContent
|
||||
.filter((block: any) => block?.type === 'thinking')
|
||||
.map((block: any) => block.thinking || '')
|
||||
.join('');
|
||||
}
|
||||
|
||||
if (thinkingContent) {
|
||||
yield { type: 'reasoning', reasoning: thinkingContent };
|
||||
}
|
||||
|
||||
// If chunk has content, stream it
|
||||
|
|
|
|||
|
|
@ -19,12 +19,32 @@ import {
|
|||
GLMConfig,
|
||||
DeepSeekConfig,
|
||||
ProviderConfig,
|
||||
MINIMAX_MODEL_IDS,
|
||||
} from './types';
|
||||
import { DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OLLAMA_BASE_URL } from '../../config/ui-constants';
|
||||
import { resilientFetch } from 'gitnexus-shared';
|
||||
|
||||
const STORAGE_KEY = 'gitnexus-llm-settings';
|
||||
|
||||
const mergeMiniMaxSettings = (
|
||||
stored?: LLMSettings['minimax'],
|
||||
): NonNullable<LLMSettings['minimax']> => {
|
||||
const merged = {
|
||||
...DEFAULT_LLM_SETTINGS.minimax,
|
||||
...stored,
|
||||
};
|
||||
|
||||
if (!(MINIMAX_MODEL_IDS as readonly string[]).includes(merged.model ?? '')) {
|
||||
return {
|
||||
...merged,
|
||||
model: DEFAULT_LLM_SETTINGS.minimax?.model,
|
||||
thinkingMode: DEFAULT_LLM_SETTINGS.minimax?.thinkingMode,
|
||||
};
|
||||
}
|
||||
|
||||
return merged;
|
||||
};
|
||||
|
||||
const mergeWithDefaults = (parsed?: Partial<LLMSettings> | null): LLMSettings => ({
|
||||
...DEFAULT_LLM_SETTINGS,
|
||||
...parsed,
|
||||
|
|
@ -52,10 +72,7 @@ const mergeWithDefaults = (parsed?: Partial<LLMSettings> | null): LLMSettings =>
|
|||
...DEFAULT_LLM_SETTINGS.openrouter,
|
||||
...parsed?.openrouter,
|
||||
},
|
||||
minimax: {
|
||||
...DEFAULT_LLM_SETTINGS.minimax,
|
||||
...parsed?.minimax,
|
||||
},
|
||||
minimax: mergeMiniMaxSettings(parsed?.minimax),
|
||||
glm: {
|
||||
...DEFAULT_LLM_SETTINGS.glm,
|
||||
...parsed?.glm,
|
||||
|
|
@ -437,7 +454,7 @@ export const getAvailableModels = (provider: LLMProvider): string[] => {
|
|||
case 'ollama':
|
||||
return ['llama3.2', 'llama3.1', 'mistral', 'codellama', 'deepseek-coder'];
|
||||
case 'minimax':
|
||||
return ['MiniMax-M2.5', 'MiniMax-M2.5-highspeed'];
|
||||
return [...MINIMAX_MODEL_IDS];
|
||||
case 'glm':
|
||||
return ['GLM-5', 'GLM-5-Turbo', 'GLM-4.7', 'GLM-4.5'];
|
||||
case 'deepseek':
|
||||
|
|
|
|||
|
|
@ -20,6 +20,71 @@ export type LLMProvider =
|
|||
| 'glm'
|
||||
| 'deepseek';
|
||||
|
||||
export const MINIMAX_ANTHROPIC_BASE_URLS = {
|
||||
global_en: 'https://api.minimax.io/anthropic',
|
||||
cn_zh: 'https://api.minimaxi.com/anthropic',
|
||||
} as const;
|
||||
|
||||
export const MINIMAX_DOCS_ROOTS = {
|
||||
global_en: 'https://platform.minimax.io/docs',
|
||||
cn_zh: 'https://platform.minimaxi.com/docs',
|
||||
} as const;
|
||||
|
||||
export const MINIMAX_MODEL_IDS = ['MiniMax-M3', 'MiniMax-M2.7'] as const;
|
||||
|
||||
export type MiniMaxModelId = (typeof MINIMAX_MODEL_IDS)[number];
|
||||
export type MiniMaxThinkingMode = 'adaptive' | 'disabled' | 'always_on';
|
||||
export type MiniMaxInputModality = 'text' | 'image' | 'video';
|
||||
|
||||
export interface MiniMaxModelCapabilities {
|
||||
contextWindow: number;
|
||||
inputModalities: readonly MiniMaxInputModality[];
|
||||
thinkingModes: readonly MiniMaxThinkingMode[];
|
||||
}
|
||||
|
||||
export const MINIMAX_MODEL_CAPABILITIES: Record<MiniMaxModelId, MiniMaxModelCapabilities> = {
|
||||
'MiniMax-M3': {
|
||||
contextWindow: 1_000_000,
|
||||
inputModalities: ['text', 'image', 'video'],
|
||||
thinkingModes: ['adaptive', 'disabled'],
|
||||
},
|
||||
'MiniMax-M2.7': {
|
||||
contextWindow: 204_800,
|
||||
inputModalities: ['text'],
|
||||
thinkingModes: ['always_on'],
|
||||
},
|
||||
};
|
||||
|
||||
export const getMiniMaxModelCapabilities = (model: string): MiniMaxModelCapabilities | undefined =>
|
||||
MINIMAX_MODEL_CAPABILITIES[model as MiniMaxModelId];
|
||||
|
||||
export type MiniMaxMediaDetail = 'low' | 'default' | 'high';
|
||||
|
||||
export type MiniMaxMediaSource =
|
||||
| {
|
||||
type: 'url';
|
||||
url: string;
|
||||
detail?: MiniMaxMediaDetail;
|
||||
fps?: number;
|
||||
max_long_side_pixel?: number;
|
||||
}
|
||||
| {
|
||||
type: 'base64';
|
||||
media_type: string;
|
||||
data: string;
|
||||
detail?: MiniMaxMediaDetail;
|
||||
fps?: number;
|
||||
max_long_side_pixel?: number;
|
||||
};
|
||||
|
||||
export type AgentUserContent =
|
||||
| string
|
||||
| Array<
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'image'; source: MiniMaxMediaSource }
|
||||
| { type: 'video'; source: MiniMaxMediaSource }
|
||||
>;
|
||||
|
||||
/**
|
||||
* Base configuration shared by all providers
|
||||
*/
|
||||
|
|
@ -94,7 +159,9 @@ export interface OpenRouterConfig extends BaseProviderConfig {
|
|||
export interface MiniMaxConfig extends BaseProviderConfig {
|
||||
provider: 'minimax';
|
||||
apiKey: string;
|
||||
model: string; // e.g., 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed'
|
||||
model: string;
|
||||
baseUrl?: string;
|
||||
thinkingMode?: MiniMaxThinkingMode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -200,7 +267,9 @@ export const DEFAULT_LLM_SETTINGS: LLMSettings = {
|
|||
},
|
||||
minimax: {
|
||||
apiKey: '',
|
||||
model: 'MiniMax-M2.5',
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
baseUrl: MINIMAX_ANTHROPIC_BASE_URLS.global_en,
|
||||
thinkingMode: 'adaptive',
|
||||
temperature: 0.1,
|
||||
},
|
||||
glm: {
|
||||
|
|
|
|||
|
|
@ -76,8 +76,20 @@
|
|||
"apiKeyPlaceholder": "Enter your MiniMax API key",
|
||||
"helperText": "Get your API key from",
|
||||
"helperLinkLabel": "MiniMax Platform",
|
||||
"modelPlaceholder": "e.g., MiniMax-M2.5, MiniMax-M2.5-highspeed",
|
||||
"helperModel": "Available: MiniMax-M2.5 (default), MiniMax-M2.5-highspeed (faster)"
|
||||
"modelPlaceholder": "e.g., MiniMax-M3 or MiniMax-M2.7",
|
||||
"helperModel": "Available: MiniMax-M3 (default) and MiniMax-M2.7",
|
||||
"endpoint": "Regional endpoint",
|
||||
"endpoints": {
|
||||
"global": "Global (api.minimax.io)",
|
||||
"china": "China (api.minimaxi.com)"
|
||||
},
|
||||
"thinking": "Thinking mode",
|
||||
"thinkingModes": {
|
||||
"adaptive": "Adaptive",
|
||||
"disabled": "Disabled",
|
||||
"always_on": "Always on"
|
||||
},
|
||||
"capabilities": "{{contextWindow}} token context | Inputs: {{modalities}}"
|
||||
},
|
||||
"glm": {
|
||||
"apiKeyPlaceholder": "Enter your Z.AI API key"
|
||||
|
|
|
|||
|
|
@ -76,8 +76,20 @@
|
|||
"apiKeyPlaceholder": "输入 MiniMax API Key",
|
||||
"helperText": "从这里获取 API Key:",
|
||||
"helperLinkLabel": "MiniMax Platform",
|
||||
"modelPlaceholder": "例如:MiniMax-M2.5、MiniMax-M2.5-highspeed",
|
||||
"helperModel": "可用:MiniMax-M2.5(默认)、MiniMax-M2.5-highspeed(更快)"
|
||||
"modelPlaceholder": "例如:MiniMax-M3 或 MiniMax-M2.7",
|
||||
"helperModel": "可用:MiniMax-M3(默认)和 MiniMax-M2.7",
|
||||
"endpoint": "区域端点",
|
||||
"endpoints": {
|
||||
"global": "全球(api.minimax.io)",
|
||||
"china": "中国(api.minimaxi.com)"
|
||||
},
|
||||
"thinking": "思考模式",
|
||||
"thinkingModes": {
|
||||
"adaptive": "自适应",
|
||||
"disabled": "关闭",
|
||||
"always_on": "始终开启"
|
||||
},
|
||||
"capabilities": "{{contextWindow}} token 上下文 | 输入:{{modalities}}"
|
||||
},
|
||||
"glm": {
|
||||
"apiKeyPlaceholder": "输入 Z.AI API Key"
|
||||
|
|
|
|||
|
|
@ -95,3 +95,34 @@ describe('streamAgentResponse abort', () => {
|
|||
expect(chunks).toEqual([{ type: 'error', error: 'Cannot abort the current transaction' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('streamAgentResponse content blocks', () => {
|
||||
const userMessage: AgentMessage[] = [{ role: 'user', content: 'hello' }];
|
||||
|
||||
it('emits thinking blocks as reasoning', async () => {
|
||||
const agent = {
|
||||
stream: async function* () {
|
||||
yield [
|
||||
'messages',
|
||||
[
|
||||
{
|
||||
_getType: () => 'ai',
|
||||
content: [{ type: 'thinking', thinking: 'Reviewing the repository context.' }],
|
||||
tool_calls: [],
|
||||
},
|
||||
],
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
const chunks = [];
|
||||
for await (const chunk of streamAgentResponse(agent as any, userMessage)) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
expect(chunks).toEqual([
|
||||
{ type: 'reasoning', reasoning: 'Reviewing the repository context.' },
|
||||
{ type: 'done', historyMessages: undefined },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
DeepSeekChatOpenAI,
|
||||
DeepSeekChatOpenAICompletions,
|
||||
} from '../../src/core/llm/deepseek-chat-model';
|
||||
import { MINIMAX_ANTHROPIC_BASE_URLS, MINIMAX_MODEL_IDS } from '../../src/core/llm/types';
|
||||
|
||||
describe('buildLangChainMessages', () => {
|
||||
it('reconstructs assistant tool-call turns for replay', () => {
|
||||
|
|
@ -50,6 +51,24 @@ describe('buildLangChainMessages', () => {
|
|||
]);
|
||||
expect((langChainMessages[2] as any).tool_call_id).toBe('call_weather');
|
||||
});
|
||||
|
||||
it('preserves MiniMax image and video content blocks', () => {
|
||||
const content = [
|
||||
{ type: 'text' as const, text: 'Compare these inputs.' },
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: { type: 'url' as const, url: 'https://example.com/image.png' },
|
||||
},
|
||||
{
|
||||
type: 'video' as const,
|
||||
source: { type: 'url' as const, url: 'https://example.com/video.mp4', fps: 1 },
|
||||
},
|
||||
];
|
||||
|
||||
const [message] = buildLangChainMessages([{ role: 'user', content }]);
|
||||
|
||||
expect((message as any).content).toEqual(content);
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeAgentHistoryMessages', () => {
|
||||
|
|
@ -206,6 +225,48 @@ it('drops reasoningContent from serialized assistant messages without tool calls
|
|||
});
|
||||
|
||||
describe('createChatModel', () => {
|
||||
it('configures MiniMax-M3 adaptive thinking on the China endpoint', () => {
|
||||
const model = createChatModel({
|
||||
provider: 'minimax',
|
||||
apiKey: 'minimax-test-key',
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
baseUrl: MINIMAX_ANTHROPIC_BASE_URLS.cn_zh,
|
||||
thinkingMode: 'adaptive',
|
||||
temperature: 0.1,
|
||||
} as any) as any;
|
||||
|
||||
expect(model.model).toBe(MINIMAX_MODEL_IDS[0]);
|
||||
expect(model.clientOptions.baseURL).toBe(MINIMAX_ANTHROPIC_BASE_URLS.cn_zh);
|
||||
expect(model.thinking).toEqual({ type: 'adaptive' });
|
||||
expect(model.temperature).toBeUndefined();
|
||||
});
|
||||
|
||||
it('supports disabled thinking for MiniMax-M3', () => {
|
||||
const model = createChatModel({
|
||||
provider: 'minimax',
|
||||
apiKey: 'minimax-test-key',
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
thinkingMode: 'disabled',
|
||||
temperature: 0.1,
|
||||
} as any) as any;
|
||||
|
||||
expect(model.thinking).toEqual({ type: 'disabled' });
|
||||
expect(model.temperature).toBe(0.1);
|
||||
});
|
||||
|
||||
it('keeps MiniMax-M2.7 thinking always on', () => {
|
||||
const model = createChatModel({
|
||||
provider: 'minimax',
|
||||
apiKey: 'minimax-test-key',
|
||||
model: MINIMAX_MODEL_IDS[1],
|
||||
thinkingMode: 'disabled',
|
||||
temperature: 0.1,
|
||||
} as any) as any;
|
||||
|
||||
expect(model.invocationParams({}).thinking).toBeUndefined();
|
||||
expect(model.temperature).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps DeepSeek model subclasses on withConfig clones used for tool binding', () => {
|
||||
const model = createChatModel({
|
||||
provider: 'deepseek',
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ import {
|
|||
getAvailableModels,
|
||||
getProviderCapabilities,
|
||||
} from '../../src/core/llm/settings-service';
|
||||
import {
|
||||
getMiniMaxModelCapabilities,
|
||||
MINIMAX_ANTHROPIC_BASE_URLS,
|
||||
MINIMAX_MODEL_IDS,
|
||||
} from '../../src/core/llm/types';
|
||||
import { createChatModel } from '../../src/core/llm/agent';
|
||||
|
||||
describe('loadSettings', () => {
|
||||
it('returns defaults when nothing is stored', () => {
|
||||
|
|
@ -17,6 +23,11 @@ describe('loadSettings', () => {
|
|||
expect(settings.activeProvider).toBeDefined();
|
||||
expect(settings.openai).toBeDefined();
|
||||
expect(settings.ollama).toBeDefined();
|
||||
expect(settings.minimax).toMatchObject({
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
baseUrl: MINIMAX_ANTHROPIC_BASE_URLS.global_en,
|
||||
thinkingMode: 'adaptive',
|
||||
});
|
||||
});
|
||||
|
||||
it('merges stored values with defaults', () => {
|
||||
|
|
@ -35,6 +46,30 @@ describe('loadSettings', () => {
|
|||
expect(settings.openai).toBeDefined();
|
||||
});
|
||||
|
||||
it('migrates unsupported legacy MiniMax models to the current default', () => {
|
||||
sessionStorage.setItem(
|
||||
'gitnexus-llm-settings',
|
||||
JSON.stringify({
|
||||
activeProvider: 'minimax',
|
||||
minimax: {
|
||||
apiKey: 'minimax-test-key',
|
||||
model: 'MiniMax-M2.5',
|
||||
temperature: 0.1,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const settings = loadSettings();
|
||||
expect(settings.minimax).toMatchObject({
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
thinkingMode: 'adaptive',
|
||||
});
|
||||
|
||||
const model = createChatModel(getActiveProviderConfig()!) as any;
|
||||
expect(model.model).toBe(MINIMAX_MODEL_IDS[0]);
|
||||
expect(model.thinking).toEqual({ type: 'adaptive' });
|
||||
});
|
||||
|
||||
it('returns defaults on corrupted JSON', () => {
|
||||
sessionStorage.setItem('gitnexus-llm-settings', 'not-json{{{');
|
||||
const settings = loadSettings();
|
||||
|
|
@ -116,6 +151,26 @@ describe('getActiveProviderConfig', () => {
|
|||
expect(config!.provider).toBe('deepseek');
|
||||
});
|
||||
|
||||
it('returns the regional endpoint and thinking mode for MiniMax', () => {
|
||||
const settings = loadSettings();
|
||||
settings.activeProvider = 'minimax';
|
||||
settings.minimax = {
|
||||
...settings.minimax,
|
||||
apiKey: 'minimax-test-key',
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
baseUrl: MINIMAX_ANTHROPIC_BASE_URLS.cn_zh,
|
||||
thinkingMode: 'disabled',
|
||||
};
|
||||
saveSettings(settings);
|
||||
|
||||
expect(getActiveProviderConfig()).toMatchObject({
|
||||
provider: 'minimax',
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
baseUrl: MINIMAX_ANTHROPIC_BASE_URLS.cn_zh,
|
||||
thinkingMode: 'disabled',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null for openrouter with empty API key', () => {
|
||||
const settings = loadSettings();
|
||||
settings.activeProvider = 'openrouter';
|
||||
|
|
@ -161,6 +216,20 @@ describe('getAvailableModels', () => {
|
|||
expect(getAvailableModels('ollama').length).toBeGreaterThan(0);
|
||||
expect(getAvailableModels('anthropic')).toContain('claude-sonnet-4-20250514');
|
||||
expect(getAvailableModels('deepseek')).toContain('deepseek-v4-flash');
|
||||
expect(getAvailableModels('minimax')).toEqual([...MINIMAX_MODEL_IDS]);
|
||||
});
|
||||
|
||||
it('describes MiniMax model input and thinking capabilities', () => {
|
||||
expect(getMiniMaxModelCapabilities(MINIMAX_MODEL_IDS[0])).toEqual({
|
||||
contextWindow: 1_000_000,
|
||||
inputModalities: ['text', 'image', 'video'],
|
||||
thinkingModes: ['adaptive', 'disabled'],
|
||||
});
|
||||
expect(getMiniMaxModelCapabilities(MINIMAX_MODEL_IDS[1])).toEqual({
|
||||
contextWindow: 204_800,
|
||||
inputModalities: ['text'],
|
||||
thinkingModes: ['always_on'],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty array for unknown provider', () => {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ Generates repository documentation from the knowledge graph using an LLM. Requir
|
|||
| Flag | Effect |
|
||||
| ------------------- | ----------------------------------------- |
|
||||
| `--force` | Force full regeneration |
|
||||
| `--model <model>` | LLM model (default: minimax/minimax-m2.5) |
|
||||
| `--model <model>` | LLM model (default: MiniMax-M3) |
|
||||
| `--base-url <url>` | LLM API base URL |
|
||||
| `--api-key <key>` | LLM API key |
|
||||
| `--concurrency <n>` | Parallel LLM calls (default: 3) |
|
||||
|
|
|
|||
|
|
@ -226,16 +226,15 @@ export const en = {
|
|||
'Clean parked LadybugDB recovery sidecars (missing-shadow WAL quarantines and dirty-recovery parks)',
|
||||
'help.option.wiki.force': 'Force full regeneration even if up to date',
|
||||
'help.option.wiki.provider':
|
||||
'LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)',
|
||||
'help.option.wiki.model': 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)',
|
||||
'LLM provider: minimax, openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: minimax)',
|
||||
'help.option.wiki.model': 'LLM model or deployment name (default: MiniMax-M3)',
|
||||
'help.option.wiki.baseUrl':
|
||||
'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1',
|
||||
'help.option.wiki.apiKey': 'LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)',
|
||||
'help.option.wiki.apiVersion':
|
||||
'Azure api-version query param, e.g. 2024-10-21 (legacy Azure API only)',
|
||||
'help.option.wiki.reasoningModel':
|
||||
'Mark deployment as reasoning model (o1/o3/o4-mini) — strips temperature, uses max_completion_tokens',
|
||||
'help.option.wiki.noReasoningModel': 'Disable reasoning model mode (overrides saved config)',
|
||||
'help.option.wiki.reasoningModel': 'Enable reasoning mode; MiniMax-M3 uses adaptive thinking',
|
||||
'help.option.wiki.noReasoningModel': 'Disable reasoning mode; MiniMax-M3 disables thinking',
|
||||
'help.option.wiki.concurrency': 'Parallel LLM calls (default: 3)',
|
||||
'help.option.wiki.timeout': 'LLM request timeout in seconds (default: disabled)',
|
||||
'help.option.wiki.retries': 'Max LLM retry attempts per request (default: 3)',
|
||||
|
|
|
|||
|
|
@ -214,15 +214,14 @@ export const zhCN = {
|
|||
'清理已暂存的 LadybugDB 恢复 sidecar(missing-shadow WAL 隔离文件与 dirty-recovery 暂存文件)',
|
||||
'help.option.wiki.force': '即使已是最新也强制完整重新生成',
|
||||
'help.option.wiki.provider':
|
||||
'LLM 提供商:openai、openrouter、azure、custom、cursor、claude、codex 或 opencode(默认:openai)',
|
||||
'help.option.wiki.model': 'LLM 模型或 Azure deployment 名称(默认:minimax/minimax-m2.5)',
|
||||
'LLM 提供商:minimax、openai、openrouter、azure、custom、cursor、claude、codex 或 opencode(默认:minimax)',
|
||||
'help.option.wiki.model': 'LLM 模型或 deployment 名称(默认:MiniMax-M3)',
|
||||
'help.option.wiki.baseUrl':
|
||||
'LLM API base URL。Azure v1:https://{resource}.openai.azure.com/openai/v1',
|
||||
'help.option.wiki.apiKey': 'LLM API key 或 Azure api-key(保存到 ~/.gitnexus/config.json)',
|
||||
'help.option.wiki.apiVersion': 'Azure api-version 查询参数,例如 2024-10-21(仅旧版 Azure API)',
|
||||
'help.option.wiki.reasoningModel':
|
||||
'标记 deployment 为 reasoning model(o1/o3/o4-mini)— 去除 temperature,使用 max_completion_tokens',
|
||||
'help.option.wiki.noReasoningModel': '禁用 reasoning model 模式(覆盖已保存配置)',
|
||||
'help.option.wiki.reasoningModel': '启用 reasoning 模式;MiniMax-M3 使用自适应 thinking',
|
||||
'help.option.wiki.noReasoningModel': '禁用 reasoning 模式;MiniMax-M3 关闭 thinking',
|
||||
'help.option.wiki.concurrency': '并行 LLM 调用数(默认:3)',
|
||||
'help.option.wiki.timeout': 'LLM 请求超时时间(秒,默认:禁用)',
|
||||
'help.option.wiki.retries': '每个请求的最大 LLM 重试次数(默认:3)',
|
||||
|
|
|
|||
|
|
@ -303,9 +303,9 @@ program
|
|||
.option('-f, --force', 'Force full regeneration even if up to date')
|
||||
.option(
|
||||
'--provider <provider>',
|
||||
'LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)',
|
||||
'LLM provider: minimax, openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: minimax)',
|
||||
)
|
||||
.option('--model <model>', 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)')
|
||||
.option('--model <model>', 'LLM model or deployment name (default: MiniMax-M3)')
|
||||
.option(
|
||||
'--base-url <url>',
|
||||
'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1',
|
||||
|
|
@ -315,11 +315,8 @@ program
|
|||
'--api-version <version>',
|
||||
'Azure api-version query param, e.g. 2024-10-21 (legacy Azure API only)',
|
||||
)
|
||||
.option(
|
||||
'--reasoning-model',
|
||||
'Mark deployment as reasoning model (o1/o3/o4-mini) — strips temperature, uses max_completion_tokens',
|
||||
)
|
||||
.option('--no-reasoning-model', 'Disable reasoning model mode (overrides saved config)')
|
||||
.option('--reasoning-model', 'Enable reasoning mode; MiniMax-M3 uses adaptive thinking')
|
||||
.option('--no-reasoning-model', 'Disable reasoning mode; MiniMax-M3 disables thinking')
|
||||
.option('--concurrency <n>', 'Parallel LLM calls (default: 3)', '3')
|
||||
.option('--timeout <seconds>', 'LLM request timeout in seconds (default: disabled)')
|
||||
.option('--retries <n>', 'Max LLM retry attempts per request (default: 3)')
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import {
|
|||
} from '../storage/repo-manager.js';
|
||||
import { WikiGenerator, type WikiOptions } from '../core/wiki/generator.js';
|
||||
import {
|
||||
MINIMAX_MODEL_IDS,
|
||||
MINIMAX_OPENAI_BASE_URLS,
|
||||
parseLLMAllowedInsecureHttpHosts,
|
||||
resolveLLMConfig,
|
||||
type LLMProvider,
|
||||
|
|
@ -216,11 +218,30 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
) {
|
||||
const existing = await loadCLIConfig();
|
||||
const updates: Partial<typeof existing> = {};
|
||||
const providerChanged = !!options.provider && options.provider !== existing.provider;
|
||||
if (providerChanged) {
|
||||
updates.apiKey = undefined;
|
||||
updates.baseUrl = undefined;
|
||||
updates.model = undefined;
|
||||
updates.apiVersion = undefined;
|
||||
updates.isReasoningModel = undefined;
|
||||
}
|
||||
if (options.apiKey) updates.apiKey = options.apiKey;
|
||||
if (options.baseUrl) updates.baseUrl = options.baseUrl;
|
||||
if (options.provider) updates.provider = options.provider;
|
||||
if (options.apiVersion) updates.apiVersion = options.apiVersion;
|
||||
if (options.reasoningModel !== undefined) updates.isReasoningModel = options.reasoningModel;
|
||||
if (options.provider === 'minimax') {
|
||||
if (providerChanged && options.reasoningModel === undefined) {
|
||||
updates.isReasoningModel = undefined;
|
||||
}
|
||||
if (!options.baseUrl && (providerChanged || !existing.baseUrl)) {
|
||||
updates.baseUrl = MINIMAX_OPENAI_BASE_URLS.global_en;
|
||||
}
|
||||
if (!options.model && (providerChanged || !existing.model)) {
|
||||
updates.model = MINIMAX_MODEL_IDS[0];
|
||||
}
|
||||
}
|
||||
// Save model to appropriate field based on provider.
|
||||
if (options.model) {
|
||||
const targetProvider = options.provider ?? existing.provider;
|
||||
|
|
@ -237,7 +258,7 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
const savedConfig = await loadCLIConfig();
|
||||
const hasSavedConfig = !!(
|
||||
isLocalProvider(savedConfig.provider) ||
|
||||
(savedConfig.apiKey && savedConfig.baseUrl)
|
||||
(savedConfig.apiKey && (savedConfig.baseUrl || savedConfig.provider === 'minimax'))
|
||||
);
|
||||
const hasCLIOverrides = !!(
|
||||
options?.apiKey ||
|
||||
|
|
@ -265,7 +286,7 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
// Non-interactive mode — need either API key or Cursor CLI
|
||||
if (!llmConfig.apiKey && !isLocalProvider(llmConfig.provider)) {
|
||||
console.log(' Error: No LLM API key found.');
|
||||
console.log(' Set OPENAI_API_KEY or GITNEXUS_API_KEY environment variable,');
|
||||
console.log(' Set MINIMAX_API_KEY, GITNEXUS_API_KEY, or OPENAI_API_KEY,');
|
||||
console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex|opencode.\n');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
|
|
@ -273,9 +294,7 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
// Non-interactive with env var or cursor — just use it
|
||||
} else {
|
||||
console.log(" No LLM configured. Let's set it up.\n");
|
||||
console.log(
|
||||
' Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, Cursor CLI, Claude CLI, Codex CLI, or OpenCode CLI.\n',
|
||||
);
|
||||
console.log(' Supports MiniMax, OpenAI-compatible APIs, and local agent CLIs.\n');
|
||||
|
||||
// Check if local agent CLIs are available.
|
||||
const hasCursor = detectCursorCLI();
|
||||
|
|
@ -292,7 +311,9 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
console.log(' [2] OpenRouter (openrouter.ai)');
|
||||
console.log(' [3] Azure OpenAI');
|
||||
console.log(' [4] Custom endpoint');
|
||||
let nextChoice = 5;
|
||||
console.log(' [5] MiniMax Global (api.minimax.io)');
|
||||
console.log(' [6] MiniMax China (api.minimaxi.com)');
|
||||
let nextChoice = 7;
|
||||
if (hasCursor) {
|
||||
const choice = String(nextChoice++);
|
||||
localChoices.push({
|
||||
|
|
@ -413,10 +434,10 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
provider: 'azure',
|
||||
};
|
||||
} else {
|
||||
// OpenAI-compatible provider (OpenAI, OpenRouter, Custom)
|
||||
// OpenAI-compatible provider setup
|
||||
if (choice === '2') {
|
||||
baseUrl = 'https://openrouter.ai/api/v1';
|
||||
defaultModel = 'minimax/minimax-m2.5';
|
||||
defaultModel = '';
|
||||
provider = 'openrouter';
|
||||
} else if (choice === '4') {
|
||||
baseUrl = await prompt(' Base URL (e.g. http://localhost:11434/v1): ');
|
||||
|
|
@ -427,6 +448,11 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
}
|
||||
defaultModel = 'gpt-4o-mini';
|
||||
provider = 'custom';
|
||||
} else if (choice === '5' || choice === '6') {
|
||||
baseUrl =
|
||||
choice === '6' ? MINIMAX_OPENAI_BASE_URLS.cn_zh : MINIMAX_OPENAI_BASE_URLS.global_en;
|
||||
defaultModel = MINIMAX_MODEL_IDS[0];
|
||||
provider = 'minimax';
|
||||
} else {
|
||||
baseUrl = 'https://api.openai.com/v1';
|
||||
defaultModel = 'gpt-4o-mini';
|
||||
|
|
@ -434,11 +460,22 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
}
|
||||
|
||||
// Model
|
||||
const modelInput = await prompt(` Model (default: ${defaultModel}): `);
|
||||
const modelInput = await prompt(
|
||||
defaultModel ? ` Model (default: ${defaultModel}): ` : ' Model: ',
|
||||
);
|
||||
const model = modelInput || defaultModel;
|
||||
if (!model) {
|
||||
console.log('\n No model provided. Aborting.\n');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// API key — pre-fill hint if env var exists
|
||||
const envKey = process.env.GITNEXUS_API_KEY || process.env.OPENAI_API_KEY || '';
|
||||
const envKey =
|
||||
(provider === 'minimax' ? process.env.MINIMAX_API_KEY : undefined) ||
|
||||
process.env.GITNEXUS_API_KEY ||
|
||||
process.env.OPENAI_API_KEY ||
|
||||
'';
|
||||
if (envKey) {
|
||||
const masked = envKey.slice(0, 6) + '...' + envKey.slice(-4);
|
||||
const useEnv = await prompt(` Use existing env key (${masked})? (Y/n): `);
|
||||
|
|
@ -458,7 +495,15 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
}
|
||||
|
||||
// Save
|
||||
await saveCLIConfig({ apiKey: key, baseUrl, model, provider });
|
||||
await saveCLIConfig({
|
||||
...savedConfig,
|
||||
apiKey: key,
|
||||
baseUrl,
|
||||
model,
|
||||
provider,
|
||||
apiVersion: undefined,
|
||||
isReasoningModel: undefined,
|
||||
});
|
||||
console.log(' Config saved to ~/.gitnexus/config.json\n');
|
||||
|
||||
llmConfig = { ...llmConfig, apiKey: key, baseUrl, model, provider };
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { CircuitOpenError, ResilientFetchExhaustedError, resilientFetch } from '
|
|||
* LLM Client for Wiki Generation
|
||||
*
|
||||
* OpenAI-compatible API client using native fetch.
|
||||
* Supports OpenAI, Azure, LiteLLM, Ollama, and any OpenAI-compatible endpoint.
|
||||
* Supports MiniMax and other OpenAI-compatible endpoints.
|
||||
*
|
||||
* Config priority: CLI flags > env vars > defaults
|
||||
*/
|
||||
|
|
@ -17,7 +17,40 @@ export type LLMProvider =
|
|||
| 'cursor'
|
||||
| 'claude'
|
||||
| 'codex'
|
||||
| 'opencode';
|
||||
| 'opencode'
|
||||
| 'minimax';
|
||||
|
||||
export const MINIMAX_OPENAI_BASE_URLS = {
|
||||
global_en: 'https://api.minimax.io/v1',
|
||||
cn_zh: 'https://api.minimaxi.com/v1',
|
||||
} as const;
|
||||
|
||||
export const MINIMAX_MODEL_IDS = ['MiniMax-M3', 'MiniMax-M2.7'] as const;
|
||||
|
||||
export type MiniMaxThinkingMode = 'adaptive' | 'disabled' | 'always_on';
|
||||
|
||||
export type LLMUserContent =
|
||||
| string
|
||||
| Array<
|
||||
| { type: 'text'; text: string }
|
||||
| {
|
||||
type: 'image_url';
|
||||
image_url: {
|
||||
url: string;
|
||||
detail?: 'low' | 'default' | 'high';
|
||||
max_long_side_pixel?: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'video_url';
|
||||
video_url: {
|
||||
url: string;
|
||||
detail?: 'low' | 'default' | 'high';
|
||||
fps?: number;
|
||||
max_long_side_pixel?: number;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
export interface LLMConfig {
|
||||
apiKey: string;
|
||||
|
|
@ -45,6 +78,17 @@ export interface LLMResponse {
|
|||
completionTokens?: number;
|
||||
}
|
||||
|
||||
export function resolveMiniMaxThinkingMode(
|
||||
model: string,
|
||||
reasoningOverride?: boolean,
|
||||
): MiniMaxThinkingMode | undefined {
|
||||
if (model === MINIMAX_MODEL_IDS[1]) return 'always_on';
|
||||
if (model === MINIMAX_MODEL_IDS[0]) {
|
||||
return reasoningOverride === false ? 'disabled' : 'adaptive';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve LLM configuration from env vars, saved config, and optional overrides.
|
||||
* Priority: overrides (CLI flags) > env vars > ~/.gitnexus/config.json > error
|
||||
|
|
@ -54,7 +98,11 @@ export interface LLMResponse {
|
|||
export async function resolveLLMConfig(overrides?: Partial<LLMConfig>): Promise<LLMConfig> {
|
||||
const { loadCLIConfig } = await import('../../storage/repo-manager.js');
|
||||
const savedConfig = await loadCLIConfig();
|
||||
const savedProvider = overrides?.provider ?? savedConfig.provider;
|
||||
const hasLegacyHttpConfig = !savedConfig.provider && !!(savedConfig.model || savedConfig.baseUrl);
|
||||
const savedProvider =
|
||||
overrides?.provider ?? savedConfig.provider ?? (hasLegacyHttpConfig ? 'openai' : 'minimax');
|
||||
const reuseSavedHttpConfig =
|
||||
savedConfig.provider === savedProvider || (hasLegacyHttpConfig && savedProvider === 'openai');
|
||||
const savedLocalModel =
|
||||
savedProvider === 'cursor'
|
||||
? savedConfig.cursorModel
|
||||
|
|
@ -73,9 +121,10 @@ export async function resolveLLMConfig(overrides?: Partial<LLMConfig>): Promise<
|
|||
|
||||
const apiKey =
|
||||
overrides?.apiKey ||
|
||||
process.env.GITNEXUS_API_KEY ||
|
||||
process.env.OPENAI_API_KEY ||
|
||||
savedConfig.apiKey ||
|
||||
(savedProvider === 'minimax' ? process.env.MINIMAX_API_KEY : undefined) ||
|
||||
(savedProvider !== 'minimax' ? process.env.GITNEXUS_API_KEY : undefined) ||
|
||||
(savedProvider !== 'minimax' ? process.env.OPENAI_API_KEY : undefined) ||
|
||||
(reuseSavedHttpConfig ? savedConfig.apiKey : undefined) ||
|
||||
'';
|
||||
|
||||
return {
|
||||
|
|
@ -83,19 +132,28 @@ export async function resolveLLMConfig(overrides?: Partial<LLMConfig>): Promise<
|
|||
baseUrl:
|
||||
overrides?.baseUrl ||
|
||||
process.env.GITNEXUS_LLM_BASE_URL ||
|
||||
savedConfig.baseUrl ||
|
||||
'https://openrouter.ai/api/v1',
|
||||
(reuseSavedHttpConfig ? savedConfig.baseUrl : undefined) ||
|
||||
(savedProvider === 'minimax'
|
||||
? MINIMAX_OPENAI_BASE_URLS.global_en
|
||||
: 'https://openrouter.ai/api/v1'),
|
||||
model:
|
||||
overrides?.model ||
|
||||
(localProvider ? undefined : process.env.GITNEXUS_MODEL) ||
|
||||
savedLocalModel ||
|
||||
(localProvider ? '' : savedConfig.model || 'minimax/minimax-m2.5'),
|
||||
(localProvider
|
||||
? ''
|
||||
: (reuseSavedHttpConfig ? savedConfig.model : undefined) ||
|
||||
(savedProvider === 'minimax' ? MINIMAX_MODEL_IDS[0] : '')),
|
||||
maxTokens: overrides?.maxTokens ?? 16_384,
|
||||
temperature: overrides?.temperature ?? 0,
|
||||
provider: savedProvider ?? 'openai',
|
||||
provider: savedProvider,
|
||||
apiVersion:
|
||||
overrides?.apiVersion || process.env.GITNEXUS_AZURE_API_VERSION || savedConfig.apiVersion,
|
||||
isReasoningModel: overrides?.isReasoningModel ?? savedConfig.isReasoningModel,
|
||||
overrides?.apiVersion ||
|
||||
(savedProvider === 'azure' ? process.env.GITNEXUS_AZURE_API_VERSION : undefined) ||
|
||||
(reuseSavedHttpConfig ? savedConfig.apiVersion : undefined),
|
||||
isReasoningModel:
|
||||
overrides?.isReasoningModel ??
|
||||
(reuseSavedHttpConfig ? savedConfig.isReasoningModel : undefined),
|
||||
allowedInsecureHttpHosts:
|
||||
overrides?.allowedInsecureHttpHosts ??
|
||||
parseLLMAllowedInsecureHttpHosts(process.env[LLM_ALLOW_INSECURE_CONNECTION_ENV]),
|
||||
|
|
@ -252,7 +310,7 @@ export interface CallLLMOptions {
|
|||
* Retries up to 3 times on transient failures (429, 5xx, network errors).
|
||||
*/
|
||||
export async function callLLM(
|
||||
prompt: string,
|
||||
prompt: LLMUserContent,
|
||||
config: LLMConfig,
|
||||
systemPrompt?: string,
|
||||
options?: CallLLMOptions,
|
||||
|
|
@ -260,7 +318,7 @@ export async function callLLM(
|
|||
// Validate base URL before any fetch (CodeQL js/http-to-file-access)
|
||||
validateLLMBaseUrl(config.baseUrl, config.allowedInsecureHttpHosts);
|
||||
|
||||
const messages: Array<{ role: string; content: string }> = [];
|
||||
const messages: Array<{ role: string; content: LLMUserContent }> = [];
|
||||
if (systemPrompt) {
|
||||
messages.push({ role: 'system', content: systemPrompt });
|
||||
}
|
||||
|
|
@ -276,8 +334,15 @@ export async function callLLM(
|
|||
);
|
||||
}
|
||||
|
||||
// Detect reasoning model (o1, o3, o4-mini etc.) or explicit override
|
||||
const reasoning = isReasoningModel(config.model, config.isReasoningModel);
|
||||
const miniMaxThinkingMode =
|
||||
config.provider === 'minimax'
|
||||
? resolveMiniMaxThinkingMode(config.model, config.isReasoningModel)
|
||||
: undefined;
|
||||
|
||||
// Detect reasoning models or explicit provider-specific thinking configuration.
|
||||
const reasoning = miniMaxThinkingMode
|
||||
? miniMaxThinkingMode !== 'disabled'
|
||||
: isReasoningModel(config.model, config.isReasoningModel);
|
||||
|
||||
const url = buildRequestUrl(config.baseUrl, azure ? config.apiVersion : undefined);
|
||||
const useStream = !!options?.onChunk;
|
||||
|
|
@ -288,6 +353,13 @@ export async function callLLM(
|
|||
messages,
|
||||
};
|
||||
|
||||
if (miniMaxThinkingMode === 'adaptive' || miniMaxThinkingMode === 'disabled') {
|
||||
body.thinking = { type: miniMaxThinkingMode };
|
||||
}
|
||||
if (config.provider === 'minimax') {
|
||||
body.reasoning_split = true;
|
||||
}
|
||||
|
||||
// max_tokens is deprecated; use max_completion_tokens for all models
|
||||
body.max_completion_tokens = config.maxTokens;
|
||||
|
||||
|
|
|
|||
|
|
@ -1982,7 +1982,8 @@ export interface CLIConfig {
|
|||
| 'cursor'
|
||||
| 'claude'
|
||||
| 'codex'
|
||||
| 'opencode';
|
||||
| 'opencode'
|
||||
| 'minimax';
|
||||
cursorModel?: string;
|
||||
claudeModel?: string;
|
||||
codexModel?: string;
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ describe('resolveLLMConfig', () => {
|
|||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
|
@ -211,7 +212,7 @@ describe('resolveLLMConfig', () => {
|
|||
vi.doMock('../../src/storage/repo-manager.js', () => ({
|
||||
loadCLIConfig: vi.fn().mockResolvedValue({
|
||||
provider: 'openai',
|
||||
model: 'minimax/minimax-m2.5',
|
||||
model: 'legacy-http-model',
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
@ -226,7 +227,7 @@ describe('resolveLLMConfig', () => {
|
|||
vi.doMock('../../src/storage/repo-manager.js', () => ({
|
||||
loadCLIConfig: vi.fn().mockResolvedValue({
|
||||
provider: 'openai',
|
||||
model: 'minimax/minimax-m2.5',
|
||||
model: 'legacy-http-model',
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
@ -237,7 +238,22 @@ describe('resolveLLMConfig', () => {
|
|||
expect(config.model).toBe('');
|
||||
});
|
||||
|
||||
it('uses default OpenRouter model for openai provider', async () => {
|
||||
it('uses MiniMax global defaults when no provider is configured', async () => {
|
||||
vi.doMock('../../src/storage/repo-manager.js', () => ({
|
||||
loadCLIConfig: vi.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
||||
const { MINIMAX_MODEL_IDS, MINIMAX_OPENAI_BASE_URLS, resolveLLMConfig } =
|
||||
await import('../../src/core/wiki/llm-client.js');
|
||||
const config = await resolveLLMConfig();
|
||||
|
||||
expect(config.provider).toBe('minimax');
|
||||
expect(config.model).toBe(MINIMAX_MODEL_IDS[0]);
|
||||
expect(config.baseUrl).toBe(MINIMAX_OPENAI_BASE_URLS.global_en);
|
||||
});
|
||||
|
||||
it('uses the MiniMax-specific API key environment variable', async () => {
|
||||
vi.stubEnv('MINIMAX_API_KEY', 'minimax-env-key');
|
||||
vi.doMock('../../src/storage/repo-manager.js', () => ({
|
||||
loadCLIConfig: vi.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
|
@ -245,9 +261,43 @@ describe('resolveLLMConfig', () => {
|
|||
const { resolveLLMConfig } = await import('../../src/core/wiki/llm-client.js');
|
||||
const config = await resolveLLMConfig();
|
||||
|
||||
expect(config.apiKey).toBe('minimax-env-key');
|
||||
});
|
||||
|
||||
it('preserves the configured China endpoint', async () => {
|
||||
const { MINIMAX_MODEL_IDS, MINIMAX_OPENAI_BASE_URLS } =
|
||||
await import('../../src/core/wiki/llm-client.js');
|
||||
vi.doMock('../../src/storage/repo-manager.js', () => ({
|
||||
loadCLIConfig: vi.fn().mockResolvedValue({
|
||||
provider: 'minimax',
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
baseUrl: MINIMAX_OPENAI_BASE_URLS.cn_zh,
|
||||
}),
|
||||
}));
|
||||
|
||||
const { resolveLLMConfig } = await import('../../src/core/wiki/llm-client.js');
|
||||
const config = await resolveLLMConfig();
|
||||
|
||||
expect(config.provider).toBe('minimax');
|
||||
expect(config.model).toBe(MINIMAX_MODEL_IDS[0]);
|
||||
expect(config.baseUrl).toBe(MINIMAX_OPENAI_BASE_URLS.cn_zh);
|
||||
});
|
||||
|
||||
it('preserves providerless HTTP configs created by earlier versions', async () => {
|
||||
vi.doMock('../../src/storage/repo-manager.js', () => ({
|
||||
loadCLIConfig: vi.fn().mockResolvedValue({
|
||||
apiKey: 'legacy-http-key',
|
||||
model: 'legacy-http-model',
|
||||
baseUrl: 'https://legacy.example/v1',
|
||||
}),
|
||||
}));
|
||||
|
||||
const { resolveLLMConfig } = await import('../../src/core/wiki/llm-client.js');
|
||||
const config = await resolveLLMConfig();
|
||||
|
||||
expect(config.provider).toBe('openai');
|
||||
expect(config.model).toBe('minimax/minimax-m2.5');
|
||||
expect(config.baseUrl).toBe('https://openrouter.ai/api/v1');
|
||||
expect(config.model).toBe('legacy-http-model');
|
||||
expect(config.baseUrl).toBe('https://legacy.example/v1');
|
||||
});
|
||||
|
||||
it('CLI overrides take priority over saved config', async () => {
|
||||
|
|
@ -270,6 +320,146 @@ describe('resolveLLMConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('wikiCommand provider switch persistence', () => {
|
||||
const originalExitCode = process.exitCode;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
process.exitCode = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.doUnmock('../../src/storage/git.js');
|
||||
vi.doUnmock('../../src/storage/repo-manager.js');
|
||||
vi.doUnmock('../../src/core/wiki/llm-client.js');
|
||||
vi.doUnmock('../../src/core/wiki/generator.js');
|
||||
vi.doUnmock('cli-progress');
|
||||
process.exitCode = originalExitCode;
|
||||
});
|
||||
|
||||
async function saveProviderSwitch(
|
||||
existing: Record<string, unknown>,
|
||||
options: Record<string, unknown>,
|
||||
) {
|
||||
const saveCLIConfig = vi.fn();
|
||||
|
||||
vi.doMock('../../src/storage/git.js', () => ({
|
||||
getGitRoot: vi.fn(),
|
||||
isGitRepo: vi.fn().mockReturnValue(true),
|
||||
}));
|
||||
vi.doMock('../../src/storage/repo-manager.js', () => ({
|
||||
getStoragePaths: vi
|
||||
.fn()
|
||||
.mockReturnValue({ storagePath: '/tmp/wiki-storage', lbugPath: '/tmp/wiki-db' }),
|
||||
loadMeta: vi.fn().mockResolvedValue({ createdAt: '2026-01-01T00:00:00Z' }),
|
||||
loadCLIConfig: vi.fn().mockResolvedValue(existing),
|
||||
saveCLIConfig,
|
||||
}));
|
||||
vi.doMock('../../src/core/wiki/llm-client.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../src/core/wiki/llm-client.js')>();
|
||||
return {
|
||||
...actual,
|
||||
resolveLLMConfig: vi.fn().mockResolvedValue({
|
||||
apiKey: options.apiKey ?? '',
|
||||
baseUrl: options.baseUrl ?? '',
|
||||
model: options.model ?? '',
|
||||
maxTokens: 16_384,
|
||||
temperature: 0,
|
||||
provider: options.provider,
|
||||
apiVersion: options.apiVersion,
|
||||
isReasoningModel: options.reasoningModel,
|
||||
}),
|
||||
};
|
||||
});
|
||||
vi.doMock('../../src/core/wiki/generator.js', () => ({
|
||||
WikiGenerator: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
run: vi.fn().mockResolvedValue({ mode: 'up-to-date', pagesGenerated: 0 }),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
vi.doMock('cli-progress', () => ({
|
||||
default: {
|
||||
SingleBar: vi.fn(function () {
|
||||
return {
|
||||
start: vi.fn(),
|
||||
update: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
};
|
||||
}),
|
||||
Presets: { shades_grey: {} },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const { wikiCommand } = await import('../../src/cli/wiki.js');
|
||||
await wikiCommand('/tmp/repo', options as Parameters<typeof wikiCommand>[1]);
|
||||
|
||||
return saveCLIConfig;
|
||||
}
|
||||
|
||||
it('preserves explicit OpenAI settings when switching away from MiniMax', async () => {
|
||||
const saveCLIConfig = await saveProviderSwitch(
|
||||
{
|
||||
provider: 'minimax',
|
||||
apiKey: 'old-minimax-key',
|
||||
baseUrl: 'https://api.minimax.io/v1',
|
||||
model: 'MiniMax-M3',
|
||||
apiVersion: 'old-version',
|
||||
isReasoningModel: false,
|
||||
},
|
||||
{
|
||||
provider: 'openai',
|
||||
apiKey: 'new-openai-key',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-5',
|
||||
apiVersion: 'v1',
|
||||
reasoningModel: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(saveCLIConfig).toHaveBeenCalledWith({
|
||||
provider: 'openai',
|
||||
apiKey: 'new-openai-key',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-5',
|
||||
apiVersion: 'v1',
|
||||
isReasoningModel: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves explicit MiniMax settings when switching from OpenAI', async () => {
|
||||
const saveCLIConfig = await saveProviderSwitch(
|
||||
{
|
||||
provider: 'openai',
|
||||
apiKey: 'old-openai-key',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4o',
|
||||
apiVersion: 'old-version',
|
||||
isReasoningModel: true,
|
||||
},
|
||||
{
|
||||
provider: 'minimax',
|
||||
apiKey: 'new-minimax-key',
|
||||
baseUrl: 'https://api.minimaxi.com/v1',
|
||||
model: 'MiniMax-M2.7',
|
||||
apiVersion: 'v2',
|
||||
reasoningModel: false,
|
||||
},
|
||||
);
|
||||
|
||||
expect(saveCLIConfig).toHaveBeenCalledWith({
|
||||
provider: 'minimax',
|
||||
apiKey: 'new-minimax-key',
|
||||
baseUrl: 'https://api.minimaxi.com/v1',
|
||||
model: 'MiniMax-M2.7',
|
||||
apiVersion: 'v2',
|
||||
isReasoningModel: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── --verbose flag ──────────────────────────────────────────────────
|
||||
|
||||
describe('--verbose flag', () => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { describe, it, expect, vi, afterEach } from 'vitest';
|
|||
// Import the function we'll add in the next step
|
||||
import {
|
||||
LLM_ALLOW_INSECURE_CONNECTION_ENV,
|
||||
MINIMAX_MODEL_IDS,
|
||||
MINIMAX_OPENAI_BASE_URLS,
|
||||
isAzureProvider,
|
||||
isReasoningModel,
|
||||
buildRequestUrl,
|
||||
|
|
@ -57,7 +59,7 @@ describe('isReasoningModel', () => {
|
|||
});
|
||||
|
||||
it('returns false for minimax', () => {
|
||||
expect(isReasoningModel('minimax/minimax-m2.5')).toBe(false);
|
||||
expect(isReasoningModel(MINIMAX_MODEL_IDS[1])).toBe(false);
|
||||
});
|
||||
|
||||
it('respects explicit override', () => {
|
||||
|
|
@ -94,6 +96,40 @@ describe('buildRequestUrl', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('resolveLLMConfig provider isolation', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('does not use OpenAI environment credentials for the default MiniMax provider', async () => {
|
||||
vi.stubEnv('OPENAI_API_KEY', 'openai-key');
|
||||
vi.stubEnv('GITNEXUS_API_KEY', 'gitnexus-key');
|
||||
vi.stubEnv('MINIMAX_API_KEY', '');
|
||||
|
||||
const config = await resolveLLMConfig();
|
||||
|
||||
expect(config.provider).toBe('minimax');
|
||||
expect(config.apiKey).toBe('');
|
||||
});
|
||||
|
||||
it('does not reuse saved credentials or API versions after switching providers', async () => {
|
||||
vi.spyOn(await import('../../src/storage/repo-manager.js'), 'loadCLIConfig').mockResolvedValue({
|
||||
provider: 'minimax',
|
||||
apiKey: 'minimax-key',
|
||||
baseUrl: MINIMAX_OPENAI_BASE_URLS.global_en,
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
apiVersion: 'minimax-version',
|
||||
});
|
||||
|
||||
const config = await resolveLLMConfig({ provider: 'openai' });
|
||||
|
||||
expect(config.apiKey).toBe('');
|
||||
expect(config.apiVersion).toBeUndefined();
|
||||
expect(config.baseUrl).toBe('https://openrouter.ai/api/v1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('callLLM — auth header', () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
|
|
@ -240,6 +276,113 @@ describe('callLLM — reasoning model params', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('callLLM — MiniMax request params', () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
const createFetchSpy = () =>
|
||||
vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ choices: [{ message: { content: 'answer' } }], usage: {} }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
|
||||
it('uses the China endpoint with adaptive thinking by default', async () => {
|
||||
const fetchSpy = createFetchSpy();
|
||||
vi.stubGlobal('fetch', fetchSpy);
|
||||
|
||||
const { callLLM } = await import('../../src/core/wiki/llm-client.js');
|
||||
await callLLM('test', {
|
||||
apiKey: 'minimax-test-key',
|
||||
baseUrl: MINIMAX_OPENAI_BASE_URLS.cn_zh,
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
maxTokens: 500,
|
||||
temperature: 0.5,
|
||||
provider: 'minimax',
|
||||
});
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
||||
const body = JSON.parse(init.body as string);
|
||||
expect(url).toBe(`${MINIMAX_OPENAI_BASE_URLS.cn_zh}/chat/completions`);
|
||||
expect(body.thinking).toEqual({ type: 'adaptive' });
|
||||
expect(body.reasoning_split).toBe(true);
|
||||
expect(body.temperature).toBeUndefined();
|
||||
});
|
||||
|
||||
it('supports disabled thinking', async () => {
|
||||
const fetchSpy = createFetchSpy();
|
||||
vi.stubGlobal('fetch', fetchSpy);
|
||||
|
||||
const { callLLM } = await import('../../src/core/wiki/llm-client.js');
|
||||
await callLLM('test', {
|
||||
apiKey: 'minimax-test-key',
|
||||
baseUrl: MINIMAX_OPENAI_BASE_URLS.global_en,
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
maxTokens: 500,
|
||||
temperature: 0.5,
|
||||
provider: 'minimax',
|
||||
isReasoningModel: false,
|
||||
});
|
||||
|
||||
const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
||||
const body = JSON.parse(init.body as string);
|
||||
expect(body.thinking).toEqual({ type: 'disabled' });
|
||||
expect(body.temperature).toBe(0.5);
|
||||
});
|
||||
|
||||
it('leaves always-on thinking implicit', async () => {
|
||||
const fetchSpy = createFetchSpy();
|
||||
vi.stubGlobal('fetch', fetchSpy);
|
||||
|
||||
const { callLLM } = await import('../../src/core/wiki/llm-client.js');
|
||||
await callLLM('test', {
|
||||
apiKey: 'minimax-test-key',
|
||||
baseUrl: MINIMAX_OPENAI_BASE_URLS.global_en,
|
||||
model: MINIMAX_MODEL_IDS[1],
|
||||
maxTokens: 500,
|
||||
temperature: 0.5,
|
||||
provider: 'minimax',
|
||||
isReasoningModel: false,
|
||||
});
|
||||
|
||||
const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
||||
const body = JSON.parse(init.body as string);
|
||||
expect(body.thinking).toBeUndefined();
|
||||
expect(body.reasoning_split).toBe(true);
|
||||
expect(body.temperature).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves image and video content parts', async () => {
|
||||
const fetchSpy = createFetchSpy();
|
||||
vi.stubGlobal('fetch', fetchSpy);
|
||||
const prompt = [
|
||||
{ type: 'text' as const, text: 'Compare these inputs.' },
|
||||
{
|
||||
type: 'image_url' as const,
|
||||
image_url: { url: 'https://example.com/image.png', detail: 'high' as const },
|
||||
},
|
||||
{
|
||||
type: 'video_url' as const,
|
||||
video_url: { url: 'https://example.com/video.mp4', fps: 1 },
|
||||
},
|
||||
];
|
||||
|
||||
const { callLLM } = await import('../../src/core/wiki/llm-client.js');
|
||||
await callLLM(prompt, {
|
||||
apiKey: 'minimax-test-key',
|
||||
baseUrl: MINIMAX_OPENAI_BASE_URLS.global_en,
|
||||
model: MINIMAX_MODEL_IDS[0],
|
||||
maxTokens: 500,
|
||||
temperature: 0.5,
|
||||
provider: 'minimax',
|
||||
});
|
||||
|
||||
const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
||||
const body = JSON.parse(init.body as string);
|
||||
expect(body.messages).toEqual([{ role: 'user', content: prompt }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('callLLM — timeout handling', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue