Skip to content

fix: add reasoning_content field for Moonshot AI thinking-enabled models - #364

Merged
jamiepine merged 7 commits into
spacedriveapp:mainfrom
chanyeinthaw:fix/moonshot-reasoning-content
Aug 8, 2026
Merged

fix: add reasoning_content field for Moonshot AI thinking-enabled models#364
jamiepine merged 7 commits into
spacedriveapp:mainfrom
chanyeinthaw:fix/moonshot-reasoning-content

Conversation

@chanyeinthaw

@chanyeinthaw chanyeinthaw commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #245 - Moonshot AI models with thinking enabled fail with error: "thinking is enabled but reasoning_content is missing in assistant tool call message".

Root Cause

Moonshot AI models (kimi-k2.5, kimi-k2.5-nvfp4, etc.) that have reasoning: true capability require the reasoning_content field in assistant tool call messages when tool calls are present. Without this field, the API returns a validation error.

The error occurred because convert_messages_to_openai() was not adding the reasoning_content field to assistant messages containing tool calls for reasoning-capable models.

Key Changes

Detect reasoning-capable models

Added needs_reasoning_content() method in SpacebotModel that:

  • Checks the ModelInfo reasoning flag from the cached models list
  • Validates against model name patterns (k2.5, k2-5, thinking, reasoning)

Add reasoning_content to assistant tool call messages

Updated convert_messages_to_openai() to:

  • Accept a needs_reasoning_content boolean parameter
  • Add "reasoning_content": "." field to assistant messages when:
    • The model requires reasoning content
    • The message contains tool_calls
    • The field is not already present

Pass reasoning flag through all provider paths

Updated the OpenAI-compatible provider methods to:

  • Call needs_reasoning_content() before message conversion
  • Pass the flag to convert_messages_to_openai()

Files Changed

File What changed
src/llm/model.rs Added needs_reasoning_content() method, updated convert_messages_to_openai() signature and implementation, wired flag through OpenAI-compatible provider paths

Testing

  • just gate-pr passes (formatting, clippy, tests, compilation)
  • Tested manually with opencode-go/kimi-k2.5 as worker model
  • Verified tool calls work correctly with reasoning-enabled models
  • No more "reasoning_content is missing" errors

Core Fix

The fix adds the required reasoning_content field to assistant tool call messages for Moonshot AI thinking-enabled models. When a model with reasoning capability sends tool calls, the API now receives the mandatory reasoning_content field containing a placeholder value ("."), satisfying the validation requirements.

Note

This fix addresses a Moonshot AI API requirement where reasoning-capable models must include the reasoning_content field when sending tool calls. The solution detects reasoning-enabled models via the ModelInfo cache and model name patterns, then injects a placeholder value into assistant tool call messages. This ensures compatibility with Kimi models (kimi-k2.5 and variants) while remaining transparent to non-reasoning models.

Written by Tembo for commit c590b65. This will update automatically on new commits.

@coderabbitai

coderabbitai Bot commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This change adds support for models that require a reasoning_content field in tool call messages. It introduces a method to detect models requiring this field (Kimi 2.5, Claude thinking models) and augments the OpenAI message conversion logic to include reasoning_content when appropriate.

Changes

Cohort / File(s) Summary
Model reasoning content support
src/llm/model.rs
Added needs_reasoning_content() method to detect models requiring reasoning_content in tool calls. Updated convert_messages_to_openai() signature to accept a needs_reasoning_content parameter and augment tool_call branches with a reasoning_content field (default ".") when needed. Updated all call sites across streaming and non-streaming OpenAI/OpenRouter code paths to propagate this parameter.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • #304: Directly modifies the same src/llm/model.rs file and handles reasoning_content as an incoming fallback, complementing this PR's outgoing reasoning_content augmentation.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding reasoning_content field for Moonshot AI thinking-enabled models.
Linked Issues check ✅ Passed The PR fully addresses issue #245 by implementing reasoning_content field injection for reasoning-capable models to resolve the API validation error.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the reasoning_content validation error for Moonshot AI models, with no unrelated modifications present.
Description check ✅ Passed The PR description clearly relates to the changeset, detailing the fix for reasoning_content field handling in Moonshot AI models with specific root causes, changes, and testing results.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/api.rs
Comment thread src/api/models.rs Outdated
Comment thread src/llm/model.rs Outdated
Comment thread src/llm/model.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/api/models.rs (1)

267-270: Clarify whether this is the full catalog or only the models.dev cache.

get_models() appends extra_models() after the cache lookup, so this accessor is narrower than the catalog the rest of the file works with. Callers like src/llm/model.rs can therefore miss local-only entries such as minimax-cn/MiniMax-M2.5. Either include extra_models() here or rename the API so the contract is obvious.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/models.rs` around lines 267 - 270, The current get_models_cache()
returns only the cached models from ensure_models_cache() which is narrower than
the full catalog used elsewhere (get_models() appends extra_models()), so update
get_models_cache() to return the full catalog by calling
ensure_models_cache().await, then extend/append the Vec<ModelInfo> with the
results of extra_models() (ensuring extra_models() is awaited/collected as
needed) so callers (e.g., code using get_models_cache() in model resolution)
receive both cache and local-only entries like minimax-cn/MiniMax-M2.5;
alternatively, if you prefer a cache-only API, rename get_models_cache() to make
the cache-only contract explicit and update all callers accordingly.
src/llm/model.rs (1)

1310-1403: Add a regression test for the assistant tool-call history branch.

This PR fixes a very specific serialization shape, but there isn't a unit test proving convert_messages_to_openai() adds reasoning_content only for assistant messages with tool_calls when the flag is enabled. A small test pair here would make this much harder to regress.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/llm/model.rs` around lines 1310 - 1403, Add a regression unit test for
convert_messages_to_openai that verifies when needs_reasoning_content is true
the function only injects the "reasoning_content" field for Assistant messages
that include tool_calls and not for User messages or Assistant messages without
tool_calls; construct test inputs with (1) an Assistant message containing a
ToolCall, (2) an Assistant message with only text, and (3) a User message with
tool results, call convert_messages_to_openai(needs_reasoning_content = true)
and assert the serialized outputs include reasoning_content only on the
assistant tool-call message and are unchanged otherwise; place the test
alongside other unit tests for model serialization and reference
convert_messages_to_openai, needs_reasoning_content, tool_calls, and
reasoning_content in assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/llm/model.rs`:
- Around line 85-109: The function needs_reasoning_content currently awaits
get_models_cache() before checking local model-name heuristics, which can block
or return false on cache failure; change the logic in needs_reasoning_content to
first perform the fast local substring check on self.model_name (look for
"k2.5", "k2-5", "thinking", "reasoning") and return true immediately if matched,
and only then asynchronously query crate::api::get_models_cache() to consult
ModelInfo.reasoning for models not matched by the heuristic; also ensure any
cache errors/defaults do not short-circuit the heuristic (i.e., treat cache
failures as unknown and fall back to the local check result).

---

Nitpick comments:
In `@src/api/models.rs`:
- Around line 267-270: The current get_models_cache() returns only the cached
models from ensure_models_cache() which is narrower than the full catalog used
elsewhere (get_models() appends extra_models()), so update get_models_cache() to
return the full catalog by calling ensure_models_cache().await, then
extend/append the Vec<ModelInfo> with the results of extra_models() (ensuring
extra_models() is awaited/collected as needed) so callers (e.g., code using
get_models_cache() in model resolution) receive both cache and local-only
entries like minimax-cn/MiniMax-M2.5; alternatively, if you prefer a cache-only
API, rename get_models_cache() to make the cache-only contract explicit and
update all callers accordingly.

In `@src/llm/model.rs`:
- Around line 1310-1403: Add a regression unit test for
convert_messages_to_openai that verifies when needs_reasoning_content is true
the function only injects the "reasoning_content" field for Assistant messages
that include tool_calls and not for User messages or Assistant messages without
tool_calls; construct test inputs with (1) an Assistant message containing a
ToolCall, (2) an Assistant message with only text, and (3) a User message with
tool results, call convert_messages_to_openai(needs_reasoning_content = true)
and assert the serialized outputs include reasoning_content only on the
assistant tool-call message and are unchanged otherwise; place the test
alongside other unit tests for model serialization and reference
convert_messages_to_openai, needs_reasoning_content, tool_calls, and
reasoning_content in assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b3af23a4-17ff-4802-8b30-118529e54d74

📥 Commits

Reviewing files that changed from the base of the PR and between 7b3b4a6 and 0f0ec70.

📒 Files selected for processing (3)
  • src/api.rs
  • src/api/models.rs
  • src/llm/model.rs

Comment thread src/llm/model.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/llm/model.rs (1)

85-107: ⚠️ Potential issue | 🟠 Major

Cache lookup gates the heuristic, causing false negatives on cache miss.

The current logic checks the ModelInfo.reasoning flag first and returns false early if the cache lookup fails or the model isn't found (line 92 unwrap_or(false)). This means that for known reasoning models like Kimi K2.5, if the models.dev fetch fails or the model isn't in the catalog, the function returns false — and the original reasoning_content is missing API error can still occur.

Consider reordering to check the cheap pattern heuristic first, so known reasoning model names still get the field injected even when the cache is unavailable:

🛠️ Suggested reordering
     async fn needs_reasoning_content(&self) -> bool {
+        // Fast path: check model name patterns first (no network/cache dependency)
+        let lower = self.model_name.to_lowercase();
+        if lower.contains("k2.5")
+            || lower.contains("k2-5")
+            || lower.contains("thinking")
+            || lower.contains("reasoning")
+        {
+            return true;
+        }
+
         // Check ModelInfo reasoning flag from cache (lookup single model instead of cloning entire catalog)
         let reasoning_enabled = crate::api::lookup_model(&self.full_model_name)
             .await
             .map(|m| m.reasoning)
             .unwrap_or(false);
-        if !reasoning_enabled {
-            return false;
-        }
-
-        // check model name patterns
-        let lower = self.model_name.to_lowercase();
-        if lower.contains("k2.5")
-            || lower.contains("k2-5")
-            || lower.contains("thinking")
-            || lower.contains("reasoning")
-        {
-            return true;
-        }
-        false
+        reasoning_enabled
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/llm/model.rs` around lines 85 - 107, The heuristic check in
needs_reasoning_content currently gates pattern detection behind the cache
lookup (crate::api::lookup_model -> ModelInfo.reasoning), causing false
negatives on cache miss; change the logic in needs_reasoning_content to evaluate
the cheap model-name pattern first (inspect self.model_name.to_lowercase() for
"k2.5", "k2-5", "thinking", "reasoning") and return true if matched, and only if
the pattern does not match then consult
crate::api::lookup_model(&self.full_model_name).await.map(|m|
m.reasoning).unwrap_or(false) to decide the final result, so known reasoning
models still require reasoning_content even when the catalog lookup fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/llm/model.rs`:
- Around line 85-107: The heuristic check in needs_reasoning_content currently
gates pattern detection behind the cache lookup (crate::api::lookup_model ->
ModelInfo.reasoning), causing false negatives on cache miss; change the logic in
needs_reasoning_content to evaluate the cheap model-name pattern first (inspect
self.model_name.to_lowercase() for "k2.5", "k2-5", "thinking", "reasoning") and
return true if matched, and only if the pattern does not match then consult
crate::api::lookup_model(&self.full_model_name).await.map(|m|
m.reasoning).unwrap_or(false) to decide the final result, so known reasoning
models still require reasoning_content even when the catalog lookup fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa94efbd-d0b3-428d-a694-b9087de11d88

📥 Commits

Reviewing files that changed from the base of the PR and between 4dae98d and 2f083aa.

📒 Files selected for processing (3)
  • src/api.rs
  • src/api/models.rs
  • src/llm/model.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/api.rs

@chanyeinthaw
chanyeinthaw force-pushed the fix/moonshot-reasoning-content branch 3 times, most recently from 6f56874 to b96e0a7 Compare March 8, 2026 14:54
@chanyeinthaw
chanyeinthaw force-pushed the fix/moonshot-reasoning-content branch from b96e0a7 to 13727d2 Compare March 8, 2026 14:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/llm/model.rs (1)

85-92: Consider tightening the substring patterns to reduce false-positive risk.

The "k2.5" and "k2-5" patterns correctly target Kimi models. However, "thinking" and "reasoning" are generic and don't match any models in the current routing configuration (per defaults_for_provider in routing.rs). These could inadvertently match future models that don't actually need reasoning_content.

If this is intentional future-proofing, it's fine since most APIs ignore unknown fields. If you want tighter matching, consider a provider check (e.g., only apply for moonshot/opencode providers) or remove the speculative patterns until specific models require them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/llm/model.rs` around lines 85 - 92, The needs_reasoning_content method
currently matches generic substrings ("thinking" and "reasoning") that may
produce false positives; update the implementation in needs_reasoning_content to
tighten matching by either (A) removing the speculative "thinking" and
"reasoning" checks and only keeping explicit Kimi patterns ("k2.5", "k2-5"), or
(B) guard the generic checks with a provider check (e.g., ensure self.provider
is Moonshot or Opencode as per defaults_for_provider in routing.rs) so only
known providers apply the extra field; modify needs_reasoning_content
accordingly to reference self.model_name and self.provider (or equivalent) and
ensure behavior is consistent with defaults_for_provider in routing.rs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/llm/model.rs`:
- Around line 85-92: The needs_reasoning_content method currently matches
generic substrings ("thinking" and "reasoning") that may produce false
positives; update the implementation in needs_reasoning_content to tighten
matching by either (A) removing the speculative "thinking" and "reasoning"
checks and only keeping explicit Kimi patterns ("k2.5", "k2-5"), or (B) guard
the generic checks with a provider check (e.g., ensure self.provider is Moonshot
or Opencode as per defaults_for_provider in routing.rs) so only known providers
apply the extra field; modify needs_reasoning_content accordingly to reference
self.model_name and self.provider (or equivalent) and ensure behavior is
consistent with defaults_for_provider in routing.rs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4872b500-dd4f-4a87-843f-ece1f966a5b7

📥 Commits

Reviewing files that changed from the base of the PR and between 6f56874 and 13727d2.

📒 Files selected for processing (1)
  • src/llm/model.rs

@jamiepine jamiepine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Validated against repaired CI; fixed the test call sites for the new needs_reasoning_content flag as a maintainer edit.

@jamiepine
jamiepine merged commit 62b9642 into spacedriveapp:main Aug 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants