Skip to content

feat(plugins): add kimi-effort plugin for reasoning effort auto-detec… - #3661

Open
WENGENG-boop wants to merge 3 commits into
MoonshotAI:mainfrom
WENGENG-boop:feat/plugin-effort
Open

feat(plugins): add kimi-effort plugin for reasoning effort auto-detec…#3661
WENGENG-boop wants to merge 3 commits into
MoonshotAI:mainfrom
WENGENG-boop:feat/plugin-effort

Conversation

@WENGENG-boop

Copy link
Copy Markdown
 ## Related Issue

 <!-- Link the issue this change came from. External PRs must link an issue approved by a maintainer (an `/approve` comment) — PRs without one

may be closed. -->

 Resolve #

 ## Problem

 When connecting third-party OpenAI-compatible, Anthropic, or Google GenAI provider models (such as o1/o3-mini, Claude 3.7 Sonnet, Gemini Flash

Thinking, DeepSeek-R1, and custom gateways), the CLI does not automatically detect whether a model supports reasoning effort adjustments unless
manually hardcoded with support_efforts and default_effort in config.toml.

 Users also lacked an interactive slash command to inspect active reasoning capabilities and adjust thinking effort levels dynamically on the

fly.

 ## What changed

 Added the official `kimi-effort` plugin to provide non-intrusive automatic capability detection and interactive `/effort` adjustment:

 1. **Official Plugin Implementation (`plugins/official/kimi-effort/`)**:
    - `scripts/effort-detector.mjs`: Implements live API probe tests (`reasoning_effort`, Anthropic `budget_tokens`, Google `thinkingConfig`)

combined with intelligent heuristic sniffing across common reasoning model families (o1/o3/gpt-5/6, claude-3-7/opus-4, gemini-2.5/3,
deepseek-r1).
- scripts/config-manager.mjs: Provides non-destructive read/write logic for config.toml, updating support_efforts, default_effort,
and ensuring "thinking" is present in model capabilities while preserving comments, indentation, and structure.
- scripts/effort-cli.mjs: Command-line engine supporting status, detect [model|all], set <effort> [model], and list operations.
- skills/effort/SKILL.md & commands/effort.md: Exposes the /effort slash command for querying and tuning effort levels (low,
medium, high, max).
- hooks/session-start.mjs: Lightweight, fail-open SessionStart lifecycle hook (< 2s budget) that automatically triggers background
detection when an unconfigured model is active.

 2. **Marketplace Registration (`plugins/marketplace.json`)**:
    - Registered `kimi-effort` under the `official` tier in the default marketplace catalog.

 3. **Changeset**:
    - Added `.changeset/add-kimi-effort-plugin.md` with a `minor` bump for `@moonshot-ai/kimi-code`.

@changeset-bot

changeset-bot Bot commented Sep 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 10fb652

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a7b02c4ee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +184 to +189
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve multiline TOML arrays before updating them

When a model uses a valid multiline support_efforts array, parseTomlValue records only the opening "[", so the SessionStart hook treats the model as unconfigured; this replacement then rewrites only the first line while leaving the array elements and closing bracket behind, producing an invalid config.toml. Parse the file with a TOML-aware implementation or replace the complete multiline value span before writing.

Useful? React with 👍 / 👎.

Comment on lines +35 to +38
1. Run:
```bash
node "C:/Users/weo/plugins/kimi-effort/scripts/effort-cli.mjs" status $target
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Run the skill CLI from its installed directory

On every installation outside this author's Windows directory, the status action runs a nonexistent C:/Users/weo/... script; the set, detect, and list branches repeat the same path, so the plugin command cannot perform any advertised operation. Use the already-supported ${KIMI_SKILL_DIR}/../../scripts/effort-cli.mjs path in every action and remove the internal user identifier from public plugin text.

AGENTS.md reference: AGENTS.md:L73-L73

Useful? React with 👍 / 👎.

Comment on lines +2 to +3
name: effort
description: Check, auto-detect, or adjust reasoning/thinking effort level for models in Kimi Code CLI

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Route /effort subcommands to a reachable handler

The TUI resolves built-in commands before skills or plugin commands, and /effort is already a built-in that only accepts a single effort level. Consequently /effort detect, /effort list, and the other advertised plugin flows never load this command and instead report an unsupported effort; this plugin command is only reachable as /kimi-effort:effort. Either wire these subcommands into the built-in handler or advertise and use the namespaced command.

Useful? React with 👍 / 👎.

Comment on lines +113 to +119
// 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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remember completed non-reasoning detections

When detection concludes that a model is non-reasoning, it saves support_efforts = [], but this condition considers only a non-empty array configured. Every subsequent SessionStart therefore performs the live POST and metadata probes again, repeatedly adding startup latency and unnecessary provider traffic for the same model. Treat an explicitly present empty support_efforts value, or a separate detection marker, as a completed result.

Useful? React with 👍 / 👎.

Comment on lines +290 to +295
if (res.ok) {
// 200 OK with reasoning_effort accepted!
return {
isReasoningModel: true,
supportedEfforts: ['low', 'medium', 'high'],
defaultEffort: 'medium',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid advertising effort levels that were never probed

A successful request with reasoning_effort: "low" proves at most that this one payload was accepted; third-party compatible gateways can support only a subset of levels or silently ignore unknown fields. Returning and persisting low, medium, and high here makes the CLI expose unverified choices, so selecting one can later fail or have no effect. Probe each level or obtain the supported set from authoritative provider metadata.

Useful? React with 👍 / 👎.

Comment on lines +112 to +121
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}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve installed plugin state when parsing fails

If installed.json is malformed or temporarily truncated, this catch keeps the fresh { plugins: [] } value and the later write replaces the original file, deleting every existing plugin registration. The core plugin manager deliberately refuses writes while this state is corrupt so it can be repaired; this installer should likewise abort or back up the file instead of treating a parse failure as an empty installation.

Useful? React with 👍 / 👎.

Comment on lines +498 to +501
// 1. Active Probing / Live Capability Probe
if (!options.skipProbe && providerConfig && providerConfig.base_url) {
try {
const probeResult = await probeModelEffort(providerConfig, actualModelName, options);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Probe providers that use their default endpoint

A provider base_url is optional in the supported configuration schema because the Anthropic and Google clients can select their standard endpoint themselves. This gate skips both live and metadata probing whenever that optional field is absent, so a valid provider using its default endpoint is reduced to model-name heuristics and custom-named reasoning models are classified as non-reasoning. Resolve the provider's effective endpoint or probe through the existing provider abstraction.

Useful? React with 👍 / 👎.

Comment on lines +404 to +406
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}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid duplicating the Google API version in probe URLs

Kimi Code forwards a configured Google base_url verbatim to the GenAI SDK, and valid configurations commonly include the API version, such as https://gateway.example/v1beta. This construction turns that into .../v1beta/v1beta/models/..., so every live probe receives a 404 and falls back to heuristics. Append only the model route when the configured URL already ends in /v1beta.

Useful? React with 👍 / 👎.

Comment on lines +465 to +470
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Probe supported OpenAI Responses providers

openai_responses is a supported Kimi Code provider type, but this dispatch handles only openai, Anthropic, and Google, so Responses providers never receive a live capability probe. Unless their model name happens to match a heuristic or their /models response exposes the expected nonstandard metadata, they are incorrectly recorded as non-reasoning despite supporting effort through the Responses API. Add an appropriate /responses probe for this provider type.

Useful? React with 👍 / 👎.

Comment on lines +326 to +329
const modelEntry = (config.models && config.models[modelAlias]) || null;
const supportedEfforts = (modelEntry && Array.isArray(modelEntry.support_efforts))
? modelEntry.support_efforts
: [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject unknown target aliases before updating config

When set receives a misspelled or nonexistent target alias, modelEntry becomes null but execution continues through the permissive standard-level branch. setThinkingEffort then changes the global [thinking].effort, cannot update the nonexistent model section, and the CLI reports that the named model was updated successfully. Match the existence validation used by status before performing any write.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 10fb652462

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +92 to +95
} else if (header.startsWith('models.')) {
let key = header.slice('models.'.length);
key = unquote(key);
result.models[key] = result.models[key] || {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve nested model overrides during detection

For a valid unquoted table such as [models.foo.overrides], this branch creates a separate foo.overrides model instead of attaching the table to foo. The advertised detect all flow—and the installer's automatic invocation of it—then probes that phantom model and calls saveModelEffort("foo.overrides", [], ""), overwriting the user's nested support_efforts and default_effort values with empty entries. Parse nested override tables as part of their parent model and exclude them from detection targets.

Useful? React with 👍 / 👎.

endIndex = lines.length;
} else {
const sectionLines = lines.slice(startIndex, endIndex);
updateKeyValueInLines(sectionLines, 'effort', `"${effortLevel}"`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep targeted changes from overwriting the global effort

When the optional target names an inactive model, this still rewrites the global [thinking].effort. The runtime's resolveThinkingEffort gives that global value precedence over model defaults, so set max claude while gemini is active also changes Gemini—and every subsequently selected model—to max; updating Claude's default_effort later in this function does not scope the selection. Either reject inactive targets or persist targeted selections without changing the global effort.

Useful? React with 👍 / 👎.

Comment on lines +412 to +414
thinkingConfig: {
thinkingBudget: 1024
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Probe Gemini 3 with thinking levels

For Gemini 3 models, this probe sends thinkingBudget, although the repository's Google provider explicitly uses thinkingLevel (MINIMAL, LOW, MEDIUM, or HIGH) for that family. A Gemini 3 endpoint that rejects the obsolete field returns a 400 mentioning thinkingConfig or thinkingBudget; the code consequently records the model as non-reasoning and persists an empty support_efforts list instead of falling back to the correct probe. Select the probe parameter according to the model family.

Useful? React with 👍 / 👎.

Comment on lines +352 to +356
max_tokens: 2048,
thinking: {
type: 'enabled',
budget_tokens: 1024
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Probe adaptive Anthropic models with their runtime payload

When a model has adaptive_thinking = true, or is a Claude 4.6-or-newer model inferred as adaptive, the runtime sends thinking: { type: "adaptive" } plus output_config.effort; this probe always sends the legacy enabled/budget form instead. Compatible endpoints that reject budget_tokens return a 400 mentioning thinking, which this detector treats as definitive evidence that the model is non-reasoning and then persists empty effort support. Honor modelConfig.adaptive_thinking and the same version inference used by the Anthropic provider before constructing the probe.

Useful? React with 👍 / 👎.

*/
export async function probeModelEffort(providerConfig, targetModel, options = {}) {
const timeoutMs = options.timeout || 5000;
const providerType = (providerConfig?.type || 'openai').toLowerCase();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor per-model protocol overrides when probing

When a model declares protocol = "anthropic", the runtime routes it through the Anthropic transport regardless of its provider's configured type, but this dispatch considers only providerConfig.type. A model on a kimi provider therefore receives no live probe, while one on an openai provider is probed through /chat/completions instead of /messages; custom-named reasoning models then fall through to heuristics and are persisted as non-reasoning. Derive the probe transport from modelConfig.protocol before falling back to the provider type.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant