Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ App-owned runtimes, provider settings, chat history, attachments, caches, and lo
| Feature | Details |
|---|---|
| **Official Claude Code** | Uses Anthropic's official CLI and Agent SDK — no replacement agent engine |
| **9 AI Providers** | NVIDIA NIM · DeepSeek · OpenRouter · Google Gemini · Anthropic · OpenAI · Ollama · LM Studio · Custom API |
| **10 AI Providers** | NVIDIA NIM · DeepSeek · MiniMax · OpenRouter · Google Gemini · Anthropic · OpenAI · Ollama · LM Studio · Custom API |
| **Portable Runtime** | Node.js, Claude Code, caches, settings, and app data stay in the project folder |
| **Studio Dashboard** | Streaming chat, Markdown, code highlighting, compact tool steps, approvals, attachments, and diagnostics |
| **Shared Claude History** | GUI and portable terminal sessions use real Claude Code conversation IDs and can resume the same history |
Expand Down Expand Up @@ -123,6 +123,7 @@ Pressing Enter selects the dashboard.
|---|---|---|
| **NVIDIA NIM** | Chat Completions adapter | [build.nvidia.com](https://build.nvidia.com) |
| **DeepSeek** | Anthropic-compatible API | [platform.deepseek.com](https://platform.deepseek.com) |
| **MiniMax** | Chat Completions adapter | [platform.minimax.io](https://platform.minimax.io) |
| **OpenRouter** | Anthropic-compatible API | [openrouter.ai](https://openrouter.ai) |
| **Google Gemini** | Chat Completions adapter | [aistudio.google.com](https://aistudio.google.com) |
| **Anthropic Claude** | Native Messages API / terminal login | [console.anthropic.com](https://console.anthropic.com) |
Expand All @@ -133,6 +134,14 @@ Pressing Enter selects the dashboard.

> **Non-Claude models are experimental and are not supported by Anthropic.** Tool calling, images, context limits, resume behavior, and other Claude Code features depend on the selected model and provider.

### MiniMax

The MiniMax preset defaults to **MiniMax-M3.1-Flash** and offers **MiniMax-M3** as the second model. Both have a 1,000,000-token context window. Existing saved models and manually entered IDs remain unchanged. Use `https://api.minimax.io/v1` globally or `https://api.minimaxi.com/v1` in China with the matching API key.

The built-in Chat Completions adapter uses adaptive thinking by default for these models. Flash always thinks and defaults to `max` reasoning effort; its supported effort values are `low`, `medium`, `high`, `xhigh`, and `max`. M3 also accepts disabled thinking and ignores effort. Flash does not accept `service_tier` or `reasoning_split=false`. The dashboard preserves its existing image attachment support; video input is not implemented by this adapter.

See MiniMax's [model documentation](https://platform.minimax.io/docs/guides/models-intro.md) and [Chat Completions API](https://platform.minimax.io/docs/api-reference/text-chat-openai.md).

## Custom OpenAI-Compatible Provider

OpenAI-compatible providers can use:
Expand Down
6 changes: 3 additions & 3 deletions dashboard/app.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 19 additions & 3 deletions lib/adapter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ function toolResultContent(block) {
for(const part of block.content){if(part.type==='text')text.push(part.text);else if(part.type==='image')images.push(imageContent(part));else throw new Error(`Unsupported content: ${part.type}`);}
return {text:text.join('\n'),images};
}
export function translateRequest(body, model, {toolMetadata}={}) {
const MINIMAX_FLASH='MiniMax-M3.1-Flash';
const MINIMAX_M3='MiniMax-M3';

export function translateRequest(body, model, {toolMetadata, provider}={}) {
if (!Array.isArray(body.messages)) throw new Error('messages must be an array');
if (body.thinking && body.thinking.type !== 'disabled') throw new Error('Extended thinking is unavailable through the Chat Completions adapter. Disable thinking for this provider.');
const minimax = provider === 'minimax' && [MINIMAX_FLASH, MINIMAX_M3].includes(model);
if (minimax) {
if (body.thinking && !['adaptive','disabled'].includes(body.thinking.type)) throw new Error('MiniMax thinking must be adaptive or disabled');
if (model === MINIMAX_FLASH && body.thinking?.type === 'disabled') throw new Error('MiniMax-M3.1-Flash thinking cannot be disabled');
if (model === MINIMAX_FLASH && (body.service_tier !== undefined || body.reasoning_split === false)) throw new Error('MiniMax-M3.1-Flash does not support service_tier or reasoning_split=false');
} else if (body.thinking && body.thinking.type !== 'disabled') throw new Error('Extended thinking is unavailable through the Chat Completions adapter. Disable thinking for this provider.');
const messages = [];
if (body.system) messages.push({ role: 'system', content: textContent(body.system) });
for (const message of body.messages) {
Expand All @@ -47,6 +55,14 @@ export function translateRequest(body, model, {toolMetadata}={}) {
if (content.length || calls.length) messages.push({ role: message.role, content: content.length ? content.every(b=>b.type==='text') ? content.map(b=>b.text).join('\n') : content : null, ...(calls.length ? { tool_calls: calls } : {}) });
}
const request = { model, messages, stream: !!body.stream };
if (minimax) {
request.thinking = { type: body.thinking?.type || 'adaptive' };
if (model === MINIMAX_FLASH) {
const effort = body.reasoning_effort ?? body.output_config?.effort ?? 'max';
if (!['low','medium','high','xhigh','max'].includes(effort)) throw new Error('Unsupported MiniMax-M3.1-Flash reasoning effort');
request.reasoning_effort = effort;
}
}
if (body.max_tokens) request.max_tokens = body.max_tokens;
if (body.temperature !== undefined) request.temperature = body.temperature;
if (body.top_p !== undefined) request.top_p = body.top_p;
Expand Down Expand Up @@ -139,7 +155,7 @@ export async function startAdapter(profile, { fetchImpl = fetch, onError = () =>
return json(res, 200, { input_tokens: count, estimated: true });
}
const remember=(id,metadata)=>{if(id&&metadata)toolMetadata.set(id,metadata);};
const request = translateRequest(body, profile.model,{toolMetadata});
const request = translateRequest(body, profile.model,{toolMetadata,provider:profile.provider});
const upstream = await fetchImpl(`${profile.baseUrl.replace(/\/+$/,'')}/chat/completions`, {
method: 'POST', headers: { 'Content-Type': 'application/json', ...(profile.key ? { Authorization: `Bearer ${profile.key}` } : {}) }, body: JSON.stringify(request), redirect: 'error', signal: AbortSignal.any([controller.signal, AbortSignal.timeout(180000)])
});
Expand Down
4 changes: 3 additions & 1 deletion lib/agent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ export class AgentManager {
includePartialMessages:true,settingSources:[],systemPrompt:{type:'preset',preset:'claude_code'},
maxTurns:40,abortController:run.controller,
...(session.sdkSessionId?{resume:session.sdkSessionId}:{}),
...(adapter?{thinking:{type:'disabled'},effort:'low'}:{}),
...(adapter?(profile.provider==='minimax'&&(!profile.adapter||profile.adapter==='builtin')&&['MiniMax-M3.1-Flash','MiniMax-M3'].includes(profile.model)
?{thinking:{type:'adaptive'},...(profile.model==='MiniMax-M3.1-Flash'?{effort:'max'}:{})}
:{thinking:{type:'disabled'},effort:'low'}):{}),
canUseTool:async(name,input,context)=>{
clearResponseTimer();
if(run.cancelled)return {behavior:'deny',message:'Session cancelled'};
Expand Down
2 changes: 2 additions & 0 deletions lib/providers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const PROVIDERS = {
deepseek: { name: 'DeepSeek', short: 'DS', description: 'Direct Anthropic-compatible API', transport: 'anthropic', baseUrl: 'https://api.deepseek.com/anthropic', modelsUrl: 'https://api.deepseek.com/models' },
nvidia: { name: 'NVIDIA NIM', short: 'NV', description: 'Cloud inference · local adapter', transport: 'openai', baseUrl: 'https://integrate.api.nvidia.com/v1' },
gemini: { name: 'Google Gemini', short: 'GE', description: 'Google AI · local adapter', transport: 'openai', baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai' },
minimax: { name: 'MiniMax', short: 'MM', description: 'MiniMax · OpenAI-compatible API', transport: 'openai', baseUrl: 'https://api.minimax.io/v1', modelsUrl: 'https://api.minimax.io/v1/models', defaultModel: 'MiniMax-M3.1-Flash', models: [{ id: 'MiniMax-M3.1-Flash', name: 'MiniMax-M3.1-Flash', contextWindow: 1000000 }, { id: 'MiniMax-M3', name: 'MiniMax-M3', contextWindow: 1000000 }], endpoints: [{ region: 'global_en', baseUrl: 'https://api.minimax.io/v1', anthropicBaseUrl: 'https://api.minimax.io/anthropic', docsRoot: 'https://platform.minimax.io/docs' }, { region: 'cn_zh', baseUrl: 'https://api.minimaxi.com/v1', anthropicBaseUrl: 'https://api.minimaxi.com/anthropic', docsRoot: 'https://platform.minimaxi.com/docs' }] },
openai: { name: 'OpenAI', short: 'OA', description: 'Chat Completions · local adapter', transport: 'openai', baseUrl: 'https://api.openai.com/v1' },
ollama: { name: 'Ollama', short: 'OL', description: 'Local models · offline inference', transport: 'anthropic', baseUrl: 'http://127.0.0.1:11434', local: true },
lmstudio: { name: 'LM Studio', short: 'LM', description: 'Your local model server', transport: 'anthropic', baseUrl: 'http://127.0.0.1:1234', local: true },
Expand Down Expand Up @@ -63,6 +64,7 @@ export async function discoverModels(profile, signal) {
const res = await fetch(providerURL(profile), { headers:providerHeaders(profile), signal:signal||AbortSignal.timeout(15000), redirect:'error' });
if (!res.ok) throw new Error(`Model discovery returned HTTP ${res.status}. Check the endpoint/credential or enter a model manually.`);
const body = await res.json();
if (profile.provider === 'minimax') return p.models.map(model => ({...model}));
return (body.data || body.models || []).map(m => {
const raw=m.context_length??m.context_window??m.max_context_length??m.max_input_tokens??m.inputTokenLimit;
const contextWindow=Number(raw);
Expand Down
4 changes: 2 additions & 2 deletions tests/backend.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const { pickWorkspace }=await import('../lib/workspace-picker.mjs');
const { nativeSessionId }=await import('../lib/native-sessions.mjs');
test.after(()=>rmSync(temp,{recursive:true,force:true}));
const profile={provider:'custom',model:'test-model',auth:'api',baseUrl:'http://127.0.0.1:9099/v1',key:'test-secret-not-real'};
test('all nine providers and safe configuration round trips',()=>{
assert.equal(Object.keys(PROVIDERS).length,9);saveProfile({...profile,contextWindow:128000});
test('all ten providers and safe configuration round trips',()=>{
assert.equal(Object.keys(PROVIDERS).length,10);assert.equal(PROVIDERS.minimax.defaultModel,'MiniMax-M3.1-Flash');assert.equal(PROVIDERS.minimax.baseUrl,'https://api.minimax.io/v1');assert.equal(PROVIDERS.minimax.endpoints[1].baseUrl,'https://api.minimaxi.com/v1');saveProfile({...profile,contextWindow:128000});
assert.equal(readConfig().profiles.custom.contextWindow,128000);assert.throws(()=>saveProfile({...profile,contextWindow:100}),/Context window/);
assert.equal(readConfig().profiles.custom.key,profile.key);assert.equal(publicConfig().profiles.custom.hasKey,true);assert.equal(publicConfig().profiles.custom.key,undefined);
saveProfile({...profile,key:undefined,model:'next'});assert.equal(readConfig().profiles.custom.key,profile.key);saveProfile(profile);
Expand Down
Loading