From 3c0898c987003d9651883b5e7e4704869743d695 Mon Sep 17 00:00:00 2001 From: simplypeace1 <238994025+simplypeace1@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:51:41 +0530 Subject: [PATCH] feat: add Kimi BYOK provider and ChatGPT thinking mode --- assets/kimi.png | Bin 0 -> 9267 bytes cli/proxima-cli.cjs | 2 +- electron/api/byok/keys.cjs | 2 +- electron/api/byok/model-fetcher.cjs | 4 + electron/api/byok/models.cjs | 4 + electron/api/byok/router.cjs | 1 + electron/api/pages/widget.cjs | 2 +- electron/api/rest-api.cjs | 7 +- electron/api/routes.cjs | 46 +- electron/api/ws-server.cjs | 11 +- electron/browser-manager.cjs | 64 +- electron/index-v2.html | 52 +- electron/ipc/core.cjs | 3 +- electron/ipc/settings.cjs | 3 +- electron/main-v2.cjs | 58 +- electron/providers/api.cjs | 21 +- electron/providers/engines/chatgpt-engine.js | 162 ++++- electron/providers/engines/kimi-engine.js | 643 +++++++++++++++++++ electron/providers/sender.cjs | 8 +- src/agentic/smart-router.js | 6 + src/agentic/task-orchestrator.js | 9 + src/config/defaults.js | 4 +- src/cost/token-tracker.js | 2 + src/mcp/helpers.js | 2 +- src/mcp/index.js | 17 +- src/mcp/ipc-bridge.js | 9 +- src/mcp/pipeline.js | 2 +- src/mcp/tools-chat.js | 39 +- src/mcp/tools-code.js | 4 +- src/mcp/tools-content.js | 4 +- src/mcp/tools-search.js | 4 +- src/mcp/tools-utility.js | 4 +- tests/config/defaults.test.js | 4 +- tests/electron/providers/sender.test.js | 11 + tests/fixtures/mcp-harness.js | 2 +- tests/mcp/helpers.test.js | 4 +- tests/mcp/ipc-bridge.test.js | 15 + tests/mcp/tools-chat.test.js | 22 +- 38 files changed, 1167 insertions(+), 90 deletions(-) create mode 100644 assets/kimi.png create mode 100644 electron/providers/engines/kimi-engine.js diff --git a/assets/kimi.png b/assets/kimi.png new file mode 100644 index 0000000000000000000000000000000000000000..2839282f73f424fd94966ebc782052945cd90554 GIT binary patch literal 9267 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 JuTiy&{txy4*ChY| literal 0 HcmV?d00001 diff --git a/cli/proxima-cli.cjs b/cli/proxima-cli.cjs index 1934cf0..9e673e4 100644 --- a/cli/proxima-cli.cjs +++ b/cli/proxima-cli.cjs @@ -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' }; diff --git a/electron/api/byok/keys.cjs b/electron/api/byok/keys.cjs index 3e61b3f..50071e7 100644 --- a/electron/api/byok/keys.cjs +++ b/electron/api/byok/keys.cjs @@ -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', ]); diff --git a/electron/api/byok/model-fetcher.cjs b/electron/api/byok/model-fetcher.cjs index e28e93f..09244ed 100644 --- a/electron/api/byok/model-fetcher.cjs +++ b/electron/api/byok/model-fetcher.cjs @@ -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', diff --git a/electron/api/byok/models.cjs b/electron/api/byok/models.cjs index 73461b2..3e4f481 100644 --- a/electron/api/byok/models.cjs +++ b/electron/api/byok/models.cjs @@ -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'], @@ -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], @@ -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', @@ -61,6 +64,7 @@ const MAX_TOKENS = Object.freeze({ claude: 4096, gemini: 8192, perplexity: 4096, + kimi: 8192, deepseek: 4096, groq: 4096, xai: 4096, diff --git a/electron/api/byok/router.cjs b/electron/api/byok/router.cjs index 2883a35..4655d1d 100644 --- a/electron/api/byok/router.cjs +++ b/electron/api/byok/router.cjs @@ -16,6 +16,7 @@ const CALLERS = Object.freeze({ claude: anthropic, gemini: google, perplexity: perplexity, + kimi: compatible, deepseek: compatible, groq: compatible, xai: compatible, diff --git a/electron/api/pages/widget.cjs b/electron/api/pages/widget.cjs index e44a472..23d6f9d 100644 --- a/electron/api/pages/widget.cjs +++ b/electron/api/pages/widget.cjs @@ -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(); diff --git a/electron/api/rest-api.cjs b/electron/api/rest-api.cjs index 94c7b8d..3b429d6 100644 --- a/electron/api/rest-api.cjs +++ b/electron/api/rest-api.cjs @@ -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' }; @@ -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; @@ -566,7 +567,7 @@ 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)) { return sendError(res, 401, 'Invalid or missing API key', 'authentication_error'); } @@ -574,7 +575,7 @@ function startRestAPI() { 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); } }); diff --git a/electron/api/routes.cjs b/electron/api/routes.cjs index 5fe239c..674f297 100644 --- a/electron/api/routes.cjs +++ b/electron/api/routes.cjs @@ -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(); @@ -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); 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); @@ -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; @@ -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' }) ); } @@ -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', @@ -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) { @@ -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'); } diff --git a/electron/api/ws-server.cjs b/electron/api/ws-server.cjs index cf0156e..4b0fb9a 100644 --- a/electron/api/ws-server.cjs +++ b/electron/api/ws-server.cjs @@ -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 }]; @@ -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) { @@ -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; @@ -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, { @@ -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); diff --git a/electron/browser-manager.cjs b/electron/browser-manager.cjs index ec228c6..4ff59c9 100644 --- a/electron/browser-manager.cjs +++ b/electron/browser-manager.cjs @@ -32,6 +32,11 @@ class BrowserManager { url: 'https://gemini.google.com/app', partition: 'persist:gemini', color: '#4285f4' + }, + kimi: { + url: 'https://www.kimi.com/', + partition: 'persist:kimi', + color: '#6b4fbb' } }; @@ -398,7 +403,7 @@ class BrowserManager { }; view.webContents.on('will-navigate', (event, url) => { - if (isAuthUrl(url)) { + if (isAuthUrl(url) && provider !== 'kimi') { console.log(`[${provider}] Intercepting navigation to auth URL:`, url.substring(0, 80)); event.preventDefault(); this.openAuthPopup(provider, url); @@ -406,7 +411,7 @@ class BrowserManager { }); view.webContents.on('will-redirect', (event, url) => { - if (isAuthUrl(url)) { + if (isAuthUrl(url) && provider !== 'kimi') { console.log(`[${provider}] Intercepting redirect to auth URL:`, url.substring(0, 80)); event.preventDefault(); this.openAuthPopup(provider, url); @@ -434,6 +439,26 @@ class BrowserManager { view.webContents.setWindowOpenHandler(({ url, frameName, features }) => { console.log(`[${provider}] Popup requested:`, url.substring(0, 80)); + // Kimi's login is an in-page modal; its Google OAuth must stay in the + // same Chromium session (not be routed through the Firefox-UA auth + // window), otherwise the OAuth callback breaks. Allow all popups for + // Kimi with the normal Chromium UA/session. + if (provider === 'kimi') { + return { + action: 'allow', + overrideBrowserWindowOptions: { + width: 600, + height: 700, + webPreferences: { + session: ses, + sandbox: true, + contextIsolation: true, + nodeIntegration: false, + } + } + }; + } + const lowerUrl = url.toLowerCase(); const isAuthPopup = lowerUrl.includes('accounts.google.com') || @@ -608,7 +633,8 @@ class BrowserManager { perplexity: 'perplexity.ai', chatgpt: 'chatgpt.com', claude: 'claude.ai', - gemini: 'gemini.google.com' + gemini: 'gemini.google.com', + kimi: 'kimi.com' }; const domain = providerDomains[provider]; @@ -801,6 +827,38 @@ class BrowserManager { return hasInput && !hasSignIn; })() `); + case 'kimi': + // kimi.com renders a chat editor even when logged out, so DOM + // checks are unreliable. Kimi's web session is backed by a JWT + // in localStorage plus an HttpOnly cookie. Login = a valid + // access token is present in storage AND the subscription + // endpoint answers 2xx when called with that token. + return await webContents.executeJavaScript(` + (async function() { + function isJwt(v) { + return typeof v === 'string' && /^eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/.test(v.trim()); + } + var token = null; + try { + var at = localStorage.getItem('access_token'); + if (isJwt(at)) token = at.trim(); + } catch (e) { } + try { + var deviceId = localStorage.getItem('proxima_kimi_device_id') || '7' + String(Math.floor(Math.random() * 9e17)).padStart(18, '0'); + var sessionId = localStorage.getItem('proxima_kimi_session_id') || '17' + String(Math.floor(Math.random() * 9e17)).padStart(17, '0'); + var headers = { 'Content-Type': 'application/json', 'Connect-Protocol-Version': '1', 'Origin': 'https://www.kimi.com', 'X-Msh-Platform': 'web', 'X-Msh-Device-Id': deviceId, 'X-Msh-Session-Id': sessionId }; + if (token) headers['Authorization'] = 'Bearer ' + token; + var res = await fetch('/apiv2/kimi.gateway.order.v1.SubscriptionService/GetSubscription', { + method: 'POST', credentials: 'include', headers: headers, body: '{}' + }); + if (res.status === 401 || res.status === 403) return false; + if (res.ok) return true; + return false; + } catch (e) { + return !!token; + } + })() + `); default: return false; } diff --git a/electron/index-v2.html b/electron/index-v2.html index afc2123..c18724a 100644 --- a/electron/index-v2.html +++ b/electron/index-v2.html @@ -154,6 +154,10 @@ background: linear-gradient(135deg, #4285f4, #8ab4f8); } + .tab-icon.kimi { + background: linear-gradient(135deg, #6b4fbb, #a78bfa); + } + .tab-icon.settings { background: transparent; border: none; @@ -1328,6 +1332,11 @@ Gemini
+
+ Kimi + Kimi +
+
Active Providers
-
0 / 4
+
0 / 5
@@ -1668,6 +1677,28 @@

Account: Session + + +
+
+
+ + Kimi +
+
+
+
+
Status: Checking...
+
Latency: --ms
+
Model: Auto
+
Account: Session
+
+
@@ -2456,7 +2487,7 @@

Platform Stats:
- 4 Providers • MCP Enabled • REST Server + 5 Providers • MCP Enabled • REST Server
@@ -2570,7 +2601,8 @@