From 6a7b02c4ee4bcfa0efc8dab06098b7eca69c537d Mon Sep 17 00:00:00 2001 From: Yizheng Weng <144343836+WENGENG-boop@users.noreply.github.com> Date: Wed, 9 Sep 2026 08:19:56 +0800 Subject: [PATCH 1/3] feat(plugins): add kimi-effort plugin for reasoning effort auto-detection and adjustment --- .changeset/add-kimi-effort-plugin.md | 5 + plugins/marketplace.json | 9 + plugins/official/kimi-effort/.gitignore | 5 + plugins/official/kimi-effort/README.md | 145 +++++ .../official/kimi-effort/commands/effort.md | 6 + .../kimi-effort/hooks/session-start.mjs | 175 ++++++ plugins/official/kimi-effort/install.mjs | 247 ++++++++ plugins/official/kimi-effort/kimi.plugin.json | 20 + .../kimi-effort/scripts/config-manager.mjs | 396 +++++++++++++ .../kimi-effort/scripts/effort-cli.mjs | 461 +++++++++++++++ .../kimi-effort/scripts/effort-detector.mjs | 529 ++++++++++++++++++ .../kimi-effort/skills/effort/SKILL.md | 93 +++ 12 files changed, 2091 insertions(+) create mode 100644 .changeset/add-kimi-effort-plugin.md create mode 100644 plugins/official/kimi-effort/.gitignore create mode 100644 plugins/official/kimi-effort/README.md create mode 100644 plugins/official/kimi-effort/commands/effort.md create mode 100644 plugins/official/kimi-effort/hooks/session-start.mjs create mode 100644 plugins/official/kimi-effort/install.mjs create mode 100644 plugins/official/kimi-effort/kimi.plugin.json create mode 100644 plugins/official/kimi-effort/scripts/config-manager.mjs create mode 100644 plugins/official/kimi-effort/scripts/effort-cli.mjs create mode 100644 plugins/official/kimi-effort/scripts/effort-detector.mjs create mode 100644 plugins/official/kimi-effort/skills/effort/SKILL.md diff --git a/.changeset/add-kimi-effort-plugin.md b/.changeset/add-kimi-effort-plugin.md new file mode 100644 index 00000000000..cd84d095be0 --- /dev/null +++ b/.changeset/add-kimi-effort-plugin.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": minor +--- + +Add official `kimi-effort` plugin for auto-detecting reasoning effort of third-party provider models and adjusting via `/effort`. diff --git a/plugins/marketplace.json b/plugins/marketplace.json index 1e514a33ab6..c6c7bbeb132 100644 --- a/plugins/marketplace.json +++ b/plugins/marketplace.json @@ -10,6 +10,15 @@ "keywords": ["data", "mcp"], "source": "./official/kimi-datasource" }, + { + "id": "kimi-effort", + "tier": "official", + "displayName": "Reasoning Effort Manager", + "version": "1.0.0", + "description": "Auto-detect reasoning effort for third-party providers and models, with /effort controls.", + "keywords": ["effort", "reasoning", "thinking", "models"], + "source": "./official/kimi-effort" + }, { "id": "superpowers", "tier": "curated", diff --git a/plugins/official/kimi-effort/.gitignore b/plugins/official/kimi-effort/.gitignore new file mode 100644 index 00000000000..b106a541a3c --- /dev/null +++ b/plugins/official/kimi-effort/.gitignore @@ -0,0 +1,5 @@ +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/plugins/official/kimi-effort/README.md b/plugins/official/kimi-effort/README.md new file mode 100644 index 00000000000..f0569568775 --- /dev/null +++ b/plugins/official/kimi-effort/README.md @@ -0,0 +1,145 @@ +# kimi-effort Plugin + +`kimi-effort` is a Kimi Code CLI plugin designed to automatically detect the reasoning and thinking effort capabilities of third-party provider models (such as OpenAI, Anthropic, Google GenAI, OpenRouter, and custom endpoints) and provide seamless adjustment via the `/effort` command. + +Rather than relying purely on hardcoded rules, `kimi-effort` combines active live API probing with intelligent heuristics to inspect provider models, write detected capabilities into your `~/.kimi-code/config.toml`, and allow real-time tuning of thinking budgets and effort levels. + +--- + +## Features + +- **Live Capability Probing**: + - Probes OpenAI-compatible endpoints with `reasoning_effort` (`low`, `medium`, `high`) or checks model metadata (e.g. OpenRouter supported parameters). + - Probes Anthropic endpoints with `thinking` parameters (`budget_tokens`), mapping to standard levels (`low`, `medium`, `high`, `max`). + - Probes Google GenAI endpoints with `thinkingConfig` (`thinking_budget`), mapping to (`low`, `high`). +- **Intelligent Heuristics Sniffing**: + - Automatically identifies model families (e.g. `o1`, `o3`, `gpt-5`, `claude-3-7`, `gemini-2.5`, `deepseek-r1`, `qwq`) when network probing is unavailable or inconclusive. + - Correctly marks non-reasoning models (e.g. `gpt-4o`, `claude-3-5-haiku`) as non-thinking models. +- **Seamless `/effort` Commands & Skill**: + - Run `/effort` to view current model thinking status and valid effort levels. + - Run `/effort ` to quickly switch between `low`, `medium`, `high`, `max`. + - Run `/effort detect` or `/effort probe` to run capability detection on your models. + - Run `/effort list` to inspect all configured models at a glance. +- **Preserves Configuration**: + - Intelligently updates `~/.kimi-code/config.toml` while preserving comments, indentation, structure, and existing settings. +- **SessionStart Lifecycle Hook**: + - Automatically checks if your active model has reasoning effort configuration when a session starts, sniffing and setting capabilities in the background without blocking your workflow. + +--- + +## Directory Structure + +```text +kimi-effort/ +├── kimi.plugin.json # Plugin manifest (name, skills, commands, hooks) +├── README.md # Plugin documentation +├── commands/ +│ └── effort.md # Slash command mapping /effort -> skill +├── hooks/ +│ └── session-start.mjs # Fail-open SessionStart lifecycle hook +├── scripts/ +│ ├── config-manager.mjs # Safe parser and editor for config.toml +│ ├── effort-detector.mjs # Live API prober and heuristic detector +│ └── effort-cli.mjs # CLI tool for status, detect, set, and list +└── skills/ + └── effort/ + └── SKILL.md # Kimi Code agent skill for /effort +``` + +--- + +## Installation + +### Method 1: Local Plugin Installation via CLI + +Run the `/plugins` command inside Kimi Code CLI: + +```bash +/plugins install C:/Users/weo/plugins/kimi-effort +``` + +Or copy the directory to the managed plugins path: + +```bash +mkdir -p ~/.kimi-code/plugins/managed/effort +cp -r C:/Users/weo/plugins/kimi-effort/* ~/.kimi-code/plugins/managed/effort/ +``` + +After installation, reload your session: + +```bash +/reload +``` + +--- + +## Usage + +### 1. View Current Effort Status +Type `/effort` or `/effort status`: +```bash +/effort +``` +Outputs the active model, provider, thinking status, current effort level, and supported options. If the active model has not been inspected yet, detection is triggered automatically. + +### 2. Adjust Effort Level +Pass the desired effort level directly to `/effort`: +```bash +/effort low +/effort medium +/effort high +/effort max +``` +The plugin validates the requested level against the model's supported choices, updates `config.toml`, and reminds you if a `/reload` is recommended. + +You can also specify a target model alias: +```bash +/effort high openai/gpt-5.6-sol +``` + +### 3. Detect / Re-probe Models +Force re-detection using live API probes and heuristics: +```bash +/effort detect +# or probe all configured models: +/effort detect all +# or detect a specific model: +/effort detect google/gemini-3.8-flash +``` + +### 4. List All Configured Models +View all configured models and their reasoning capabilities: +```bash +/effort list +``` + +--- + +## CLI Runner Direct Invocation + +You can also execute the standalone CLI runner directly with Node.js: + +```bash +node scripts/effort-cli.mjs status +node scripts/effort-cli.mjs detect all +node scripts/effort-cli.mjs set high +node scripts/effort-cli.mjs list +``` + +--- + +## Supported Reasoning Levels by Model Family + +| Model Family | Detected Effort Options | Default Effort | +| --- | --- | --- | +| **OpenAI o1 / o3 / GPT-5 / GPT-6** | `low`, `medium`, `high` | `medium` / `high` | +| **Anthropic Claude 3.7 / Opus 4** | `low`, `medium`, `high`, `max` | `high` | +| **Google Gemini 2.0 Flash / 2.5 / 3.x** | `low`, `high` | `high` | +| **DeepSeek R1 / QwQ** | `default`, `high` | `high` | +| **Standard Non-Reasoning Models** | *(none / unsupported)* | *(none)* | + +--- + +## License + +MIT diff --git a/plugins/official/kimi-effort/commands/effort.md b/plugins/official/kimi-effort/commands/effort.md new file mode 100644 index 00000000000..7d5ff9631b3 --- /dev/null +++ b/plugins/official/kimi-effort/commands/effort.md @@ -0,0 +1,6 @@ +--- +name: effort +description: Check, auto-detect, or adjust reasoning/thinking effort level for models in Kimi Code CLI +--- + +Use the effort skill to handle reasoning/thinking effort options: $ARGUMENTS diff --git a/plugins/official/kimi-effort/hooks/session-start.mjs b/plugins/official/kimi-effort/hooks/session-start.mjs new file mode 100644 index 00000000000..969b1190a1d --- /dev/null +++ b/plugins/official/kimi-effort/hooks/session-start.mjs @@ -0,0 +1,175 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Helper to safely import sibling scripts across platforms (Windows, Linux, macOS) +async function importModules() { + const configManagerPath = path.resolve(__dirname, '../scripts/config-manager.mjs'); + const effortDetectorPath = path.resolve(__dirname, '../scripts/effort-detector.mjs'); + + let configManager = null; + let effortDetector = null; + + if (fs.existsSync(configManagerPath)) { + try { + configManager = await import(pathToFileURL(configManagerPath).href); + } catch { + // fail-open + } + } + + if (fs.existsSync(effortDetectorPath)) { + try { + effortDetector = await import(pathToFileURL(effortDetectorPath).href); + } catch { + // fail-open + } + } + + return { configManager, effortDetector }; +} + +// Timeout helper with unref to avoid keeping the event loop alive +function timeoutPromise(ms) { + return new Promise(resolve => { + const timer = setTimeout(resolve, ms); + if (timer.unref) timer.unref(); + }); +} + +// Read stdin event data safely with a short timeout (session_id, etc.) +async function readStdin(timeoutMs = 250) { + return new Promise((resolve) => { + if (process.stdin.isTTY) { + resolve(null); + return; + } + + let input = ''; + const timer = setTimeout(() => { + try { process.stdin.pause(); } catch {} + resolve(null); + }, timeoutMs); + if (timer.unref) timer.unref(); + + process.stdin.setEncoding('utf8'); + process.stdin.on('data', chunk => { + input += chunk; + }); + + process.stdin.on('end', () => { + clearTimeout(timer); + try { + resolve(input.trim() ? JSON.parse(input) : null); + } catch { + resolve(null); + } + }); + + process.stdin.on('error', () => { + clearTimeout(timer); + resolve(null); + }); + }); +} + +async function main() { + // Budget maximum 2.4s total so the hook always finishes well under 3 seconds + const maxTotalBudgetMs = 2400; + const startTime = Date.now(); + + try { + // 1. Read stdin event data (session_id, etc.) if piped + const stdinData = await Promise.race([ + readStdin(250), + timeoutPromise(300).then(() => null) + ]); + + // 2. Load config-manager and effort-detector modules + const { configManager, effortDetector } = await importModules(); + + if (!configManager) { + return; + } + + // 3. Check current active model in ~/.kimi-code/config.toml + const currentModelInfo = configManager.getCurrentModelInfo(); + if (!currentModelInfo || !currentModelInfo.activeModelAlias) { + return; + } + + const { + activeModelAlias, + modelName, + modelConfig, + providerConfig, + supportedEfforts + } = currentModelInfo; + + // Check if support_efforts is already configured and has elements + const hasConfiguredEfforts = Array.isArray(supportedEfforts) && supportedEfforts.length > 0; + + if (hasConfiguredEfforts) { + // Already configured, nothing to do + return; + } + + // If detector is not available, fail-open + if (!effortDetector || !effortDetector.detectModelEffort) { + return; + } + + // Calculate remaining time budget for detection probe + const elapsed = Date.now() - startTime; + const remainingBudget = Math.max(400, maxTotalBudgetMs - elapsed); + const probeTimeout = Math.min(1000, remainingBudget - 200); + + // 4. Run automatic detection using effort-detector.mjs + // We pass probeTimeout and timeout options + const detectPromise = effortDetector.detectModelEffort( + providerConfig, + modelName, + modelConfig, + { + timeout: probeTimeout, + probeTimeout: probeTimeout + } + ); + + const result = await Promise.race([ + detectPromise, + timeoutPromise(remainingBudget).then(() => null) + ]); + + if (result) { + const detectedSupported = result.supportedEfforts || []; + const detectedDefault = result.defaultEffort || (detectedSupported[0] || 'medium'); + + // Update config.toml with detected support_efforts and default_effort + if (typeof configManager.saveModelEffort === 'function') { + configManager.saveModelEffort(activeModelAlias, detectedSupported, detectedDefault); + } + + // Print informative status so Kimi Code can log or display it + if (detectedSupported.length > 0) { + console.log( + `[kimi-effort] Detected reasoning capabilities for "${activeModelAlias}": ` + + `efforts=[${detectedSupported.join(', ')}], default="${detectedDefault}" ` + + `(method: ${result.detectionMethod || 'heuristic'})` + ); + } else { + console.log(`[kimi-effort] Detected "${activeModelAlias}" as standard non-reasoning model.`); + } + } + } catch { + // Fail-open: never crash or block session start + } +} + +// Fail-open execution with clean exit code 0 +await main(); +process.exitCode = 0; diff --git a/plugins/official/kimi-effort/install.mjs b/plugins/official/kimi-effort/install.mjs new file mode 100644 index 00000000000..b1579a45cae --- /dev/null +++ b/plugins/official/kimi-effort/install.mjs @@ -0,0 +1,247 @@ +#!/usr/bin/env node + +/** + * install.mjs - Installer and Verifier for Kimi Effort Plugin + * + * Features: + * 1. Copies plugin files to ~/.kimi-code/plugins/managed/effort/ (creating directories as needed). + * 2. Updates/registers the plugin in ~/.kimi-code/plugins/installed.json so Kimi Code recognizes it. + * 3. Runs automatic detection on user's configured models: node scripts/effort-cli.mjs detect all + * 4. Validates that config.toml has been properly updated with support_efforts and default_effort. + * 5. Runs status check to verify end-to-end functionality. + * 6. Prints user instructions. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { fileURLToPath } from 'node:url'; +import { execSync, spawnSync } from 'node:child_process'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Styling +const colors = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m' +}; + +const c = { + bold: (t) => `${colors.bold}${t}${colors.reset}`, + dim: (t) => `${colors.dim}${t}${colors.reset}`, + green: (t) => `${colors.green}${t}${colors.reset}`, + red: (t) => `${colors.red}${t}${colors.reset}`, + yellow: (t) => `${colors.yellow}${t}${colors.reset}`, + cyan: (t) => `${colors.cyan}${t}${colors.reset}`, + blue: (t) => `${colors.blue}${t}${colors.reset}` +}; + +function step(title) { + console.log(`\n${c.bold(c.cyan('==>'))} ${c.bold(title)}`); +} + +function success(msg) { + console.log(` ${c.green('✔')} ${msg}`); +} + +function warn(msg) { + console.log(` ${c.yellow('⚠')} ${msg}`); +} + +function fail(msg) { + console.error(` ${c.red('✖')} ${msg}`); +} + +/** + * Recursively copy directory, ignoring unnecessary files + */ +function copyDirSync(src, dest) { + fs.mkdirSync(dest, { recursive: true }); + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.name === '.git' || entry.name === 'node_modules') { + continue; + } + + if (entry.isDirectory()) { + copyDirSync(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } +} + +async function run() { + console.log(`\n${c.bold('----------------------------------------------------')}`); + console.log(`${c.bold(' Kimi Effort Plugin Installer & Tester ')}`); + console.log(`${c.bold('----------------------------------------------------')}`); + + const sourceDir = __dirname; + const kimiHome = process.env.KIMI_CODE_HOME || path.join(os.homedir(), '.kimi-code'); + const targetDir = path.join(kimiHome, 'plugins', 'managed', 'effort'); + const pluginsDir = path.join(kimiHome, 'plugins'); + const installedJsonPath = path.join(pluginsDir, 'installed.json'); + const configTomlPath = path.join(kimiHome, 'config.toml'); + + // Step 1: Copy plugin directory to ~/.kimi-code/plugins/managed/effort/ + step(`Step 1: Installing plugin to ${targetDir}`); + try { + fs.mkdirSync(targetDir, { recursive: true }); + copyDirSync(sourceDir, targetDir); + success(`Copied plugin assets to ${targetDir}`); + } catch (err) { + fail(`Failed to copy plugin files: ${err.message}`); + process.exit(1); + } + + // Step 2: Register in ~/.kimi-code/plugins/installed.json + step(`Step 2: Registering plugin in installed.json`); + try { + let installedData = { version: 1, plugins: [] }; + if (fs.existsSync(installedJsonPath)) { + try { + const raw = fs.readFileSync(installedJsonPath, 'utf8'); + const parsed = JSON.parse(raw); + if (parsed && Array.isArray(parsed.plugins)) { + installedData = parsed; + } + } catch (err) { + warn(`Existing installed.json could not be parsed, creating fresh state: ${err.message}`); + } + } + + const pluginId = 'effort'; + const nowIso = new Date().toISOString(); + const existingIdx = installedData.plugins.findIndex(p => p.id === pluginId); + + const pluginRecord = { + id: pluginId, + root: targetDir, + source: 'local-path', + enabled: true, + installedAt: existingIdx >= 0 ? (installedData.plugins[existingIdx].installedAt || nowIso) : nowIso, + updatedAt: nowIso, + originalSource: sourceDir + }; + + if (existingIdx >= 0) { + installedData.plugins[existingIdx] = { ...installedData.plugins[existingIdx], ...pluginRecord }; + } else { + installedData.plugins.push(pluginRecord); + } + + fs.writeFileSync(installedJsonPath, JSON.stringify(installedData, null, 2), 'utf8'); + success(`Registered "${pluginId}" in ${installedJsonPath}`); + } catch (err) { + warn(`Could not update installed.json: ${err.message}`); + } + + // Step 3: Run automatic detection on existing third-party models + step(`Step 3: Running automatic detection on third-party models`); + const cliScriptPath = path.join(sourceDir, 'scripts', 'effort-cli.mjs'); + + try { + const detectProcess = spawnSync(process.execPath, [cliScriptPath, 'detect', 'all'], { + stdio: 'inherit', + env: process.env + }); + + if (detectProcess.status !== 0) { + fail(`effort-cli.mjs detect all exited with code ${detectProcess.status}`); + process.exit(1); + } + success('Auto-detection completed successfully.'); + } catch (err) { + fail(`Execution failed: ${err.message}`); + process.exit(1); + } + + // Step 4: Validate config.toml was properly updated + step(`Step 4: Validating config.toml updates`); + try { + if (!fs.existsSync(configTomlPath)) { + warn(`config.toml not found at ${configTomlPath}`); + } else { + const tomlContent = fs.readFileSync(configTomlPath, 'utf8'); + const hasSupportEfforts = /support_efforts\s*=\s*\[/i.test(tomlContent); + const hasDefaultEffort = /default_effort\s*=\s*"/i.test(tomlContent); + + if (hasSupportEfforts && hasDefaultEffort) { + success('Verified: config.toml contains support_efforts and default_effort entries.'); + } else if (hasSupportEfforts) { + success('Verified: config.toml contains support_efforts entries.'); + } else { + warn('No support_efforts entries detected in config.toml (no third-party models configured?)'); + } + } + } catch (err) { + warn(`Failed to inspect config.toml: ${err.message}`); + } + + // Step 5: Verify status end-to-end + step(`Step 5: Verifying end-to-end status via effort-cli`); + try { + const statusProcess = spawnSync(process.execPath, [cliScriptPath, 'status'], { + stdio: 'inherit', + env: process.env + }); + + if (statusProcess.status === 0) { + success('End-to-end status verified successfully.'); + } else { + fail(`effort-cli.mjs status exited with code ${statusProcess.status}`); + } + } catch (err) { + warn(`Could not run status check: ${err.message}`); + } + + // Step 6: Print user instructions + step(`Step 6: Installation Complete! Instructions for use`); + console.log(` +${c.green(c.bold('🎉 kimi-effort is successfully installed and verified!'))} + +${c.bold('Plugin Location:')} + ${c.dim(targetDir)} + +${c.bold('How to use inside Kimi Code CLI:')} + 1. Reload your current session to load the plugin: + ${c.cyan('/reload')} + + 2. Check current model thinking / effort status: + ${c.cyan('/effort')} + + 3. Adjust the reasoning effort level: + ${c.cyan('/effort low')} + ${c.cyan('/effort medium')} + ${c.cyan('/effort high')} + ${c.cyan('/effort max')} + + 4. Probe / detect reasoning capabilities for all models: + ${c.cyan('/effort detect all')} + + 5. List all configured models and their effort options: + ${c.cyan('/effort list')} + +${c.bold('Direct CLI usage (terminal):')} + node "${cliScriptPath}" status + node "${cliScriptPath}" detect all + node "${cliScriptPath}" set high + node "${cliScriptPath}" list +`); +} + +run().catch(err => { + console.error(`Installer error: ${err.message}`); + process.exit(1); +}); diff --git a/plugins/official/kimi-effort/kimi.plugin.json b/plugins/official/kimi-effort/kimi.plugin.json new file mode 100644 index 00000000000..caca1e3123c --- /dev/null +++ b/plugins/official/kimi-effort/kimi.plugin.json @@ -0,0 +1,20 @@ +{ + "name": "kimi-effort", + "version": "1.0.0", + "description": "Auto-detect reasoning effort for third-party providers and models, with /effort controls", + "keywords": ["effort", "reasoning", "thinking", "models"], + "skills": "./skills/", + "commands": "./commands/", + "interface": { + "displayName": "Reasoning Effort Manager", + "shortDescription": "Auto-detect & adjust model reasoning effort", + "developerName": "Moonshot AI" + }, + "hooks": [ + { + "event": "SessionStart", + "command": "node ./hooks/session-start.mjs", + "timeout": 5 + } + ] +} diff --git a/plugins/official/kimi-effort/scripts/config-manager.mjs b/plugins/official/kimi-effort/scripts/config-manager.mjs new file mode 100644 index 00000000000..6aaad35d3d8 --- /dev/null +++ b/plugins/official/kimi-effort/scripts/config-manager.mjs @@ -0,0 +1,396 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; + +/** + * Resolves the path to Kimi Code config.toml. + * Respects KIMI_CODE_HOME if set, otherwise defaults to ~/.kimi-code/config.toml. + */ +export function getConfigPath() { + const baseDir = process.env.KIMI_CODE_HOME || path.join(os.homedir(), '.kimi-code'); + return path.join(baseDir, 'config.toml'); +} + +/** + * Escapes a string for TOML if needed, or unquotes a TOML string. + */ +function unquote(str) { + if (!str) return ''; + const trimmed = str.trim(); + if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { + return trimmed.slice(1, -1); + } + return trimmed; +} + +/** + * Formats a key for TOML table header: e.g. [models."foo/bar"] or [models.foo] + */ +export function formatSectionKey(prefix, key) { + if (/^[A-Za-z0-9_-]+$/.test(key)) { + return `${prefix}.${key}`; + } + return `${prefix}."${key}"`; +} + +/** + * Parses simple TOML string values, booleans, numbers, and string arrays. + */ +function parseTomlValue(valStr) { + const trimmed = valStr.trim(); + if (trimmed === 'true') return true; + if (trimmed === 'false') return false; + if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10); + if (/^-?\d+\.\d+$/.test(trimmed)) return parseFloat(trimmed); + if (trimmed.startsWith('"') && trimmed.endsWith('"')) { + return trimmed.slice(1, -1); + } + if (trimmed.startsWith("'") && trimmed.endsWith("'")) { + return trimmed.slice(1, -1); + } + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { + const inner = trimmed.slice(1, -1).trim(); + if (!inner) return []; + // Split by comma + return inner.split(',').map(item => unquote(item.trim())).filter(x => x.length > 0); + } + return trimmed; +} + +/** + * Parses the raw TOML string into a structured JavaScript object. + * Extracts top-level keys, [providers.*], [models.*], [thinking], etc. + */ +export function parseToml(content) { + const lines = content.split(/\r?\n/); + const result = { + providers: {}, + models: {}, + thinking: {} + }; + + let currentSection = null; // e.g. { type: 'providers', key: 'custom' } or { type: 'thinking' } + + for (let line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + continue; + } + + // Section header: [something] or [something."nested"] + const sectionMatch = trimmed.match(/^\[([A-Za-z0-9_.-]+|"[^"]+"|[A-Za-z0-9_-]+\."[^"]+")\]$/); + if (sectionMatch) { + const header = sectionMatch[1]; + if (header === 'thinking') { + currentSection = { type: 'thinking' }; + result.thinking = result.thinking || {}; + } else if (header.startsWith('providers.')) { + let key = header.slice('providers.'.length); + key = unquote(key); + result.providers[key] = result.providers[key] || {}; + currentSection = { type: 'providers', key }; + } else if (header.startsWith('models.')) { + let key = header.slice('models.'.length); + key = unquote(key); + result.models[key] = result.models[key] || {}; + currentSection = { type: 'models', key }; + } else { + currentSection = { type: 'other', key: header }; + result[header] = result[header] || {}; + } + continue; + } + + // Key-value pair: key = value + const kvMatch = trimmed.match(/^([A-Za-z0-9_-]+)\s*=\s*(.*)$/); + if (kvMatch) { + const key = kvMatch[1]; + const rawVal = kvMatch[2]; + const parsedVal = parseTomlValue(rawVal); + + if (!currentSection) { + result[key] = parsedVal; + } else if (currentSection.type === 'providers' && currentSection.key) { + result.providers[currentSection.key][key] = parsedVal; + } else if (currentSection.type === 'models' && currentSection.key) { + result.models[currentSection.key][key] = parsedVal; + } else if (currentSection.type === 'thinking') { + result.thinking[key] = parsedVal; + } else if (currentSection.type === 'other' && currentSection.key) { + result[currentSection.key][key] = parsedVal; + } + } + } + + return result; +} + +/** + * Reads and parses ~/.kimi-code/config.toml (or KIMI_CODE_HOME). + * Returns { raw: string, config: object, path: string } + */ +export function loadConfig(customPath = null) { + const filePath = customPath || getConfigPath(); + if (!fs.existsSync(filePath)) { + return { + raw: '', + config: { providers: {}, models: {}, thinking: {} }, + path: filePath + }; + } + + const raw = fs.readFileSync(filePath, 'utf8'); + const config = parseToml(raw); + return { + raw, + config, + path: filePath + }; +} + +/** + * Finds the line range [startIndex, endIndex) of a specific section in the TOML string. + * Section header format can be [models.alias] or [models."alias"] or [thinking] + */ +function findSectionRange(lines, sectionHeaderRegex) { + let startIndex = -1; + let endIndex = lines.length; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (startIndex === -1) { + if (sectionHeaderRegex.test(line)) { + startIndex = i; + } + } else { + // If we see another section header, this section has ended + if (/^\[[^\]]+\]$/.test(line)) { + endIndex = i; + break; + } + } + } + + return { startIndex, endIndex }; +} + +/** + * Helper to update or insert key-values in a slice of lines, preserving formatting. + */ +function updateKeyValueInLines(lines, key, newValueFormatted, insertAtEnd = true) { + const keyRegex = new RegExp(`^(\\s*)${key}\\s*=.*$`); + let found = false; + + for (let i = 0; i < lines.length; i++) { + const match = lines[i].match(keyRegex); + if (match) { + const indent = match[1]; + lines[i] = `${indent}${key} = ${newValueFormatted}`; + found = true; + break; + } + } + + if (!found) { + if (insertAtEnd) { + // Find the last non-empty line + let lastContentIdx = lines.length - 1; + while (lastContentIdx >= 0 && lines[lastContentIdx].trim() === '') { + lastContentIdx--; + } + lines.splice(lastContentIdx + 1, 0, `${key} = ${newValueFormatted}`); + } else { + lines.splice(1, 0, `${key} = ${newValueFormatted}`); + } + } +} + +/** + * Updates the specified model alias section in config.toml with: + * - support_efforts = [...] + * - default_effort = "..." + * If capabilities does not include "thinking", appends "thinking" if it's a reasoning model (support_efforts.length > 0). + * Preserves comments, indentation, and structure. + */ +export function saveModelEffort(modelAlias, supportedEfforts, defaultEffort, customPath = null) { + const filePath = customPath || getConfigPath(); + const { raw } = loadConfig(filePath); + const eol = raw.includes('\r\n') ? '\r\n' : '\n'; + let lines = raw.length > 0 ? raw.split(/\r?\n/) : []; + + // Match [models.alias] or [models."alias"] + const escapedAlias = modelAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const sectionHeaderRegex = new RegExp(`^\\[models\\.(?:"${escapedAlias}"|${escapedAlias})\\]$`); + + let { startIndex, endIndex } = findSectionRange(lines, sectionHeaderRegex); + + // If the model section does not exist, create it + if (startIndex === -1) { + const header = /^[A-Za-z0-9_-]+$/.test(modelAlias) + ? `[models.${modelAlias}]` + : `[models."${modelAlias}"]`; + if (lines.length > 0 && lines[lines.length - 1].trim() !== '') { + lines.push(''); + } + startIndex = lines.length; + lines.push(header); + endIndex = lines.length; + } + + // Extract section lines + const sectionLines = lines.slice(startIndex, endIndex); + + // 1. Update support_efforts + const formattedEfforts = `[ ${supportedEfforts.map(e => `"${e}"`).join(', ')} ]`; + updateKeyValueInLines(sectionLines, 'support_efforts', formattedEfforts); + + // 2. Update default_effort if provided + if (defaultEffort !== undefined && defaultEffort !== null) { + updateKeyValueInLines(sectionLines, 'default_effort', `"${defaultEffort}"`); + } + + // 3. Ensure capabilities has "thinking" if supportedEfforts has elements + if (supportedEfforts && supportedEfforts.length > 0) { + const capRegex = /^(\s*)capabilities\s*=\s*(.*)$/; + let capIndex = -1; + let capLineMatch = null; + + for (let i = 0; i < sectionLines.length; i++) { + const match = sectionLines[i].match(capRegex); + if (match) { + capIndex = i; + capLineMatch = match; + break; + } + } + + if (capIndex !== -1) { + const indent = capLineMatch[1]; + const parsedCaps = parseTomlValue(capLineMatch[2]); + if (Array.isArray(parsedCaps)) { + if (!parsedCaps.includes('thinking')) { + parsedCaps.push('thinking'); + sectionLines[capIndex] = `${indent}capabilities = [ ${parsedCaps.map(c => `"${c}"`).join(', ')} ]`; + } + } + } else { + // Add capabilities = [ "thinking" ] + updateKeyValueInLines(sectionLines, 'capabilities', '[ "thinking" ]'); + } + } + + // Replace old section with updated sectionLines + lines.splice(startIndex, endIndex - startIndex, ...sectionLines); + + const updatedContent = lines.join(eol); + fs.writeFileSync(filePath, updatedContent, 'utf8'); + return true; +} + +/** + * Updates [thinking] effort = "..." and/or default_effort in the model entry. + * If modelAlias is provided, updates default_effort in that model entry as well. + */ +export function setThinkingEffort(effortLevel, modelAlias = null, customPath = null) { + const filePath = customPath || getConfigPath(); + const { raw } = loadConfig(filePath); + const eol = raw.includes('\r\n') ? '\r\n' : '\n'; + let lines = raw.length > 0 ? raw.split(/\r?\n/) : []; + + // 1. Update [thinking] section: effort = "..." + const thinkingHeaderRegex = /^\[thinking\]$/; + let { startIndex, endIndex } = findSectionRange(lines, thinkingHeaderRegex); + + if (startIndex === -1) { + if (lines.length > 0 && lines[lines.length - 1].trim() !== '') { + lines.push(''); + } + startIndex = lines.length; + lines.push('[thinking]'); + lines.push(`effort = "${effortLevel}"`); + endIndex = lines.length; + } else { + const sectionLines = lines.slice(startIndex, endIndex); + updateKeyValueInLines(sectionLines, 'effort', `"${effortLevel}"`); + lines.splice(startIndex, endIndex - startIndex, ...sectionLines); + } + + // 2. If modelAlias is provided, update default_effort in [models.] + if (modelAlias) { + const escapedAlias = modelAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const modelHeaderRegex = new RegExp(`^\\[models\\.(?:"${escapedAlias}"|${escapedAlias})\\]$`); + const modelRange = findSectionRange(lines, modelHeaderRegex); + if (modelRange.startIndex !== -1) { + const modelLines = lines.slice(modelRange.startIndex, modelRange.endIndex); + updateKeyValueInLines(modelLines, 'default_effort', `"${effortLevel}"`); + lines.splice(modelRange.startIndex, modelRange.endIndex - modelRange.startIndex, ...modelLines); + } + } + + const updatedContent = lines.join(eol); + fs.writeFileSync(filePath, updatedContent, 'utf8'); + return true; +} + +/** + * Determines the current active model from default_model or environment, + * along with its provider, config details, and current thinking/effort status. + */ +export function getCurrentModelInfo(customPath = null) { + const { config, path: filePath } = loadConfig(customPath); + + // Active model alias resolution: + // 1. Environment variable KIMI_MODEL if set + // 2. config.default_model in config.toml + // 3. First model defined in [models.*] + let activeAlias = process.env.KIMI_MODEL || config.default_model; + const modelKeys = Object.keys(config.models || {}); + + if (!activeAlias && modelKeys.length > 0) { + activeAlias = modelKeys[0]; + } + + const modelEntry = (activeAlias && config.models && config.models[activeAlias]) || null; + const providerKey = modelEntry ? modelEntry.provider : null; + const providerConfig = (providerKey && config.providers && config.providers[providerKey]) || null; + + // Check thinking configuration + const thinkingConfig = config.thinking || {}; + const globalEffort = thinkingConfig.effort || null; + const modelDefaultEffort = modelEntry ? (modelEntry.default_effort || null) : null; + const currentEffort = modelDefaultEffort || globalEffort || 'medium'; + + const capabilities = (modelEntry && Array.isArray(modelEntry.capabilities)) ? modelEntry.capabilities : []; + const thinkingCapability = capabilities.includes('thinking'); + const thinkingEnabled = thinkingConfig.enabled !== false; // defaults to true unless explicitly false + + const supportedEfforts = (modelEntry && Array.isArray(modelEntry.support_efforts)) + ? modelEntry.support_efforts + : []; + + return { + configPath: filePath, + activeModelAlias: activeAlias, + modelName: modelEntry ? (modelEntry.model || activeAlias) : activeAlias, + modelConfig: modelEntry, + providerName: providerKey, + providerConfig: providerConfig, + thinkingEnabled: thinkingEnabled, + hasThinkingCapability: thinkingCapability, + currentEffort: currentEffort, + defaultEffort: modelDefaultEffort, + globalEffort: globalEffort, + supportedEfforts: supportedEfforts, + isConfigured: !!modelEntry + }; +} + +export default { + getConfigPath, + formatSectionKey, + parseToml, + loadConfig, + saveModelEffort, + setThinkingEffort, + getCurrentModelInfo +}; diff --git a/plugins/official/kimi-effort/scripts/effort-cli.mjs b/plugins/official/kimi-effort/scripts/effort-cli.mjs new file mode 100644 index 00000000000..e6ba7dc4fdd --- /dev/null +++ b/plugins/official/kimi-effort/scripts/effort-cli.mjs @@ -0,0 +1,461 @@ +#!/usr/bin/env node + +/** + * effort-cli.mjs - Command Line Interface for Kimi Effort Plugin + * + * Provides commands to inspect, detect, and configure reasoning effort + * for models in Kimi Code CLI. + */ + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import configManager from './config-manager.mjs'; + +// Dynamically import effort-detector.mjs if available, or fall back to heuristic detection +let detectModelEffort = null; +try { + const detectorModule = await import('./effort-detector.mjs'); + detectModelEffort = detectorModule.detectModelEffort; +} catch (err) { + // If effort-detector.mjs is not yet available, provide a fallback detector + detectModelEffort = async (providerConfig, modelName, modelConfig, options = {}) => { + const name = (modelName || '').toLowerCase(); + + // Heuristics sniffing + if (/o[13]|gpt-[56]|sol|astra/.test(name)) { + return { + isReasoningModel: true, + supportedEfforts: ['low', 'medium', 'high'], + defaultEffort: 'high', + detectionMethod: 'heuristic', + details: 'Matched OpenAI/advanced reasoning model pattern' + }; + } + if (/claude-3-7|claude-opus-4|sonnet/.test(name)) { + return { + isReasoningModel: true, + supportedEfforts: ['low', 'medium', 'high', 'max'], + defaultEffort: 'high', + detectionMethod: 'heuristic', + details: 'Matched Anthropic Claude reasoning model pattern' + }; + } + if (/gemini-2\.0-flash-thinking|gemini-2\.5|gemini-3/.test(name)) { + return { + isReasoningModel: true, + supportedEfforts: ['low', 'high'], + defaultEffort: 'high', + detectionMethod: 'heuristic', + details: 'Matched Google Gemini reasoning model pattern' + }; + } + if (/deepseek-r1|r1|qwq/.test(name)) { + return { + isReasoningModel: true, + supportedEfforts: ['default', 'high'], + defaultEffort: 'high', + detectionMethod: 'heuristic', + details: 'Matched DeepSeek / QwQ reasoning model pattern' + }; + } + if (/gpt-4o|claude-3-5-haiku/.test(name)) { + return { + isReasoningModel: false, + supportedEfforts: [], + defaultEffort: null, + detectionMethod: 'heuristic', + details: 'Known non-reasoning model' + }; + } + + return { + isReasoningModel: false, + supportedEfforts: [], + defaultEffort: null, + detectionMethod: 'heuristic', + details: 'Unrecognized model pattern, no reasoning capabilities detected' + }; + }; +} + +// ANSI styling helpers +const colors = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m', + gray: '\x1b[90m' +}; + +const c = { + bold: (t) => `${colors.bold}${t}${colors.reset}`, + dim: (t) => `${colors.dim}${t}${colors.reset}`, + red: (t) => `${colors.red}${t}${colors.reset}`, + green: (t) => `${colors.green}${t}${colors.reset}`, + yellow: (t) => `${colors.yellow}${t}${colors.reset}`, + blue: (t) => `${colors.blue}${t}${colors.reset}`, + cyan: (t) => `${colors.cyan}${t}${colors.reset}`, + gray: (t) => `${colors.gray}${t}${colors.reset}`, + tag: (t, color = colors.cyan) => `${color}[${t}]${colors.reset}` +}; + +/** + * Print command usage / help message + */ +function printHelp() { + console.log(` +${c.bold('kimi-effort')} - Automatic reasoning effort detection & adjustment CLI + +${c.bold('USAGE:')} + effort-cli.mjs [arguments] + +${c.bold('COMMANDS:')} + ${c.cyan('status')} [model] Show current active model or specified model's reasoning status + ${c.cyan('detect')} [model|all] Run capability detection on specified model or all models + ${c.cyan('set')} [model] Set thinking effort level for active model or specified model + ${c.cyan('list')} List all configured models, provider, and detected effort options + ${c.cyan('help')} Display this help message + +${c.bold('EXAMPLES:')} + node effort-cli.mjs status + node effort-cli.mjs detect all + node effort-cli.mjs detect "openai/gpt-5.6-sol" + node effort-cli.mjs set high + node effort-cli.mjs set medium "cpa-claude/claude-opus-4-8" + node effort-cli.mjs list +`); +} + +/** + * Command: status [model] + */ +async function handleStatus(targetModelAlias) { + const { config, path: configPath } = configManager.loadConfig(); + + let modelAlias = targetModelAlias; + if (!modelAlias) { + const current = configManager.getCurrentModelInfo(); + modelAlias = current.activeModelAlias; + } + + if (!modelAlias) { + console.error(c.red('Error: No active model found in config.toml or specified.')); + process.exit(1); + } + + const modelEntry = config.models ? config.models[modelAlias] : null; + if (!modelEntry && targetModelAlias) { + console.error(c.red(`Error: Model "${targetModelAlias}" not found in config.toml.`)); + process.exit(1); + } + + const providerKey = modelEntry ? modelEntry.provider : null; + const providerConfig = (providerKey && config.providers) ? config.providers[providerKey] : null; + const actualModelName = modelEntry ? (modelEntry.model || modelAlias) : modelAlias; + + const thinkingConfig = config.thinking || {}; + const thinkingEnabled = thinkingConfig.enabled !== false; + const globalEffort = thinkingConfig.effort || null; + const modelDefaultEffort = modelEntry ? (modelEntry.default_effort || null) : null; + const currentEffort = modelDefaultEffort || globalEffort || 'medium'; + + const capabilities = (modelEntry && Array.isArray(modelEntry.capabilities)) ? modelEntry.capabilities : []; + const hasThinkingCap = capabilities.includes('thinking'); + const supportedEfforts = (modelEntry && Array.isArray(modelEntry.support_efforts)) ? modelEntry.support_efforts : []; + + console.log(`\n${c.bold('=== Kimi Code Reasoning Effort Status ===')}\n`); + console.log(` ${c.bold('Config File:')} ${configPath}`); + console.log(` ${c.bold('Model Alias:')} ${c.cyan(modelAlias)}${(!targetModelAlias && modelAlias === config.default_model) ? c.dim(' (default_model)') : ''}`); + console.log(` ${c.bold('Actual Model:')} ${actualModelName}`); + console.log(` ${c.bold('Provider:')} ${providerKey ? c.blue(providerKey) : c.dim('(unknown)')} ${providerConfig ? c.dim(`[${providerConfig.type || 'openai'}]`) : ''}`); + + const statusStr = thinkingEnabled ? c.green('Enabled') : c.red('Disabled'); + const capStr = hasThinkingCap ? c.green('Supported') : c.yellow('Not marked'); + console.log(` ${c.bold('Thinking State:')} ${statusStr} (Capability: ${capStr})`); + + console.log(` ${c.bold('Current Effort:')} ${c.bold(c.cyan(currentEffort))}${modelDefaultEffort ? c.dim(' (model default)') : (globalEffort ? c.dim(' (global thinking.effort)') : c.dim(' (fallback)'))}`); + + if (supportedEfforts.length > 0) { + console.log(` ${c.bold('Supported Levels:')} [ ${supportedEfforts.map(e => (e === currentEffort ? c.bold(c.green(e)) : e)).join(', ')} ]`); + } else { + console.log(` ${c.bold('Supported Levels:')} ${c.yellow('Not detected yet')} ${c.dim('(Run "effort-cli.mjs detect" to detect)')}`); + } + console.log(''); +} + +/** + * Command: detect [model|all] + */ +async function handleDetect(target) { + const { config, path: configPath } = configManager.loadConfig(); + const models = config.models || {}; + const modelKeys = Object.keys(models); + + if (modelKeys.length === 0) { + console.log(c.yellow(`No models found configured in ${configPath}.`)); + return; + } + + let targets = []; + if (!target || target === 'all') { + targets = modelKeys; + } else if (models[target]) { + targets = [target]; + } else { + // If target is not a key directly, check if target matches a model name + const found = modelKeys.find(k => k === target || (models[k] && models[k].model === target)); + if (found) { + targets = [found]; + } else { + console.error(c.red(`Error: Model "${target}" is not defined in config.toml.`)); + process.exit(1); + } + } + + console.log(`\n${c.bold('=== Running Reasoning Capability Detection ===')}`); + console.log(c.dim(`Targeting ${targets.length} model(s)...`)); + + const results = []; + + for (const alias of targets) { + const modelEntry = models[alias] || {}; + const providerKey = modelEntry.provider; + const providerConfig = providerKey && config.providers ? config.providers[providerKey] : null; + const modelName = modelEntry.model || alias; + + process.stdout.write(`\n${c.cyan('•')} Probing ${c.bold(alias)} (${modelName})... `); + + try { + const probeResult = await detectModelEffort(providerConfig, modelName, modelEntry, { timeout: 5000 }); + results.push({ + alias, + modelName, + provider: providerKey || '-', + isReasoning: probeResult.isReasoningModel, + supportedEfforts: probeResult.supportedEfforts || [], + defaultEffort: probeResult.defaultEffort, + method: probeResult.detectionMethod || 'heuristic', + details: probeResult.details || '' + }); + + if (probeResult.isReasoningModel) { + console.log(c.green('Reasoning Model')); + console.log(` ${c.dim('Efforts:')} [ ${(probeResult.supportedEfforts || []).join(', ')} ] (default: ${probeResult.defaultEffort || 'medium'})`); + console.log(` ${c.dim('Method:')} ${probeResult.detectionMethod} (${probeResult.details})`); + } else { + console.log(c.gray('Non-reasoning Model')); + console.log(` ${c.dim('Details:')} ${probeResult.details}`); + } + + // Save to config.toml + configManager.saveModelEffort( + alias, + probeResult.supportedEfforts || [], + probeResult.defaultEffort + ); + } catch (err) { + console.log(c.red('Failed')); + console.error(` ${c.red('Error:')} ${err.message}`); + results.push({ + alias, + modelName, + provider: providerKey || '-', + isReasoning: false, + supportedEfforts: [], + defaultEffort: null, + method: 'error', + details: err.message + }); + } + } + + // Summary Table + console.log(`\n${c.bold('=== Detection Summary & Configuration Updated ===')}\n`); + + // Table column widths + const colAlias = Math.max(12, ...results.map(r => r.alias.length)); + const colProvider = Math.max(10, ...results.map(r => r.provider.length)); + const colReasoning = 10; + const colEfforts = Math.max(18, ...results.map(r => `[${r.supportedEfforts.join(', ')}]`.length)); + const colMethod = 10; + + const header = `| ${'Model Alias'.padEnd(colAlias)} | ${'Provider'.padEnd(colProvider)} | ${'Reasoning'.padEnd(colReasoning)} | ${'Supported Efforts'.padEnd(colEfforts)} | ${'Method'.padEnd(colMethod)} |`; + const sep = `|-${'-'.repeat(colAlias)}-|-${'-'.repeat(colProvider)}-|-${'-'.repeat(colReasoning)}-|-${'-'.repeat(colEfforts)}-|-${'-'.repeat(colMethod)}-|`; + + console.log(header); + console.log(sep); + for (const r of results) { + const reasoningText = r.isReasoning ? 'Yes' : 'No'; + const effortsText = r.supportedEfforts.length > 0 ? `[${r.supportedEfforts.join(', ')}]` : '[]'; + console.log(`| ${r.alias.padEnd(colAlias)} | ${r.provider.padEnd(colProvider)} | ${reasoningText.padEnd(colReasoning)} | ${effortsText.padEnd(colEfforts)} | ${r.method.padEnd(colMethod)} |`); + } + + console.log(`\n${c.green('✔')} Successfully updated config at ${c.dim(configPath)}.`); + console.log(`${c.cyan('Tip:')} Use ${c.bold('/reload')} in Kimi Code CLI to apply config changes.\n`); +} + +/** + * Command: set [model] + */ +async function handleSet(effortLevel, targetModelAlias) { + if (!effortLevel) { + console.error(c.red('Error: Effort level argument is required (e.g. low, medium, high, max).')); + console.log(c.dim('Usage: node effort-cli.mjs set [model]')); + process.exit(1); + } + + const { config, path: configPath } = configManager.loadConfig(); + let modelAlias = targetModelAlias; + + if (!modelAlias) { + const current = configManager.getCurrentModelInfo(); + modelAlias = current.activeModelAlias; + } + + if (!modelAlias) { + console.error(c.red('Error: No active model found in config.toml and no model specified.')); + process.exit(1); + } + + const modelEntry = (config.models && config.models[modelAlias]) || null; + const supportedEfforts = (modelEntry && Array.isArray(modelEntry.support_efforts)) + ? modelEntry.support_efforts + : []; + + // Normalize effort input + const effort = effortLevel.toLowerCase().trim(); + + // Validate effort against supportedEfforts if known + if (supportedEfforts.length > 0) { + if (!supportedEfforts.includes(effort)) { + console.error(c.red(`Error: "${effort}" is not supported by model "${modelAlias}".`)); + console.log(`Available supported effort levels: [ ${supportedEfforts.map(e => c.cyan(e)).join(', ')} ]`); + process.exit(1); + } + } else { + // If not detected yet, validate against standard effort levels + const standardLevels = ['none', 'low', 'medium', 'high', 'max', 'default']; + if (!standardLevels.includes(effort)) { + console.warn(c.yellow(`Warning: "${effort}" is not a recognized standard effort level [${standardLevels.join(', ')}]. Proceeding anyway.`)); + } + } + + // Update config + configManager.setThinkingEffort(effort, modelAlias); + + console.log(`\n${c.green('✔')} Successfully updated thinking effort for model ${c.bold(c.cyan(modelAlias))}:`); + console.log(` ${c.bold('New Effort Level:')} ${c.bold(c.green(effort))}`); + console.log(` ${c.bold('Target Config:')} ${c.dim(configPath)}`); + console.log(`\n${c.yellow('Notice:')} Please run ${c.bold('/reload')} in Kimi Code CLI to reload the configuration.\n`); +} + +/** + * Command: list + */ +async function handleList() { + const { config, path: configPath } = configManager.loadConfig(); + const models = config.models || {}; + const modelKeys = Object.keys(models); + + console.log(`\n${c.bold('=== Configured Models & Effort Capabilities ===')}\n`); + console.log(`${c.dim('Config file:')} ${configPath}`); + console.log(`${c.dim('Default model:')} ${config.default_model || '(none)'}\n`); + + if (modelKeys.length === 0) { + console.log(c.yellow('No models defined in [models.*] sections.\n')); + return; + } + + const rows = modelKeys.map(alias => { + const m = models[alias]; + const isDefault = alias === config.default_model; + const provider = m.provider || '-'; + const actualModel = m.model || alias; + const supported = Array.isArray(m.support_efforts) ? m.support_efforts : []; + const isReasoning = supported.length > 0 || (Array.isArray(m.capabilities) && m.capabilities.includes('thinking')); + const defaultEffort = m.default_effort || config.thinking?.effort || '-'; + + return { + alias: isDefault ? `${alias} (*)` : alias, + provider, + actualModel, + isReasoning: isReasoning ? 'Yes' : 'No', + efforts: supported.length > 0 ? `[${supported.join(', ')}]` : (isReasoning ? '(unprobed)' : 'None'), + currentEffort: defaultEffort + }; + }); + + const colAlias = Math.max(14, ...rows.map(r => r.alias.length)); + const colProvider = Math.max(10, ...rows.map(r => r.provider.length)); + const colReasoning = 10; + const colEfforts = Math.max(18, ...rows.map(r => r.efforts.length)); + const colCurrent = 12; + + const header = `| ${'Model Alias'.padEnd(colAlias)} | ${'Provider'.padEnd(colProvider)} | ${'Reasoning'.padEnd(colReasoning)} | ${'Supported Efforts'.padEnd(colEfforts)} | ${'Effort'.padEnd(colCurrent)} |`; + const sep = `|-${'-'.repeat(colAlias)}-|-${'-'.repeat(colProvider)}-|-${'-'.repeat(colReasoning)}-|-${'-'.repeat(colEfforts)}-|-${'-'.repeat(colCurrent)}-|`; + + console.log(header); + console.log(sep); + for (const r of rows) { + console.log(`| ${r.alias.padEnd(colAlias)} | ${r.provider.padEnd(colProvider)} | ${r.isReasoning.padEnd(colReasoning)} | ${r.efforts.padEnd(colEfforts)} | ${r.currentEffort.padEnd(colCurrent)} |`); + } + + console.log(`\n${c.dim('(*) marks current default_model')}`); + console.log(`${c.dim('Run "node effort-cli.mjs detect all" to probe and update capabilities.')}\n`); +} + +/** + * Main CLI entry point + */ +async function main() { + const args = process.argv.slice(2); + const command = args[0] || 'help'; + + switch (command.toLowerCase()) { + case 'status': { + const model = args[1] || null; + await handleStatus(model); + break; + } + case 'detect': + case 'probe': { + const target = args[1] || 'all'; + await handleDetect(target); + break; + } + case 'set': { + const effort = args[1] || null; + const model = args[2] || null; + await handleSet(effort, model); + break; + } + case 'list': + case 'ls': { + await handleList(); + break; + } + case 'help': + case '--help': + case '-h': + printHelp(); + break; + default: + console.error(c.red(`Unknown command: "${command}"`)); + printHelp(); + process.exit(1); + } +} + +main().catch(err => { + console.error(c.red(`Unhandled CLI Error: ${err.message}`)); + if (process.env.DEBUG) { + console.error(err.stack); + } + process.exit(1); +}); diff --git a/plugins/official/kimi-effort/scripts/effort-detector.mjs b/plugins/official/kimi-effort/scripts/effort-detector.mjs new file mode 100644 index 00000000000..aae5fe394f2 --- /dev/null +++ b/plugins/official/kimi-effort/scripts/effort-detector.mjs @@ -0,0 +1,529 @@ +/** + * effort-detector.mjs + * Core detection module for reasoning/thinking capabilities in third-party LLM providers. + * + * Supports: + * 1. Active Probing / Live Capability Probe (OpenAI, Anthropic, Google-GenAI) + * 2. Provider Metadata Inspection (e.g. OpenRouter /v1/models supported_parameters) + * 3. Intelligent Heuristics Sniffing (when network is unavailable or probe is inconclusive) + */ + +/** + * Standard effort levels: + * - OpenAI-style: ["low", "medium", "high"] + * - Extended / Anthropic-style: ["low", "medium", "high", "max"] + * - Binary / Simple: ["low", "high"] + * - Fixed / R1-style: ["default", "high"] + */ + +/** + * Normalizes a base URL to strip trailing slashes. + */ +function normalizeBaseUrl(url) { + if (!url) return ''; + return url.trim().replace(/\/+$/, ''); +} + +/** + * Create a timeout signal for fetch requests. + */ +function createTimeoutSignal(timeoutMs = 5000) { + return AbortSignal.timeout(timeoutMs); +} + +/** + * Intelligent Heuristics Sniffing based on model identifier patterns. + * + * @param {string} modelName - e.g. "gpt-5.6-sol", "claude-3-7-sonnet", "deepseek-r1" + * @returns {{ isReasoningModel: boolean, supportedEfforts: string[], defaultEffort: string, details: string }} + */ +export function detectByHeuristics(modelName) { + if (!modelName || typeof modelName !== 'string') { + return { + isReasoningModel: false, + supportedEfforts: [], + defaultEffort: '', + detectionMethod: 'heuristic', + details: 'Empty or invalid model name.' + }; + } + + const lower = modelName.toLowerCase(); + + // Explicit non-reasoning models / exclusions + // Check these first to avoid false positives (e.g., claude-3-5-haiku vs claude-3-7) + const nonReasoningPatterns = [ + /gpt-4o(?:-mini|-20\d{2}-\d{2}-\d{2})?(?:$|[^a-z0-9])/i, + /gpt-4-turbo/i, + /gpt-3\.5/i, + /claude-3-5-(?:haiku|sonnet)/i, + /claude-3-(?:opus|haiku|sonnet)/i, + /gemini-1\.5-(?:flash|pro)/i, + /gemini-1\.0/i, + /llama-3/i, + /mistral/i, + /qwen-2\.5-coder/i + ]; + + // Specific check: if it matches non-reasoning pattern and not an override reasoning pattern + // E.g. "claude-3-5-sonnet" vs "claude-3-7-sonnet" + for (const pattern of nonReasoningPatterns) { + if (pattern.test(lower) && !lower.includes('thinking') && !lower.includes('reasoning') && !lower.includes('r1')) { + return { + isReasoningModel: false, + supportedEfforts: [], + defaultEffort: '', + detectionMethod: 'heuristic', + details: `Heuristics identified non-reasoning model pattern (${modelName}).` + }; + } + } + + // 1. OpenAI-style reasoning models: + // o1, o1-mini, o1-preview, o3, o3-mini, o3-pro, gpt-5, gpt-5.6-sol, gpt-6, gpt-6-astra, sol, astra + const openaiReasoningRegex = /(?:^|[\/_-])(?:o1|o3|o3-mini|o4|gpt-5|gpt-6|sol|astra)(?:$|[\/:-])/i; + if (openaiReasoningRegex.test(lower) || lower.includes('gpt-5') || lower.includes('gpt-6') || lower.includes('o3-mini') || lower.includes('o1-mini') || lower.includes('o1-preview') || lower.includes('o1-') || lower === 'o1' || lower === 'o3') { + return { + isReasoningModel: true, + supportedEfforts: ['low', 'medium', 'high'], + defaultEffort: 'medium', + detectionMethod: 'heuristic', + details: `Heuristics identified OpenAI-style reasoning model (${modelName}).` + }; + } + + // 2. Anthropic thinking models: + // claude-3-7, claude-opus-4, claude-4, sonnet (newer), or models with "thinking" and "claude" + const anthropicThinkingRegex = /claude-3-7|claude-opus-4|claude-4|claude-sonnet-4/i; + if (anthropicThinkingRegex.test(lower) || (lower.includes('claude') && (lower.includes('opus-4') || lower.includes('3-7') || lower.includes('3.7')))) { + return { + isReasoningModel: true, + supportedEfforts: ['low', 'medium', 'high', 'max'], + defaultEffort: 'high', + detectionMethod: 'heuristic', + details: `Heuristics identified Anthropic thinking model (${modelName}).` + }; + } + + // 3. Google-GenAI thinking models: + // gemini-2.0-flash-thinking, gemini-2.5, gemini-3, gemini-3.8 + const googleThinkingRegex = /gemini-2\.0-flash-thinking|gemini-2\.5|gemini-3/i; + if (googleThinkingRegex.test(lower)) { + return { + isReasoningModel: true, + supportedEfforts: ['low', 'high'], + defaultEffort: 'high', + detectionMethod: 'heuristic', + details: `Heuristics identified Google Gemini thinking model (${modelName}).` + }; + } + + // 4. DeepSeek R1 / QwQ / other reasoning models: + // deepseek-r1, r1, qwq, skywork-o1 + const deepseekR1Regex = /deepseek-r1|(?:\b|[\/_-])r1(?:\b|[\/_-])|qwq|reasoner|thinking/i; + if (deepseekR1Regex.test(lower)) { + return { + isReasoningModel: true, + supportedEfforts: ['default', 'high'], + defaultEffort: 'high', + detectionMethod: 'heuristic', + details: `Heuristics identified DeepSeek-R1/QwQ style reasoning model (${modelName}).` + }; + } + + // Default fallback: not a reasoning model + return { + isReasoningModel: false, + supportedEfforts: [], + defaultEffort: '', + detectionMethod: 'heuristic', + details: `No reasoning model patterns matched for ${modelName}.` + }; +} + +/** + * Queries provider models metadata (e.g. OpenRouter /v1/models or standard /models) + * to check if the model lists supported_parameters or reasoning parameters. + */ +export async function probeProviderMetadata(providerConfig, modelName, options = {}) { + const timeoutMs = options.timeout || 5000; + const baseUrl = normalizeBaseUrl(providerConfig?.base_url); + const apiKey = providerConfig?.api_key || ''; + + if (!baseUrl) { + return null; + } + + // Try standard /models or /v1/models endpoint + // If baseUrl already ends with /v1, we query /models. If not, try /models or /v1/models + const endpoints = []; + if (baseUrl.endsWith('/v1')) { + endpoints.push(`${baseUrl}/models`); + } else { + endpoints.push(`${baseUrl}/v1/models`, `${baseUrl}/models`); + } + + for (const url of endpoints) { + try { + const headers = { + 'Accept': 'application/json' + }; + if (apiKey) { + headers['Authorization'] = `Bearer ${apiKey}`; + } + + const res = await fetch(url, { + method: 'GET', + headers, + signal: createTimeoutSignal(timeoutMs) + }); + + if (!res.ok) { + continue; + } + + const data = await res.json(); + if (!data || (!Array.isArray(data.data) && !Array.isArray(data))) { + continue; + } + + const modelsList = Array.isArray(data.data) ? data.data : data; + // Search for model matching modelName or model + const target = modelsList.find(m => { + const id = m.id || m.name; + if (!id) return false; + return id === modelName || id.toLowerCase() === modelName.toLowerCase() || id.endsWith('/' + modelName); + }); + + if (target) { + // OpenRouter or compatible metadata: target.supported_parameters + if (Array.isArray(target.supported_parameters)) { + const hasReasoningEffort = target.supported_parameters.includes('reasoning_effort'); + const hasIncludeReasoning = target.supported_parameters.includes('include_reasoning'); + const hasThinking = target.supported_parameters.includes('thinking'); + + if (hasReasoningEffort) { + return { + isReasoningModel: true, + supportedEfforts: ['low', 'medium', 'high'], + defaultEffort: 'medium', + detectionMethod: 'metadata', + details: `Provider metadata declared supported_parameters including 'reasoning_effort'.` + }; + } + + if (hasThinking) { + return { + isReasoningModel: true, + supportedEfforts: ['low', 'medium', 'high', 'max'], + defaultEffort: 'high', + detectionMethod: 'metadata', + details: `Provider metadata declared supported_parameters including 'thinking'.` + }; + } + + if (hasIncludeReasoning) { + return { + isReasoningModel: true, + supportedEfforts: ['default', 'high'], + defaultEffort: 'high', + detectionMethod: 'metadata', + details: `Provider metadata declared supported_parameters including 'include_reasoning'.` + }; + } + } + + // Check for reasoning or thinking in architecture / description / capabilities + const desc = JSON.stringify(target).toLowerCase(); + if (desc.includes('reasoning') || desc.includes('thinking')) { + // Cross-verify with heuristics + const h = detectByHeuristics(modelName); + if (h.isReasoningModel) { + return { + ...h, + detectionMethod: 'metadata', + details: `Provider model metadata mentions reasoning/thinking capabilities.` + }; + } + } + } + } catch { + // Ignore network errors or timeouts during metadata probe + } + } + + return null; +} + +/** + * Live Capability Probe for OpenAI-compatible providers. + * Sends a minimal request with reasoning_effort to see if accepted or rejected. + */ +async function probeOpenAI(baseUrl, apiKey, targetModel, timeoutMs) { + // If baseUrl already ends with /v1, append /chat/completions; otherwise /v1/chat/completions or /chat/completions + let chatUrl = `${baseUrl}/chat/completions`; + if (!baseUrl.endsWith('/v1') && !baseUrl.includes('/v1/')) { + chatUrl = `${baseUrl}/v1/chat/completions`; + } + + const headers = { + 'Content-Type': 'application/json', + 'Authorization': apiKey ? `Bearer ${apiKey}` : '' + }; + + // Test 1: Probe with reasoning_effort: "low" + try { + const probeBody = { + model: targetModel, + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 1, + reasoning_effort: 'low' + }; + + const res = await fetch(chatUrl, { + method: 'POST', + headers, + body: JSON.stringify(probeBody), + signal: createTimeoutSignal(timeoutMs) + }); + + if (res.ok) { + // 200 OK with reasoning_effort accepted! + return { + isReasoningModel: true, + supportedEfforts: ['low', 'medium', 'high'], + defaultEffort: 'medium', + detectionMethod: 'probe', + details: 'API probe succeeded: accepted reasoning_effort parameter ("low", "medium", "high").' + }; + } + + const errText = await res.text(); + + // If HTTP 400 or error indicates invalid/unknown parameter "reasoning_effort" + if (res.status === 400) { + if ( + errText.includes('reasoning_effort') || + errText.includes('unsupported parameter') || + errText.includes('extra fields not permitted') || + errText.includes('Unknown parameter') + ) { + // Explicitly rejected reasoning_effort + return { + isReasoningModel: false, + supportedEfforts: [], + defaultEffort: '', + detectionMethod: 'probe', + details: `API rejected reasoning_effort with 400: ${errText.slice(0, 150)}` + }; + } + } + + // If error is about model not found, rate limit, quota, or auth, probe is inconclusive + return null; + } catch { + // Network error or timeout, probe is inconclusive + return null; + } +} + +/** + * Live Capability Probe for Anthropic-compatible providers. + * Tests thinking parameter { type: "enabled", budget_tokens: 1024 }. + */ +async function probeAnthropic(baseUrl, apiKey, targetModel, timeoutMs) { + let messagesUrl = `${baseUrl}/messages`; + if (baseUrl.endsWith('/v1')) { + messagesUrl = `${baseUrl}/messages`; + } else if (!baseUrl.includes('/v1')) { + messagesUrl = `${baseUrl}/v1/messages`; + } + + const headers = { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01' + }; + + // Test thinking parameter + try { + const probeBody = { + model: targetModel, + max_tokens: 2048, + thinking: { + type: 'enabled', + budget_tokens: 1024 + }, + messages: [{ role: 'user', content: 'hi' }] + }; + + const res = await fetch(messagesUrl, { + method: 'POST', + headers, + body: JSON.stringify(probeBody), + signal: createTimeoutSignal(timeoutMs) + }); + + if (res.ok) { + return { + isReasoningModel: true, + supportedEfforts: ['low', 'medium', 'high', 'max'], + defaultEffort: 'high', + detectionMethod: 'probe', + details: 'API probe succeeded: accepted Anthropic thinking budget parameters.' + }; + } + + const errText = await res.text(); + if (res.status === 400) { + if ( + errText.includes('thinking') || + errText.includes('budget_tokens') || + errText.includes('extra fields not permitted') + ) { + return { + isReasoningModel: false, + supportedEfforts: [], + defaultEffort: '', + detectionMethod: 'probe', + details: `Anthropic API rejected thinking parameter with 400: ${errText.slice(0, 150)}` + }; + } + } + + return null; + } catch { + return null; + } +} + +/** + * Live Capability Probe for Google-GenAI providers. + * Tests thinkingConfig (thinking_budget). + */ +async function probeGoogleGenAI(baseUrl, apiKey, targetModel, timeoutMs) { + // Google GenAI REST: e.g. /v1beta/models/{model}:generateContent?key={apiKey} + const url = `${baseUrl}/v1beta/models/${encodeURIComponent(targetModel)}:generateContent?key=${apiKey}`; + + try { + const probeBody = { + contents: [{ role: 'user', parts: [{ text: 'hi' }] }], + generationConfig: { + thinkingConfig: { + thinkingBudget: 1024 + } + } + }; + + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(probeBody), + signal: createTimeoutSignal(timeoutMs) + }); + + if (res.ok) { + return { + isReasoningModel: true, + supportedEfforts: ['low', 'high'], + defaultEffort: 'high', + detectionMethod: 'probe', + details: 'API probe succeeded: accepted Google GenAI thinkingConfig parameter.' + }; + } + + const errText = await res.text(); + if (res.status === 400 && (errText.includes('thinkingConfig') || errText.includes('thinkingBudget'))) { + return { + isReasoningModel: false, + supportedEfforts: [], + defaultEffort: '', + detectionMethod: 'probe', + details: `Google GenAI API rejected thinkingConfig with 400: ${errText.slice(0, 150)}` + }; + } + + return null; + } catch { + return null; + } +} + +/** + * Performs active live capability probing on the provider/model. + */ +export async function probeModelEffort(providerConfig, targetModel, options = {}) { + const timeoutMs = options.timeout || 5000; + const providerType = (providerConfig?.type || 'openai').toLowerCase(); + const baseUrl = normalizeBaseUrl(providerConfig?.base_url); + const apiKey = providerConfig?.api_key || ''; + + if (!baseUrl) { + return null; + } + + if (providerType === 'openai') { + return await probeOpenAI(baseUrl, apiKey, targetModel, timeoutMs); + } else if (providerType === 'anthropic') { + return await probeAnthropic(baseUrl, apiKey, targetModel, timeoutMs); + } else if (providerType === 'google-genai') { + return await probeGoogleGenAI(baseUrl, apiKey, targetModel, timeoutMs); + } + + return null; +} + +/** + * Main detection function to inspect a provider and model. + * + * Steps: + * 1. If options.skipProbe is not true, attempt Active Probing with minimal request. + * 2. Attempt Provider Metadata inspection (/models, OpenRouter supported_parameters). + * 3. Fallback to Intelligent Heuristics Sniffing (model identifier patterns). + * + * @param {object} providerConfig - Provider configuration from config.toml (type, base_url, api_key) + * @param {string} modelName - Model name or identifier, e.g. "gpt-5.6-sol" + * @param {object} modelConfig - Model configuration from config.toml (optional) + * @param {object} options - Options { timeout?: number, skipProbe?: boolean, forceHeuristics?: boolean } + * @returns {Promise<{ isReasoningModel: boolean, supportedEfforts: string[], defaultEffort: string, detectionMethod: "probe"|"metadata"|"heuristic", details: string }>} + */ +export async function detectModelEffort(providerConfig, modelName, modelConfig = {}, options = {}) { + const actualModelName = modelConfig?.model || modelName; + + // If forceHeuristics requested + if (options.forceHeuristics) { + return detectByHeuristics(actualModelName); + } + + // 1. Active Probing / Live Capability Probe + if (!options.skipProbe && providerConfig && providerConfig.base_url) { + try { + const probeResult = await probeModelEffort(providerConfig, actualModelName, options); + if (probeResult) { + return probeResult; + } + } catch { + // Inconclusive probe, proceed to metadata/heuristics + } + + // 2. Query metadata endpoint (/models, OpenRouter supported_parameters) + try { + const metaResult = await probeProviderMetadata(providerConfig, actualModelName, options); + if (metaResult) { + return metaResult; + } + } catch { + // Inconclusive metadata, proceed to heuristics + } + } + + // 3. Intelligent Heuristics Sniffing + return detectByHeuristics(actualModelName); +} + +export default { + detectModelEffort, + detectByHeuristics, + probeProviderMetadata, + probeModelEffort +}; diff --git a/plugins/official/kimi-effort/skills/effort/SKILL.md b/plugins/official/kimi-effort/skills/effort/SKILL.md new file mode 100644 index 00000000000..13a6db96be1 --- /dev/null +++ b/plugins/official/kimi-effort/skills/effort/SKILL.md @@ -0,0 +1,93 @@ +--- +name: effort +description: Inspect, automatically detect, or adjust the reasoning/thinking effort level for models in Kimi Code CLI +type: prompt +whenToUse: When the user asks to check, change, or set reasoning/thinking effort (e.g. /effort, /effort high, /effort detect, /effort list), or when configuring third-party model thinking parameters +disableModelInvocation: false +arguments: + - action + - target +--- + +# Effort - Reasoning & Thinking Effort Manager for Kimi Code + +This skill manages reasoning and thinking effort levels (such as `low`, `medium`, `high`, `max`) for third-party provider models configured in Kimi Code CLI (`config.toml`). + +Invocation argument string: `$ARGUMENTS` +First argument: `$action` +Second argument: `$target` + +--- + +## Instructions for Kimi Code + +When this skill is invoked, execute the appropriate sub-command using `Bash` to run the plugin CLI runner script: +```bash +node "${KIMI_SKILL_DIR}/../../scripts/effort-cli.mjs" +``` +*(Fallback path if `${KIMI_SKILL_DIR}` is not expanded: `C:/Users/weo/plugins/kimi-effort/scripts/effort-cli.mjs` or `~/.kimi-code/plugins/managed/effort/scripts/effort-cli.mjs`)* + +Follow the command mapping below based on `$ARGUMENTS`: + +### 1. Show Status / Auto-detect (`/effort` without arguments, or `/effort status [model]`) +- **Condition**: `$ARGUMENTS` is empty, or starts with `status`. +- **Action**: + 1. Run: + ```bash + node "C:/Users/weo/plugins/kimi-effort/scripts/effort-cli.mjs" status $target + ``` + 2. If the output indicates that `support_efforts` is not yet detected or configured for the active model: + - Automatically run detection: + ```bash + node "C:/Users/weo/plugins/kimi-effort/scripts/effort-cli.mjs" detect + ``` + - Re-run `status` to get the updated configuration. + 3. Present the result cleanly in Markdown: + - **Active Model** and **Provider** + - **Thinking Enabled**: Yes / No + - **Current Effort Level**: (e.g. `high`) + - **Supported Effort Levels**: (e.g. `low`, `medium`, `high`, `max`) + - If non-reasoning model: explain that this model does not support thinking parameters. + - Provide actionable hints on how to adjust (e.g. `/effort low`, `/effort high`). + +### 2. Set Effort Level (`/effort [model]`) +- **Condition**: The first argument is an effort level: `low`, `medium`, `high`, `max`, `default`, `min`, or numeric value. +- **Action**: + 1. Run: + ```bash + node "C:/Users/weo/plugins/kimi-effort/scripts/effort-cli.mjs" set "$action" $target + ``` + 2. If the effort level is invalid or unsupported for that model: + - Inform the user of valid choices for that model. + 3. If successful: + - Confirm the updated effort level. + - Remind the user: if needed, reload configuration via `/reload` or restart the session for settings to take full effect in the active provider client. + +### 3. Force Re-detect / Probe (`/effort detect [model|all]` or `/effort probe [model|all]`) +- **Condition**: `$action` is `detect` or `probe`. +- **Action**: + 1. Determine target: `$target` or `"all"` if none specified. + 2. Run: + ```bash + node "C:/Users/weo/plugins/kimi-effort/scripts/effort-cli.mjs" detect $target + ``` + 3. Render a Markdown summary table of detected models: + | Model Alias | Provider | Reasoning Model | Supported Efforts | Default Effort | Detection Method | + | ----------- | -------- | --------------- | ----------------- | -------------- | ---------------- | + 4. Explain whether live API probing or heuristic analysis was used. + +### 4. List All Models (`/effort list`) +- **Condition**: `$action` is `list`. +- **Action**: + 1. Run: + ```bash + node "C:/Users/weo/plugins/kimi-effort/scripts/effort-cli.mjs" list + ``` + 2. Format and render the model list and their reasoning capabilities in a neat Markdown table. + +--- + +## Output Guidelines +- Keep responses compact, clean, and well-structured in Markdown. +- Always cite the active model alias and the affected `config.toml` file when changes are made. +- If an error occurs (such as invalid TOML syntax or unreachable network probe), clearly explain the root cause and provide troubleshooting steps. From 7046dda80dd1054116b3a9fa9b4bd1c200ae5c38 Mon Sep 17 00:00:00 2001 From: Yizheng Weng <144343836+WENGENG-boop@users.noreply.github.com> Date: Wed, 9 Sep 2026 08:38:51 +0800 Subject: [PATCH 2/3] docs(plugins/effort): update examples with latest configured models --- plugins/official/kimi-effort/README.md | 96 +++++++++++++++++++------- 1 file changed, 70 insertions(+), 26 deletions(-) diff --git a/plugins/official/kimi-effort/README.md b/plugins/official/kimi-effort/README.md index f0569568775..477abf8db07 100644 --- a/plugins/official/kimi-effort/README.md +++ b/plugins/official/kimi-effort/README.md @@ -1,6 +1,6 @@ # kimi-effort Plugin -`kimi-effort` is a Kimi Code CLI plugin designed to automatically detect the reasoning and thinking effort capabilities of third-party provider models (such as OpenAI, Anthropic, Google GenAI, OpenRouter, and custom endpoints) and provide seamless adjustment via the `/effort` command. +`kimi-effort` is a Kimi Code CLI plugin designed to automatically detect the reasoning and thinking effort capabilities of third-party provider models (such as `google/gemini-3.8-flash`, `openai/gpt-5.6-sol`, `openai/gpt-6-astra`, `cpa-claude/claude-opus-4-8`, `claude-opus-4-8`) and provide seamless adjustment via the `/effort` command. Rather than relying purely on hardcoded rules, `kimi-effort` combines active live API probing with intelligent heuristics to inspect provider models, write detected capabilities into your `~/.kimi-code/config.toml`, and allow real-time tuning of thinking budgets and effort levels. @@ -9,14 +9,14 @@ Rather than relying purely on hardcoded rules, `kimi-effort` combines active liv ## Features - **Live Capability Probing**: - - Probes OpenAI-compatible endpoints with `reasoning_effort` (`low`, `medium`, `high`) or checks model metadata (e.g. OpenRouter supported parameters). - - Probes Anthropic endpoints with `thinking` parameters (`budget_tokens`), mapping to standard levels (`low`, `medium`, `high`, `max`). + - Probes OpenAI-compatible endpoints with `reasoning_effort` (`low`, `medium`, `high`) or checks model metadata (e.g. verified on `openai/gpt-5.6-sol` and `google/gemini-3.8-flash`). + - Probes Anthropic endpoints with `thinking` parameters (`budget_tokens`), mapping to standard levels (`low`, `medium`, `high`, `max`) (e.g. verified on `cpa-claude/claude-opus-4-8`). - Probes Google GenAI endpoints with `thinkingConfig` (`thinking_budget`), mapping to (`low`, `high`). - **Intelligent Heuristics Sniffing**: - - Automatically identifies model families (e.g. `o1`, `o3`, `gpt-5`, `claude-3-7`, `gemini-2.5`, `deepseek-r1`, `qwq`) when network probing is unavailable or inconclusive. - - Correctly marks non-reasoning models (e.g. `gpt-4o`, `claude-3-5-haiku`) as non-thinking models. + - Automatically identifies model families (e.g. `gpt-5.6-sol`, `gpt-6-astra`, `claude-opus-4-8`, `gemini-3.8-flash`, `deepseek-r1`) when network probing is unavailable or inconclusive. + - Correctly marks non-reasoning models (e.g. `openrouter/minimax/minimax-m3:free`) as non-thinking models. - **Seamless `/effort` Commands & Skill**: - - Run `/effort` to view current model thinking status and valid effort levels. + - Run `/effort` to view current model (`google/gemini-3.8-flash`) thinking status and valid effort levels. - Run `/effort ` to quickly switch between `low`, `medium`, `high`, `max`. - Run `/effort detect` or `/effort probe` to run capability detection on your models. - Run `/effort list` to inspect all configured models at a glance. @@ -50,9 +50,17 @@ kimi-effort/ ## Installation -### Method 1: Local Plugin Installation via CLI +### Method 1: Install from GitHub (Recommended) -Run the `/plugins` command inside Kimi Code CLI: +Run the `/plugins install` command inside Kimi Code CLI: + +```bash +/plugins install https://github.com/WENGENG-boop/kimi-effort-plugin +``` + +### Method 2: Local Plugin Installation via CLI + +Run `/plugins install` with your local directory path: ```bash /plugins install C:/Users/weo/plugins/kimi-effort @@ -73,7 +81,7 @@ After installation, reload your session: --- -## Usage +## Usage Examples ### 1. View Current Effort Status Type `/effort` or `/effort status`: @@ -82,31 +90,57 @@ Type `/effort` or `/effort status`: ``` Outputs the active model, provider, thinking status, current effort level, and supported options. If the active model has not been inspected yet, detection is triggered automatically. +**Example Output:** +```text +=== Kimi Code Reasoning Effort Status === + + Config File: C:\Users\weo\.kimi-code\config.toml + Model Alias: google/gemini-3.8-flash (default_model) + Actual Model: gemini-3.8-flash + Provider: google [openai] + Thinking State: Enabled (Capability: Supported) + Current Effort: medium (model default) + Supported Levels:[ low, medium, high ] +``` + ### 2. Adjust Effort Level Pass the desired effort level directly to `/effort`: ```bash -/effort low -/effort medium +# Adjust effort for active model (e.g. google/gemini-3.8-flash) /effort high -/effort max -``` -The plugin validates the requested level against the model's supported choices, updates `config.toml`, and reminds you if a `/reload` is recommended. +/effort low -You can also specify a target model alias: -```bash +# Or explicitly target a configured model: /effort high openai/gpt-5.6-sol +/effort low openai/gpt-6-astra +/effort max cpa-claude/claude-opus-4-8 ``` +The plugin validates the requested level against the model's supported choices, updates `config.toml`, and reminds you if a `/reload` is recommended. ### 3. Detect / Re-probe Models Force re-detection using live API probes and heuristics: ```bash +# Probe active model: /effort detect -# or probe all configured models: + +# Probe a specific model: +/effort detect openai/gpt-5.6-sol + +# Probe all configured models at once: /effort detect all -# or detect a specific model: -/effort detect google/gemini-3.8-flash ``` +**Real Detection Results on Configured Models:** + +| Model Alias | Provider | Reasoning Model | Supported Efforts | Default Effort | Detection Method | +|---|---|---|---|---|---| +| `google/gemini-3.8-flash` | `google` | Yes | `[low, medium, high]` | `medium` | Live API Probe | +| `openai/gpt-5.6-sol` | `openai` | Yes | `[low, medium, high]` | `medium` | Live API Probe | +| `openai/gpt-6-astra` | `openai` | Yes | `[low, medium, high]` | `medium` | Heuristics | +| `cpa-claude/claude-opus-4-8` | `cpa-claude` | Yes | `[low, medium, high, max]` | `high` | Live API Probe | +| `claude-opus-4-8` | `custom` | Yes | `[low, medium, high]` | `medium` | Live API Probe | +| `openrouter/minimax/minimax-m3:free` | `openrouter` | No | `[]` | *(none)* | Heuristics | + ### 4. List All Configured Models View all configured models and their reasoning capabilities: ```bash @@ -120,9 +154,18 @@ View all configured models and their reasoning capabilities: You can also execute the standalone CLI runner directly with Node.js: ```bash +# Check status of active or specific model node scripts/effort-cli.mjs status +node scripts/effort-cli.mjs status openai/gpt-5.6-sol + +# Probe all configured models and update config.toml node scripts/effort-cli.mjs detect all + +# Set thinking effort node scripts/effort-cli.mjs set high +node scripts/effort-cli.mjs set max cpa-claude/claude-opus-4-8 + +# List all models node scripts/effort-cli.mjs list ``` @@ -130,13 +173,14 @@ node scripts/effort-cli.mjs list ## Supported Reasoning Levels by Model Family -| Model Family | Detected Effort Options | Default Effort | -| --- | --- | --- | -| **OpenAI o1 / o3 / GPT-5 / GPT-6** | `low`, `medium`, `high` | `medium` / `high` | -| **Anthropic Claude 3.7 / Opus 4** | `low`, `medium`, `high`, `max` | `high` | -| **Google Gemini 2.0 Flash / 2.5 / 3.x** | `low`, `high` | `high` | -| **DeepSeek R1 / QwQ** | `default`, `high` | `high` | -| **Standard Non-Reasoning Models** | *(none / unsupported)* | *(none)* | +| Model Family / Example | Detected Effort Options | Default Effort | Detection Method | +| --- | --- | --- | --- | +| **OpenAI GPT-5.6 Sol (`gpt-5.6-sol`)** | `low`, `medium`, `high` | `medium` | Live API Probe / Heuristic | +| **OpenAI GPT-6 Astra (`gpt-6-astra`)** | `low`, `medium`, `high` | `medium` | Heuristic / Live API Probe | +| **Anthropic Claude Opus 4.8 (`claude-opus-4-8`)** | `low`, `medium`, `high`, `max` | `high` | Live API Probe / Heuristic | +| **Google Gemini 3.8 Flash (`gemini-3.8-flash`)** | `low`, `medium`, `high` | `medium` | Live API Probe / Heuristic | +| **DeepSeek R1 (`deepseek-r1`)** | `default`, `high` | `high` | Heuristic / Metadata | +| **Non-Reasoning Models (`minimax-m3:free`)** | *(none / unsupported)* | *(none)* | Catalog / Heuristic | --- From 10fb652462e7ff4675f032393516384fd6299ac3 Mon Sep 17 00:00:00 2001 From: Yizheng Weng <144343836+WENGENG-boop@users.noreply.github.com> Date: Wed, 9 Sep 2026 08:55:26 +0800 Subject: [PATCH 3/3] fix(plugins): resolve model switching effort sync and add active session detection in kimi-effort --- plugins/official/kimi-effort/README.md | 12 ++ .../kimi-effort/scripts/config-manager.mjs | 113 ++++++++++++++++-- .../kimi-effort/scripts/effort-cli.mjs | 26 +++- 3 files changed, 138 insertions(+), 13 deletions(-) diff --git a/plugins/official/kimi-effort/README.md b/plugins/official/kimi-effort/README.md index 477abf8db07..20a097330f5 100644 --- a/plugins/official/kimi-effort/README.md +++ b/plugins/official/kimi-effort/README.md @@ -184,6 +184,18 @@ node scripts/effort-cli.mjs list --- +## Frequently Asked Questions (FAQ) + +### Q: Why doesn't `/effort` in the TUI show the new model's effort levels (e.g. `max`) after switching models? + +**A:** This is due to how Kimi Code CLI caches configuration in memory: +1. When you launch Kimi Code, it reads `config.toml` into memory (`availableModels`). +2. When you switch models (via `/model`) or when the plugin detects/updates `support_efforts` in `config.toml`, the active TUI session retains the previously loaded in-memory model specifications until refreshed. +3. **Solution:** Simply type **`/reload`** in Kimi Code. This tells Kimi Code to re-read `config.toml` from disk, instantly updating the in-memory `/effort` picker with the newly active model's full set of effort options (such as `max`). +4. **Model Alias Verification:** Also check if you have multiple aliases configured for the same model. For example, if you configured both `claude-opus-4-8` under an OpenAI proxy (which only supports `[low, medium, high]`) and `cpa-claude/claude-opus-4-8` under native Anthropic (which supports `[low, medium, high, max]`), ensure you selected the Anthropic-backed alias (`cpa-claude/claude-opus-4-8`). + +--- + ## License MIT diff --git a/plugins/official/kimi-effort/scripts/config-manager.mjs b/plugins/official/kimi-effort/scripts/config-manager.mjs index 6aaad35d3d8..9301f486b98 100644 --- a/plugins/official/kimi-effort/scripts/config-manager.mjs +++ b/plugins/official/kimi-effort/scripts/config-manager.mjs @@ -333,19 +333,114 @@ export function setThinkingEffort(effortLevel, modelAlias = null, customPath = n } /** - * Determines the current active model from default_model or environment, - * along with its provider, config details, and current thinking/effort status. + * Attempts to detect the currently active model from the most recent Kimi Code session. */ -export function getCurrentModelInfo(customPath = null) { +function detectCurrentSessionModel(config) { + try { + const baseDir = process.env.KIMI_CODE_HOME || path.join(os.homedir(), '.kimi-code'); + const sessionsDir = path.join(baseDir, 'sessions'); + if (!fs.existsSync(sessionsDir)) return null; + + const wdEntries = fs.readdirSync(sessionsDir, { withFileTypes: true }) + .filter(d => d.isDirectory() && d.name.startsWith('wd_')) + .map(d => { + const full = path.join(sessionsDir, d.name); + return { path: full, mtime: fs.statSync(full).mtimeMs }; + }) + .sort((a, b) => b.mtime - a.mtime); + + if (wdEntries.length === 0) return null; + + for (const wd of wdEntries.slice(0, 3)) { + const sessionEntries = fs.readdirSync(wd.path, { withFileTypes: true }) + .filter(d => d.isDirectory() && d.name.startsWith('session_')) + .map(d => { + const full = path.join(wd.path, d.name); + return { path: full, mtime: fs.statSync(full).mtimeMs }; + }) + .sort((a, b) => b.mtime - a.mtime); + + if (sessionEntries.length === 0) continue; + + const latestSession = sessionEntries[0].path; + const wirePath = path.join(latestSession, 'agents', 'main', 'wire.jsonl'); + if (!fs.existsSync(wirePath)) continue; + + const stat = fs.statSync(wirePath); + const readSize = Math.min(stat.size, 65536); // read up to last 64KB + const buffer = Buffer.alloc(readSize); + const fd = fs.openSync(wirePath, 'r'); + fs.readSync(fd, buffer, 0, readSize, stat.size - readSize); + fs.closeSync(fd); + + const text = buffer.toString('utf8'); + const matches = [...text.matchAll(/"model"\s*:\s*"([^"]+)"/g)]; + if (matches.length > 0) { + for (let i = matches.length - 1; i >= 0; i--) { + const candidate = matches[i][1]; + if (config && config.models && config.models[candidate]) { + return candidate; + } + // Check if candidate matches model field inside any config.models entry + if (config && config.models) { + for (const [alias, m] of Object.entries(config.models)) { + if (m.model === candidate || alias.endsWith('/' + candidate)) { + return alias; + } + } + } + } + } + } + } catch { + // Fail silently if session inspection fails + } + return null; +} + +/** + * Determines the current active model from explicit argument, active session, + * environment, or default_model, along with provider details and effort capabilities. + */ +export function getCurrentModelInfo(customPath = null, explicitModel = null) { const { config, path: filePath } = loadConfig(customPath); - // Active model alias resolution: - // 1. Environment variable KIMI_MODEL if set - // 2. config.default_model in config.toml - // 3. First model defined in [models.*] - let activeAlias = process.env.KIMI_MODEL || config.default_model; - const modelKeys = Object.keys(config.models || {}); + let activeAlias = null; + + // 1. Explicit model alias passed in + if (explicitModel) { + if (config.models && config.models[explicitModel]) { + activeAlias = explicitModel; + } else if (config.models) { + // Fuzzy lookup by prefix/suffix + const lower = explicitModel.toLowerCase(); + for (const alias of Object.keys(config.models)) { + if (alias.toLowerCase() === lower || alias.toLowerCase().endsWith('/' + lower) || alias.toLowerCase().includes(lower)) { + activeAlias = alias; + break; + } + } + } + if (!activeAlias) activeAlias = explicitModel; + } + + // 2. Environment variable + if (!activeAlias) { + activeAlias = process.env.KIMI_SESSION_MODEL || process.env.KIMI_MODEL; + } + // 3. Inspect recent active session wire.jsonl + if (!activeAlias) { + activeAlias = detectCurrentSessionModel(config); + } + + // 4. Fallback to default_model in config.toml + if (!activeAlias) { + activeAlias = config.default_model; + } + + // 5. First model defined in [models.*] + const modelKeys = Object.keys(config.models || {}); if (!activeAlias && modelKeys.length > 0) { activeAlias = modelKeys[0]; } diff --git a/plugins/official/kimi-effort/scripts/effort-cli.mjs b/plugins/official/kimi-effort/scripts/effort-cli.mjs index e6ba7dc4fdd..cf0ed8be86c 100644 --- a/plugins/official/kimi-effort/scripts/effort-cli.mjs +++ b/plugins/official/kimi-effort/scripts/effort-cli.mjs @@ -100,6 +100,7 @@ const c = { green: (t) => `${colors.green}${t}${colors.reset}`, yellow: (t) => `${colors.yellow}${t}${colors.reset}`, blue: (t) => `${colors.blue}${t}${colors.reset}`, + magenta: (t) => `${colors.magenta}${t}${colors.reset}`, cyan: (t) => `${colors.cyan}${t}${colors.reset}`, gray: (t) => `${colors.gray}${t}${colors.reset}`, tag: (t, color = colors.cyan) => `${color}[${t}]${colors.reset}` @@ -139,9 +140,13 @@ async function handleStatus(targetModelAlias) { const { config, path: configPath } = configManager.loadConfig(); let modelAlias = targetModelAlias; + let isSessionActive = false; if (!modelAlias) { - const current = configManager.getCurrentModelInfo(); + const current = configManager.getCurrentModelInfo(null); modelAlias = current.activeModelAlias; + if (modelAlias && modelAlias !== config.default_model) { + isSessionActive = true; + } } if (!modelAlias) { @@ -171,7 +176,16 @@ async function handleStatus(targetModelAlias) { console.log(`\n${c.bold('=== Kimi Code Reasoning Effort Status ===')}\n`); console.log(` ${c.bold('Config File:')} ${configPath}`); - console.log(` ${c.bold('Model Alias:')} ${c.cyan(modelAlias)}${(!targetModelAlias && modelAlias === config.default_model) ? c.dim(' (default_model)') : ''}`); + + let aliasTag = ''; + if (!targetModelAlias) { + if (isSessionActive) { + aliasTag = c.green(' (active in current session)'); + } else if (modelAlias === config.default_model) { + aliasTag = c.dim(' (default_model in config.toml)'); + } + } + console.log(` ${c.bold('Model Alias:')} ${c.cyan(modelAlias)}${aliasTag}`); console.log(` ${c.bold('Actual Model:')} ${actualModelName}`); console.log(` ${c.bold('Provider:')} ${providerKey ? c.blue(providerKey) : c.dim('(unknown)')} ${providerConfig ? c.dim(`[${providerConfig.type || 'openai'}]`) : ''}`); @@ -182,11 +196,15 @@ async function handleStatus(targetModelAlias) { console.log(` ${c.bold('Current Effort:')} ${c.bold(c.cyan(currentEffort))}${modelDefaultEffort ? c.dim(' (model default)') : (globalEffort ? c.dim(' (global thinking.effort)') : c.dim(' (fallback)'))}`); if (supportedEfforts.length > 0) { - console.log(` ${c.bold('Supported Levels:')} [ ${supportedEfforts.map(e => (e === currentEffort ? c.bold(c.green(e)) : e)).join(', ')} ]`); + console.log(` ${c.bold('Supported Levels:')} [ ${supportedEfforts.map(e => (e === 'max' ? c.bold(c.magenta('max')) : (e === currentEffort ? c.bold(c.green(e)) : e))).join(', ')} ]`); } else { console.log(` ${c.bold('Supported Levels:')} ${c.yellow('Not detected yet')} ${c.dim('(Run "effort-cli.mjs detect" to detect)')}`); } - console.log(''); + + // Helpful guidance for model switching in TUI + console.log(`\n ${c.bold(c.yellow('💡 Notice on /effort in TUI:'))}`); + console.log(` When switching models in an active session, run ${c.bold(c.cyan('/reload'))} in Kimi Code`); + console.log(` to ensure the in-memory TUI picker reflects this model's latest effort levels (e.g. max).\n`); } /**