Skip to content

feat(go/plugins/compat_oai): support reasoning_content for moonshot#5154

Open
naqerl wants to merge 6 commits intogenkit-ai:mainfrom
naqerl:naqerl/go-oai-compat-reasoning
Open

feat(go/plugins/compat_oai): support reasoning_content for moonshot#5154
naqerl wants to merge 6 commits intogenkit-ai:mainfrom
naqerl:naqerl/go-oai-compat-reasoning

Conversation

@naqerl
Copy link
Copy Markdown

@naqerl naqerl commented Apr 19, 2026

This change adds support for preserving reasoning_content in OpenAI-compatible APIs that require it (specifically Moonshot/Kimi). Without this fix, follow-up tool-result requests are rejected with "reasoning_content is missing" error.

The implementation:

  • Public API was not changed, only added internal behavior for API's that return reasoning
  • Extracts reasoning content from message parts when building assistant messages
  • Preserves reasoning metadata (reasoningKey) for round-trip compatibility
  • Parses reasoning fields from API responses using common field names (reasoning_content, reasoning, reasoning_text)
  • Adds live test for Moonshot-compatible backends

ISSUE: N/A

CHANGELOG:

  • Add reasoning_content preservation in WithMessages for tool calls
  • Add reasoning field extraction from API responses
  • Add Moonshot live test for end-to-end verification

Description here... Help the reviewer by:

  • linking to an issue that includes more details
  • if it's a new feature include samples of how to use the new feature
  • (optional if issue link is provided) if you fixed a bug include basic bug details

Checklist (if applicable):

…imi compatibility

This change adds support for preserving reasoning_content in OpenAI-compatible
APIs that require it (specifically Moonshot/Kimi). Without this fix, follow-up
tool-result requests are rejected with "reasoning_content is missing" error.

The implementation:
- Extracts reasoning content from message parts when building assistant messages
- Preserves reasoning metadata (reasoningKey) for round-trip compatibility
- Parses reasoning fields from API responses using common field names
(reasoning_content, reasoning, reasoning_text)
- Adds live test for Moonshot-compatible backends

ISSUE: N/A

CHANGELOG:
- [x] Add reasoning_content preservation in WithMessages for tool calls
- [x] Add reasoning field extraction from API responses
- [x] Add Moonshot live test for end-to-end verification
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for reasoning content in OpenAI-compatible providers such as Moonshot/Kimi. It implements logic to extract reasoning fields from response metadata, prepend them to message content, and preserve them in the conversation history. Feedback highlights a potential issue where reasoning content might be duplicated in assistant messages and suggests expanding the scope of reasoning preservation beyond messages with tool calls. Additionally, it was recommended to refactor helper functions into methods of the ModelGenerator struct for better consistency with the existing codebase.

Comment on lines 83 to 92
if len(toolCalls) > 0 {
am.ToolCalls = (toolCalls)
reasoningKey, ok := firstReasoningMetadataKey(msg.Content)
reasoning := concatenateReasoningContent(msg.Content)
if ok && reasoning != "" {
am.SetExtraFields(map[string]any{
reasoningKey: reasoning,
})
}
}
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.

high

The current implementation has two issues:

  1. Duplication: am.Content is already set to include all parts (including reasoning) via concatenateContent at line 77. By adding the reasoning again via SetExtraFields, it is sent twice to the provider, which can degrade model performance or cause errors in strict APIs like Moonshot.
  2. Limited Scope: Reasoning preservation is currently restricted to messages with tool calls. However, reasoning should be preserved for all assistant messages in the conversation history to maintain context and satisfy provider requirements for consistency.

I suggest moving the reasoning extraction outside the tool call check and overriding am.Content with only text/data parts when reasoning is present.

if len(toolCalls) > 0 {
	am.ToolCalls = (toolCalls)
}
reasoningKey, ok := g.firstReasoningMetadataKey(msg.Content)
reasoning := g.concatenateReasoningContent(msg.Content)
if ok && reasoning != "" {
	am.SetExtraFields(map[string]any{
		reasoningKey: reasoning,
	})
	// Override Content to only include non-reasoning parts to avoid duplication.
	am.Content.OfString = param.NewOpt(g.concatenateTextContent(msg.Content))
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fair

Comment thread go/plugins/compat_oai/generate.go Outdated
Comment on lines +287 to +310
// concatenateReasoningContent works the same as concatenateContent
// but does it only for the reasoning parts
func concatenateReasoningContent(parts []*ai.Part) string {
var sb strings.Builder
for _, part := range parts {
if part.IsReasoning() {
sb.WriteString(part.Text)
}
}
return sb.String()
}

// firstReasoningMetadataKey returns the key which is used for the reasoning values
func firstReasoningMetadataKey(parts []*ai.Part) (string, bool) {
for _, part := range parts {
if !part.IsReasoning() || part.Metadata == nil {
continue
}
if key, ok := part.Metadata["reasoningKey"].(string); ok && key != "" {
return key, true
}
}
return "", false
}
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.

medium

For consistency with the existing concatenateContent method, these helper functions should be defined as methods of ModelGenerator. Additionally, I've added concatenateTextContent to support filtering out reasoning parts when building assistant messages.

// concatenateReasoningContent works the same as concatenateContent
// but does it only for the reasoning parts
func (g *ModelGenerator) concatenateReasoningContent(parts []*ai.Part) string {
	var sb strings.Builder
	for _, part := range parts {
		if part.IsReasoning() {
			sb.WriteString(part.Text)
		}
	}
	return sb.String()
}

// concatenateTextContent works the same as concatenateContent
// but excludes reasoning parts
func (g *ModelGenerator) concatenateTextContent(parts []*ai.Part) string {
	var sb strings.Builder
	for _, part := range parts {
		if part.IsText() || part.IsData() {
			sb.WriteString(part.Text)
		}
	}
	return sb.String()
}

// firstReasoningMetadataKey returns the key which is used for the reasoning values
func (g *ModelGenerator) firstReasoningMetadataKey(parts []*ai.Part) (string, bool) {
	for _, part := range parts {
		if !part.IsReasoning() || part.Metadata == nil {
			continue
		}
		if key, ok := part.Metadata["reasoningKey"].(string); ok && key != "" {
			return key, true
		}
	}
	return "", false
}

@naqerl naqerl force-pushed the naqerl/go-oai-compat-reasoning branch from 1729ab8 to 219c5d1 Compare April 20, 2026 14:32
@apascal07 apascal07 self-requested a review April 21, 2026 17:17
Copy link
Copy Markdown
Collaborator

@apascal07 apascal07 left a comment

Choose a reason for hiding this comment

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

Please fix tests, address Gemini feedback (and resolve comments), then re-request review.

@naqerl
Copy link
Copy Markdown
Author

naqerl commented Apr 21, 2026 via email

@MichaelDoyle
Copy link
Copy Markdown
Contributor

Hey @naqerl - thanks for looking at this. Are you planning to sync back w/ the request changes?

@naqerl
Copy link
Copy Markdown
Author

naqerl commented May 6, 2026 via email

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants