Skip to content

feat: reasoning_content in API for reasoning models#6322

Merged
DOsinga merged 6 commits intoblock:mainfrom
Abhijay007:feat/reasoningContent
Feb 12, 2026
Merged

feat: reasoning_content in API for reasoning models#6322
DOsinga merged 6 commits intoblock:mainfrom
Abhijay007:feat/reasoningContent

Conversation

@Abhijay007
Copy link
Collaborator

@Abhijay007 Abhijay007 commented Jan 2, 2026

closes #6192

PR description

This PR adds support for the reasoning_content field used by reasoning models like DeepSeek's reasoner, enabling goose to capture and display the step-by-step reasoning process that these models expose separately from their main response content.

Type of Change

  • Feature

AI Assistance

  • This PR was created or reviewed with AI assistance, used goose for approach and code

Testing

Tested on the desktop UI using the DeepSeek-Reasoner with sample “thinking” prompts.

Screenshots/Demos (for UX changes)

deepLearning

Copilot AI review requested due to automatic review settings January 2, 2026 18:27
@Abhijay007 Abhijay007 marked this pull request as draft January 2, 2026 18:27
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for the reasoning_content field used by reasoning models like DeepSeek's reasoner, enabling goose to capture and display the step-by-step reasoning process that these models expose separately from their main response content.

Key Changes:

  • Added ReasoningContent type to the message content system for storing model reasoning steps
  • Implemented bidirectional handling in OpenAI-compatible format (both request formatting and response parsing, including streaming)
  • Updated all provider formatters to appropriately skip reasoning content for non-OpenAI providers

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
crates/goose/src/conversation/message.rs Added ReasoningContent struct and enum variant with helper methods (reasoning(), as_reasoning())
crates/goose/src/providers/formats/openai.rs Implemented full support for reasoning_content in message formatting, response parsing, and streaming responses with comprehensive tests
crates/goose/src/providers/formats/anthropic.rs Added skip logic for reasoning content (not used by Anthropic)
crates/goose/src/providers/formats/bedrock.rs Added skip logic for reasoning content (not used by Bedrock)
crates/goose/src/providers/formats/databricks.rs Added skip logic for reasoning content (not used by Databricks)
crates/goose/src/providers/formats/snowflake.rs Added skip logic for reasoning content (not used by Snowflake)
crates/goose/src/context_mgmt/mod.rs Added formatting support for reasoning content in conversation compaction
crates/goose-server/src/openapi.rs Exported ReasoningContent type for API schema generation
ui/desktop/openapi.json Added OpenAPI schema definition for ReasoningContent type
ui/desktop/src/api/types.gen.ts Generated TypeScript types for the new reasoning content variant

@Abhijay007 Abhijay007 marked this pull request as ready for review January 8, 2026 17:20
Copilot AI review requested due to automatic review settings January 8, 2026 17:20
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment on lines +254 to +255
// DeepSeek requires reasoning_content field when tool_calls are present
// Set it to the captured reasoning text, or empty string if not present
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The comment states "DeepSeek requires reasoning_content field when tool_calls are present" but this logic applies to all OpenAI-compatible providers. While OpenAI APIs typically ignore unknown fields, consider clarifying the comment to indicate this behavior affects all OpenAI-compatible providers, not just DeepSeek. Additionally, setting reasoning_content to an empty string when tool_calls exist but no reasoning was provided might not be necessary for all providers.

Suggested change
// DeepSeek requires reasoning_content field when tool_calls are present
// Set it to the captured reasoning text, or empty string if not present
// For OpenAI-compatible providers, include reasoning_content when tool_calls are present
// Use the captured reasoning text if available, otherwise an empty string for compatibility

Copilot uses AI. Check for mistakes.
@taniacryptid
Copy link
Contributor

Thank you for working on this and the other PRs! Tagging @block/goose-maintainers @block/goose-core-maintainers for each to have a look ❤️

Copy link
Collaborator

@jamadeo jamadeo left a comment

Choose a reason for hiding this comment

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

thank you @Abhijay007 !

main question is around "thinking" vs "reasoning" -- I think we called it Reasoning in the internal type because it was first modeled after the anthropic format, but I think we could/should use just one for the internal type, unless there are real incompatibilities we'd need to address between providers

@zanesq
Copy link
Collaborator

zanesq commented Jan 26, 2026

@Abhijay007 checking if still in progress?

@Abhijay007
Copy link
Collaborator Author

@Abhijay007 checking if still in progress?

Will look into this soon, will share an update

@michange
Copy link

Which reasoning/thinking models should this PR support?
What about ACP (= Agent Client Protocol) standard for client↔agent communication. Goose is adopting it. May it eventually cover event streaming ?

@DOsinga
Copy link
Collaborator

DOsinga commented Feb 11, 2026

can we get this landed? @Abhijay007 - if you address @alexhancock 's last comments and match main, let's merge?

@Abhijay007
Copy link
Collaborator Author

can we get this landed? @Abhijay007 - if you address @alexhancock 's last comments and match main, let's merge?

Will look into this today will resolve it asap

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment on lines +264 to +274
// DeepSeek requires reasoning_content field when tool_calls are present
// Set it to the captured reasoning text, or empty string if not present
if converted.get("tool_calls").is_some() {
let reasoning = reasoning_text.unwrap_or_default();
converted["reasoning_content"] = json!(reasoning);
} else if let Some(reasoning) = reasoning_text {
if !reasoning.is_empty() {
converted["reasoning_content"] = json!(reasoning);
}
}

Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

format_messages unconditionally adds a reasoning_content field whenever tool_calls are present (and even sets it to an empty string), which will be included in requests for all OpenAI/OpenAI-compatible providers using this formatter and can cause 400s on providers that don’t recognize this non-standard message field. Gate emitting reasoning_content behind an explicit capability check (e.g., model/provider is DeepSeek reasoner) or move this into a provider-specific request mutator similar to openrouter::add_reasoning_details_to_request, and avoid sending the field at all for providers that don’t require it.

Suggested change
// DeepSeek requires reasoning_content field when tool_calls are present
// Set it to the captured reasoning text, or empty string if not present
if converted.get("tool_calls").is_some() {
let reasoning = reasoning_text.unwrap_or_default();
converted["reasoning_content"] = json!(reasoning);
} else if let Some(reasoning) = reasoning_text {
if !reasoning.is_empty() {
converted["reasoning_content"] = json!(reasoning);
}
}

Copilot uses AI. Check for mistakes.
Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
…ompatibility

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
Copilot AI review requested due to automatic review settings February 12, 2026 14:07
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

continue;
}
MessageContent::Reasoning(r) => {
reasoning_text = Some(r.text.clone());
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

format_messages only keeps the last MessageContent::Reasoning block (reasoning_text = Some(...)), but streaming aggregation can append many reasoning deltas to a single message id, so earlier reasoning is silently dropped when re-sending conversation history; accumulate/append all reasoning parts instead of overwriting.

Suggested change
reasoning_text = Some(r.text.clone());
match &mut reasoning_text {
Some(existing) => {
if !existing.is_empty() {
existing.push('\n');
}
existing.push_str(&r.text);
}
None => {
reasoning_text = Some(r.text.clone());
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +285 to +290
// DeepSeek requires reasoning_content field when tool_calls are present
// Set it to the captured reasoning text, or empty string if not present
if converted.get("tool_calls").is_some() {
let reasoning = reasoning_text.unwrap_or_default();
converted["reasoning_content"] = json!(reasoning);
} else if let Some(reasoning) = reasoning_text {
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

reasoning_content is currently added to any message that has tool_calls, even though the comment says it’s a DeepSeek-specific requirement; this risks sending a non-standard field to other OpenAI-compatible providers, so gate it behind a DeepSeek/reasoning-model check (e.g., model name/provider capability) rather than only tool_calls presence.

Suggested change
// DeepSeek requires reasoning_content field when tool_calls are present
// Set it to the captured reasoning text, or empty string if not present
if converted.get("tool_calls").is_some() {
let reasoning = reasoning_text.unwrap_or_default();
converted["reasoning_content"] = json!(reasoning);
} else if let Some(reasoning) = reasoning_text {
// Only include reasoning_content when we actually have captured reasoning text.
if let Some(reasoning) = reasoning_text {

Copilot uses AI. Check for mistakes.
Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
@DOsinga DOsinga added this pull request to the merge queue Feb 12, 2026
Merged via the queue into block:main with commit 738f5f6 Feb 12, 2026
19 checks passed
tlongwell-block added a commit that referenced this pull request Feb 12, 2026
…provenance

* origin/main: (68 commits)
  Upgraded npm packages for latest security updates (#7183)
  docs: reasoning effort levels for Codex provider (#6798)
  Fix speech local (#7181)
  chore: add .gooseignore to .gitignore (#6826)
  Improve error message logging from electron (#7130)
  chore(deps): bump jsonwebtoken from 9.3.1 to 10.3.0 (#6924)
  docs: standalone mcp apps and apps extension (#6791)
  workflow: auto-update cli-commands on release (#6755)
  feat(apps): Integrate AppRenderer from @mcp-ui/client SDK (#7013)
  fix(MCP): decode resource content (#7155)
  feat: reasoning_content in API for reasoning models (#6322)
  Fix/configure add provider custom headers (#7157)
  fix: handle keyring fallback as success (#7177)
  Update process-wrap to 9.0.3 (9.0.2 is yanked) (#7176)
  feat: support extra field in chatcompletion tool_calls for gemini openai compat (#6184)
  fix: replace panic with proper error handling in get_tokenizer (#7175)
  Lifei/smoke test for developer (#7174)
  fix text editor view broken (#7167)
  docs: White label guide (#6857)
  Add PATH detection back to developer extension (#7161)
  ...

# Conflicts:
#	.github/workflows/nightly.yml
jh-block added a commit that referenced this pull request Feb 13, 2026
* origin/main: (21 commits)
  nit: show dir in title, and less... jank (#7138)
  feat(gemini-cli): use stream-json output and re-use session (#7118)
  chore(deps): bump qs from 6.14.1 to 6.14.2 in /documentation (#7191)
  Switch jsonwebtoken to use aws-lc-rs (already used by rustls) (#7189)
  chore(deps): bump qs from 6.14.1 to 6.14.2 in /evals/open-model-gym/mcp-harness (#7184)
  Add SLSA build provenance attestations to release workflows (#7097)
  fix save and run recipe not working (#7186)
  Upgraded npm packages for latest security updates (#7183)
  docs: reasoning effort levels for Codex provider (#6798)
  Fix speech local (#7181)
  chore: add .gooseignore to .gitignore (#6826)
  Improve error message logging from electron (#7130)
  chore(deps): bump jsonwebtoken from 9.3.1 to 10.3.0 (#6924)
  docs: standalone mcp apps and apps extension (#6791)
  workflow: auto-update cli-commands on release (#6755)
  feat(apps): Integrate AppRenderer from @mcp-ui/client SDK (#7013)
  fix(MCP): decode resource content (#7155)
  feat: reasoning_content in API for reasoning models (#6322)
  Fix/configure add provider custom headers (#7157)
  fix: handle keyring fallback as success (#7177)
  ...
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.

[Feature] reasoning_content in API for reasoning models

7 participants