feat: reasoning_content in API for reasoning models#6322
Conversation
There was a problem hiding this comment.
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
ReasoningContenttype 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 |
| // DeepSeek requires reasoning_content field when tool_calls are present | ||
| // Set it to the captured reasoning text, or empty string if not present |
There was a problem hiding this comment.
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.
| // 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 |
|
Thank you for working on this and the other PRs! Tagging @block/goose-maintainers @block/goose-core-maintainers for each to have a look ❤️ |
jamadeo
left a comment
There was a problem hiding this comment.
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
|
@Abhijay007 checking if still in progress? |
Will look into this soon, will share an update |
|
Which reasoning/thinking models should this PR support? |
|
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 |
1fdd7a2 to
2c98a7b
Compare
2c98a7b to
b8a0a89
Compare
| // 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); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| // 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); | |
| } | |
| } |
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>
36254d4 to
1cad693
Compare
| continue; | ||
| } | ||
| MessageContent::Reasoning(r) => { | ||
| reasoning_text = Some(r.text.clone()); |
There was a problem hiding this comment.
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.
| 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()); | |
| } | |
| } |
| // 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 { |
There was a problem hiding this comment.
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.
| // 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 { |
Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
…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
* 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) ...
closes #6192
PR description
This PR adds support for the
reasoning_contentfield 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
AI Assistance
Testing
Tested on the desktop UI using the
DeepSeek-Reasonerwith sample “thinking” prompts.Screenshots/Demos (for UX changes)