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
Binary file added assets/kimi.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion cli/proxima-cli.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ function startSpinner(text) {

const FALLBACK_INFO = {
mode: 'session',
providers: ['chatgpt', 'claude', 'gemini', 'perplexity'],
providers: ['chatgpt', 'claude', 'gemini', 'perplexity', 'kimi'],
models: {},
firstProvider: 'claude'
};
Expand Down
2 changes: 1 addition & 1 deletion electron/api/byok/keys.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const ENC_VERSION = 1;
let _storeCache = null;

const KNOWN_PROVIDERS = Object.freeze([
'chatgpt', 'claude', 'gemini', 'perplexity',
'chatgpt', 'claude', 'gemini', 'perplexity', 'kimi',
'deepseek', 'groq', 'xai', 'openrouter', 'together', 'fireworks', 'mistral', 'nvidia',
]);

Expand Down
4 changes: 4 additions & 0 deletions electron/api/byok/model-fetcher.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ const MODELS_CONFIG = Object.freeze({
'sonar-deep-research', 'r1-1776',
],
},
kimi: {
url: 'https://api.moonshot.cn/v1/models',
auth: 'bearer',
},
deepseek: {
url: 'https://api.deepseek.com/v1/models',
auth: 'bearer',
Expand Down
4 changes: 4 additions & 0 deletions electron/api/byok/models.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const FALLBACK_MODELS = Object.freeze({
claude: ['claude-sonnet-5', 'claude-3-5-sonnet-latest', 'claude-3-haiku-20240307'],
gemini: ['gemini-3.5-flash', 'gemini-3.1-pro', 'gemini-2.0-flash-lite'],
perplexity: ['sonar-pro', 'sonar'],
kimi: ['moonshot-v1-32k', 'moonshot-v1-8k'],
deepseek: ['deepseek-chat'],
groq: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'],
xai: ['grok-2-1212', 'grok-beta'],
Expand All @@ -23,6 +24,7 @@ const DEFAULT_MODELS = Object.freeze({
claude: FALLBACK_MODELS.claude[0],
gemini: FALLBACK_MODELS.gemini[0],
perplexity: FALLBACK_MODELS.perplexity[0],
kimi: FALLBACK_MODELS.kimi[0],
deepseek: FALLBACK_MODELS.deepseek[0],
groq: FALLBACK_MODELS.groq[0],
xai: FALLBACK_MODELS.xai[0],
Expand All @@ -44,6 +46,7 @@ const API_ENDPOINTS = Object.freeze({
claude: 'https://api.anthropic.com/v1/messages',
google: 'https://generativelanguage.googleapis.com/v1beta/models',
perplexity: 'https://api.perplexity.ai/chat/completions',
kimi: 'https://api.moonshot.cn/v1/chat/completions',
deepseek: 'https://api.deepseek.com/v1/chat/completions',
groq: 'https://api.groq.com/openai/v1/chat/completions',
xai: 'https://api.x.ai/v1/chat/completions',
Expand All @@ -61,6 +64,7 @@ const MAX_TOKENS = Object.freeze({
claude: 4096,
gemini: 8192,
perplexity: 4096,
kimi: 8192,
deepseek: 4096,
groq: 4096,
xai: 4096,
Expand Down
1 change: 1 addition & 0 deletions electron/api/byok/router.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const CALLERS = Object.freeze({
claude: anthropic,
gemini: google,
perplexity: perplexity,
kimi: compatible,
deepseek: compatible,
groq: compatible,
xai: compatible,
Expand Down
2 changes: 1 addition & 1 deletion electron/api/pages/widget.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function getChatJS() {
} catch (e) {
console.warn('[Widget] Could not fetch models, using defaults:', e.message);
_gatewayMode = 'session';
_enabledProviders = ['chatgpt', 'claude', 'gemini', 'perplexity'];
_enabledProviders = ['chatgpt', 'claude', 'gemini', 'perplexity', 'kimi'];
}
populateDropdown();
populateBattle();
Expand Down
7 changes: 4 additions & 3 deletions electron/api/rest-api.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const MODEL_ALIASES = {
'gemini': 'gemini', 'gemini-pro': 'gemini', 'gemini-2': 'gemini', 'gemini-2.5': 'gemini',
'google': 'gemini', 'bard': 'gemini',
'perplexity': 'perplexity', 'pplx': 'perplexity', 'sonar': 'perplexity',
'kimi': 'kimi', 'moonshot': 'kimi', 'moonshotai': 'kimi',
'auto': 'auto', 'all': 'all'
};

Expand Down Expand Up @@ -342,7 +343,7 @@ function pickBestProvider(preferred) {
return enabled.includes(base) ? base : null;
}

const priorityList = ['claude', 'chatgpt', 'gemini', 'perplexity', 'deepseek', 'groq', 'xai', 'openrouter', 'together', 'fireworks', 'mistral', 'nvidia'];
const priorityList = ['claude', 'chatgpt', 'gemini', 'perplexity', 'kimi', 'deepseek', 'groq', 'xai', 'openrouter', 'together', 'fireworks', 'mistral', 'nvidia'];
const priority = priorityList.filter(p => enabled.includes(p));
if (priority.length === 0) {
return enabled.length > 0 ? enabled[0] : null;
Expand Down Expand Up @@ -566,15 +567,15 @@ function startRestAPI() {
}

const url = new URL(req.url, `http://localhost:${REST_PORT}`);
const publicPaths = ['/', '/docs', '/cli', '/ws', '/websocket', '/openapi.json', '/v1/openapi.json', '/api-key', '/v1/byok/models', '/v1/models', '/v1/stats', '/v1/functions'];
const publicPaths = ['/', '/docs', '/cli', '/ws', '/websocket', '/openapi.json', '/v1/openapi.json', '/api-key', '/v1/byok/models', '/v1/models', '/v1/stats', '/v1/functions', '/v1/kimi/session'];
if (!publicPaths.includes(url.pathname) && !validateApiKey(req)) {
Comment on lines 569 to 571

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Kimi session exposed unauthenticated 🐞 Bug ⛨ Security

/v1/kimi/session is added to REST publicPaths, making it accessible without an API key; it
returns internal session identifiers (chatId/parentId/resetCount). This can leak session state to
any local process that can reach the loopback server.
Agent Prompt
### Issue description
The new Kimi session-status endpoint is publicly accessible (no API key required) and returns session identifiers.

### Issue Context
Even though the server binds to 127.0.0.1, the API key mechanism is the main barrier against untrusted local callers; adding a public endpoint bypasses it.

### Fix Focus Areas
- electron/api/rest-api.cjs[569-572]
- electron/api/routes.cjs[499-510]

### What to change
- Prefer requiring API key for `/v1/kimi/session`.
- If the plugin must access it without a key, reduce returned data (e.g., return only `resetCount`) and/or require a per-install secret in a header/query param.
- Consider aligning header parsing with the rest of the API (`x-proxima-conversation`) and documenting the auth model.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

return sendError(res, 401, 'Invalid or missing API key', 'authentication_error');
}
try {
const body = req.method === 'POST' ? await parseBody(req) : {};
const providerKey = (req.headers['x-provider-key'] || '').trim();
if (providerKey) body._byokKey = providerKey;
await handleRoute(req.method, url.pathname, body, res);
await handleRoute(req.method, url.pathname, body, res, req.headers, url.searchParams);
} catch (err) { console.error('[API] Error:', err.message); if (!res.headersSent) sendError(res, err.statusCode || 500, err.message); }
});

Expand Down
46 changes: 37 additions & 9 deletions electron/api/routes.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ function createRouteHandler(deps) {
byok,
} = deps;

return async function handleRoute(method, pathname, body, res) {
return async function handleRoute(method, pathname, body, res, headers, searchParams) {
const handleMCPRequest = getHandler();
const conversationId = (body && (body.conversationId || body.conversation_id || body.sessionId || body.session_id)) || null;
const conversationId = (body && (body.conversationId || body.conversation_id || body.sessionId || body.session_id))
|| (headers && (headers['x-proxima-conversation'] || headers['X-Proxima-Conversation']))
|| null;

if (method === 'POST' && pathname === `${API_PREFIX}/chat/completions`) {
const fn = (body.function || '').toLowerCase().trim();
Expand Down Expand Up @@ -253,10 +255,17 @@ function createRouteHandler(deps) {
else sendJSON(res, 200, formatChatResponse(result, provider));
}
} else {
const toolPrompt = buildToolCallingPrompt(body);
if (!toolPrompt) return sendError(res, 400, 'No message provided');
// Kimi's engine needs the raw messages array (it reads the
// system prompt to detect a NEW chat and only forwards the
// latest user turn on follow-ups). Flattening to a string
// via buildToolCallingPrompt would lose the system prompt
// and make the engine reuse a stale/too-long chat_id.
const inputForEngine = provider === 'kimi'
? (Array.isArray(body.messages) ? body.messages : message)
: buildToolCallingPrompt(body);
if (!inputForEngine) return sendError(res, 400, 'No message provided');
if (TOOL_DEBUG) console.log('[TOOL-CALL/Legacy] Provider:', provider, '| Tools:', body.tools.length);
const result = await queryProvider(provider, toolPrompt, filePath, gemini, null, conversationId, body._byokKey, null, resolved.byokModelId);
const result = await queryProvider(provider, inputForEngine, filePath, gemini, null, conversationId, body._byokKey, null, resolved.byokModelId);
Comment on lines +266 to +268

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Rest ignores think flag 🐞 Bug ≡ Correctness

REST /v1/chat/completions session-provider calls never forward a request’s think flag to the MCP
sendMessage action, so REST clients cannot disable ChatGPT thinking mode (it will default to
enabled). This breaks the “think flag end-to-end” behavior for API consumers.
Agent Prompt
### Issue description
REST routes call `queryProvider()` for session-mode providers, but the underlying `handleMCPRequest({action:'sendMessage'})` payload omits `think`. Since the main process defaults `thinkMode = data.think !== false`, REST calls effectively force think=true.

### Issue Context
- MCP tools already support `think`.
- WS server supports `think`.
- REST should support it too for API consumers.

### Fix Focus Areas
- electron/api/routes.cjs[24-110]
- electron/api/rest-api.cjs[363-414]
- electron/main-v2.cjs[542-548]

### What to change
- Decide where `think` should be parsed from REST requests (e.g., `body.think` for OpenAI-compatible calls).
- Extend `queryProvider()` in `rest-api.cjs` to accept a `think` argument and include it in the `handleMCPRequest` data.
- Update route handler call sites to pass `think` through for session providers (especially ChatGPT).
- Add tests covering `think:false` via REST.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

if (TOOL_DEBUG) console.log('[TOOL-CALL/Legacy] Response text (first 300):', JSON.stringify(result.text?.slice(0, 300)));
const parsed = parseToolCallResponse(result.text);
if (TOOL_DEBUG) console.log('[TOOL-CALL/Legacy] Parsed:', parsed.isToolCall, '| toolCalls:', parsed.toolCalls?.length || 0);
Expand All @@ -283,7 +292,12 @@ function createRouteHandler(deps) {
if (resolved.mode === 'single') {
const provider = resolved.providers[0];
const isByokActive = !!(body._byokKey || (byok.keys.isEnabled() ? byok.keys.getKey(provider) : null));
const inputMessage = isByokActive ? (body.messages || message) : message;
// Kimi's engine needs the full messages array so it can send the
// system prompt + context once at chat start and only the new
// user turn afterwards (parent_id chain carries the rest).
const inputMessage = isByokActive
? (body.messages || message)
: (provider === 'kimi' && Array.isArray(body.messages) ? body.messages : message);

let onChunk = null;
let streamedAny = false;
Expand Down Expand Up @@ -373,7 +387,7 @@ function createRouteHandler(deps) {
aliases: Object.entries(MODEL_ALIASES).filter(([_, v]) => v === p).map(([k]) => k).filter(k => k !== p),
});
}
['chatgpt', 'claude', 'gemini', 'perplexity'].filter(p => !enabled.includes(p)).forEach(p =>
['chatgpt', 'claude', 'gemini', 'perplexity', 'kimi'].filter(p => !enabled.includes(p)).forEach(p =>
models.push({ id: p, object: 'model', owned_by: 'proxima', status: 'disabled' })
);
}
Expand All @@ -395,7 +409,7 @@ function createRouteHandler(deps) {
function: null,
name: 'chat',
description: 'Default chat completion. Omit the "function" field to chat with a model.',
fields: { model: 'auto|claude|chatgpt|gemini|perplexity|all (or array of models)', message: 'string (or use messages[])', stream: 'boolean (optional)', filePath: 'local file path for multimodal/Gemini routing (optional)' }
fields: { model: 'auto|claude|chatgpt|gemini|perplexity|kimi|all (or array of models)', message: 'string (or use messages[])', stream: 'boolean (optional)', filePath: 'local file path for multimodal/Gemini routing (optional)' }
},
{
function: 'search',
Expand Down Expand Up @@ -472,7 +486,7 @@ function createRouteHandler(deps) {
if (method === 'POST' && pathname === `${API_PREFIX}/conversations/new`) {
const requested = (body && (body.provider || body.model)) || null;
if (!requested) {
return sendError(res, 400, 'A "provider" (or "model") is required: chatgpt, claude, gemini, or perplexity.');
return sendError(res, 400, 'A "provider" (or "model") is required: chatgpt, claude, gemini, perplexity, or kimi.');
}
const target = pickBestProvider(requested);
if (!target) {
Expand All @@ -482,6 +496,20 @@ function createRouteHandler(deps) {
catch (e) { return sendError(res, 500, e.message); }
}

if (method === 'GET' && pathname === `${API_PREFIX}/kimi/session`) {
// Session-status endpoint for the opencode plugin. The plugin polls this
// before each request; when resetCount changes, it knows Kimi rolled to
// a NEW chat_id and must re-send the system prompt + handoff summary.
const cid = (searchParams && (searchParams.get('conversationId') || searchParams.get('conversation_id') || searchParams.get('sessionId')))
|| (headers && headers['x-proxima-conversation'])
|| null;
try {
const r = await handleMCPRequest({ action: 'getKimiSessionStatus', provider: 'kimi', data: { conversationId: cid } });
if (!r || r.success === false) return sendJSON(res, 200, { success: false, resetCount: 0, chatId: null, parentId: null, error: (r && r.error) || 'unavailable' });
return sendJSON(res, 200, { success: true, resetCount: r.resetCount || 0, chatId: r.chatId || null, parentId: r.parentId || null });
} catch (e) { return sendJSON(res, 200, { success: false, resetCount: 0, chatId: null, parentId: null, error: e.message }); }
}

if (method === 'GET' && (pathname === `${API_PREFIX}/openapi.json` || pathname === '/openapi.json')) {
try { return sendJSON(res, 200, JSON.parse(require('fs').readFileSync(require('path').join(__dirname, '..', '..', 'docs', 'openapi.json'), 'utf8'))); }
catch (e) { return sendError(res, 500, 'OpenAPI spec not found'); }
Expand Down
11 changes: 6 additions & 5 deletions electron/api/ws-server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ function closeWebSocket() {
}
}

async function queryProvider(provider, message, filePath = null, engine = null, conversationId = null, modelId = null) {
async function queryProvider(provider, message, filePath = null, engine = null, conversationId = null, modelId = null, think = true) {
const byokKey = byok.keys.isEnabled() ? byok.keys.getKey(provider) : null;
if (byokKey) {
const messages = [{ role: 'user', content: message }];
Expand All @@ -167,7 +167,7 @@ async function queryProvider(provider, message, filePath = null, engine = null,
const sendResult = await handleMCPRequest({
action: 'sendMessage',
provider,
data: { message, filePath, engine, conversationId }
data: { message, filePath, engine, conversationId, think }
});

if (!sendResult.success) {
Expand Down Expand Up @@ -199,7 +199,7 @@ function pickBestProvider(preferred) {
if (found) return found;
return null;
}
const priorityList = ['claude', 'chatgpt', 'gemini', 'perplexity', 'deepseek', 'groq', 'xai', 'openrouter', 'together', 'fireworks', 'mistral', 'nvidia'];
const priorityList = ['claude', 'chatgpt', 'gemini', 'perplexity', 'kimi', 'deepseek', 'groq', 'xai', 'openrouter', 'together', 'fireworks', 'mistral', 'nvidia'];
const priority = priorityList.find(p => enabled.includes(p));
if (priority) return priority;
return enabled.length > 0 ? enabled[0] : null;
Expand Down Expand Up @@ -265,9 +265,10 @@ async function handleWSMessage(ws, clientId, msg) {
sendJSON(ws, { type: 'status', id: requestId, status: 'processing', model: provider, timestamp: new Date().toISOString() });

const conversationId = msg.conversationId || msg.conversation_id || msg.sessionId || msg.session_id || null;
const think = msg.think !== false;
const startTime = Date.now();
try {
const content = await queryProvider(provider, message, filePath, engine, conversationId, byokModelId);
const content = await queryProvider(provider, message, filePath, engine, conversationId, byokModelId, think);
const responseTimeMs = Date.now() - startTime;

sendJSON(ws, {
Expand Down Expand Up @@ -546,7 +547,7 @@ async function handleWSMessage(ws, clientId, msg) {
case 'reset': {
const requested = msg.model || null;
if (!requested) {
sendJSON(ws, { type: 'error', id: requestId, error: 'A "model" (provider) is required to reset: chatgpt, claude, gemini, or perplexity.', timestamp: new Date().toISOString() });
sendJSON(ws, { type: 'error', id: requestId, error: 'A "model" (provider) is required to reset: chatgpt, claude, gemini, perplexity, or kimi.', timestamp: new Date().toISOString() });
break;
}
const target = pickBestProvider(requested);
Expand Down
Loading