Skip to content

Conversation

@sawka
Copy link
Member

@sawka sawka commented Feb 10, 2026

No description provided.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

Walkthrough

Removed automatic injection of a system prompt into initial messages. Introduced multimodal support: new ChatContentPart and ChatImageUrl types, added ContentParts to ChatRequestMessage, and implemented MarshalJSON/UnmarshalJSON to allow either a text Content or a ContentParts array. Split AI message conversion into text-only and multimodal flows (convertAIMessageTextOnly, convertAIMessageMultimodal), updated UI conversion to render ContentParts when present, and deep-copied/cleaned ContentParts in StoredChatMessage/ChatRequestMessage. Added image-part tracking in AI-to-stored conversion and an explicit error when all image conversions fail.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No description was provided by the author, making it impossible to assess whether it relates to the changeset. Please provide a pull request description explaining the motivation, implementation approach, and any breaking changes or migration notes.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: implementing image support for OpenAI chat using content parts, which is the primary focus across multiple files.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sawka/chat-images

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
pkg/aiusechat/openaichat/openaichat-types.go (1)

26-45: Internal metadata fields are serialized in the wire format when ContentParts are marshaled.

FileName, PreviewUrl, and MimeType carry json:"..." tags, so they will appear in the JSON payload when ContentParts is marshaled at line 78. The clean() method strips them, but only if the caller remembers to call clean() before marshaling. Consider using json:"-" on these internal fields and handling them separately (e.g., in StoredChatMessage serialization) to make it impossible for them to leak to the API regardless of call-site discipline.

Proposed fix: tag internal fields with `json:"-"` and use a separate internal struct for storage

One approach: use json:"-" on internal fields and introduce an internal-only wrapper for persistence:

 type ChatContentPart struct {
 	Type     string        `json:"type"`                // "text" or "image_url"
 	Text     string        `json:"text,omitempty"`      // for type "text"
 	ImageUrl *ChatImageUrl `json:"image_url,omitempty"` // for type "image_url"
 
-	FileName   string `json:"filename,omitempty"`   // internal: original filename
-	PreviewUrl string `json:"previewurl,omitempty"` // internal: 128x128 webp preview
-	MimeType   string `json:"mimetype,omitempty"`   // internal: original mimetype
+	FileName   string `json:"-"` // internal: original filename
+	PreviewUrl string `json:"-"` // internal: 128x128 webp preview
+	MimeType   string `json:"-"` // internal: original mimetype
 }

This would require custom marshal/unmarshal on StoredChatMessage (or ChatContentPart) for persistence, but eliminates the risk of internal metadata leaking to any upstream API.


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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pkg/aiusechat/openaichat/openaichat-convertmessage.go`:
- Around line 238-307: convertAIMessageMultimodal can return a StoredChatMessage
with empty ContentParts when all parts are skipped; update
convertAIMessageMultimodal to detect when contentParts is empty after processing
(variable contentParts in function convertAIMessageMultimodal) and return a
non-nil error (or a clear sentinel) instead of returning a StoredChatMessage
with empty Message.ContentParts, so callers can avoid persisting/using empty
messages; ensure the error text clearly mentions "no content parts" and
references aiMsg.MessageId for debugging.
🧹 Nitpick comments (2)
pkg/aiusechat/openaichat/openaichat-types.go (2)

92-119: UnmarshalJSON: consider handling JSON null content explicitly.

When content is explicitly null in JSON (not absent), len(raw.Content) is 4 (the bytes n,u,l,l), so the early return on line 103 won't fire. The code then falls into the array unmarshal path, which succeeds silently (setting parts to nil). While the end result is correct, the intent isn't obvious. A brief comment or an explicit null check (e.g., bytes.Equal(raw.Content, []byte("null"))) would make the behavior clearer.

Suggested clarification
 	if len(raw.Content) == 0 {
 		return nil
 	}
+	// explicit null → no content
+	if string(raw.Content) == "null" {
+		return nil
+	}
 	// try array first

261-264: Shallow copy of ContentParts doesn't deep-copy the *ChatImageUrl pointer field.

copy() on line 263 performs a shallow element copy. Each ChatContentPart contains a *ChatImageUrl pointer, so the original and the copied message will share the same ChatImageUrl instances. This is inconsistent with the ToolCalls deep-copy pattern (lines 251-259) where the ToolUseData pointer is explicitly cloned. If ChatImageUrl is ever mutated after Copy(), both the original and copy would be affected.

Deep-copy ChatImageUrl to match ToolCalls pattern
 	if len(m.Message.ContentParts) > 0 {
 		copied.Message.ContentParts = make([]ChatContentPart, len(m.Message.ContentParts))
-		copy(copied.Message.ContentParts, m.Message.ContentParts)
+		for i, cp := range m.Message.ContentParts {
+			copied.Message.ContentParts[i] = cp
+			if cp.ImageUrl != nil {
+				imgCopy := *cp.ImageUrl
+				copied.Message.ContentParts[i].ImageUrl = &imgCopy
+			}
+		}
 	}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pkg/aiusechat/openaichat/openaichat-types.go`:
- Around line 262-265: The current shallow copy of m.Message.ContentParts
(copying into copied.Message.ContentParts) leaves ChatContentPart.ImageUrl
pointers shared; update the clone so each ChatContentPart is deep-copied: after
allocating copied.Message.ContentParts and copying the slice, loop over each
index and if m.Message.ContentParts[i].ImageUrl != nil, allocate a new
ChatImageUrl, copy its fields (e.g., Detail, any other fields) into it and
assign to copied.Message.ContentParts[i].ImageUrl; mirror the pattern used in
the ToolCalls/ToolUseData deep-copy to ensure no shared pointers remain.
🧹 Nitpick comments (1)
pkg/aiusechat/openaichat/openaichat-types.go (1)

70-91: MarshalJSON: empty content omits the "content" key entirely.

When both Content and ContentParts are empty/nil, raw.Content remains a nil json.RawMessage, and the omitempty tag on the wire struct causes the "content" key to be dropped from the JSON output. Some OpenAI-compatible APIs expect "content": null to be present (e.g., for assistant messages with tool calls). If this is intentional, a brief comment would help; otherwise, consider emitting null explicitly.

Comment on lines +262 to +265
if len(m.Message.ContentParts) > 0 {
copied.Message.ContentParts = make([]ChatContentPart, len(m.Message.ContentParts))
copy(copied.Message.ContentParts, m.Message.ContentParts)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Shallow copy of ContentParts doesn't deep-copy the ImageUrl pointer.

copy() copies struct values, but ChatContentPart.ImageUrl is a *ChatImageUrl. After copy(...), the original and the clone share the same ChatImageUrl pointer. If the copy is later mutated (e.g., ImageUrl.Detail changed), it will corrupt the original.

Compare with the ToolCalls deep-copy on lines 252–260 which explicitly clones ToolUseData.

Proposed fix
 	if len(m.Message.ContentParts) > 0 {
 		copied.Message.ContentParts = make([]ChatContentPart, len(m.Message.ContentParts))
-		copy(copied.Message.ContentParts, m.Message.ContentParts)
+		for i, cp := range m.Message.ContentParts {
+			copied.Message.ContentParts[i] = cp
+			if cp.ImageUrl != nil {
+				imgCopy := *cp.ImageUrl
+				copied.Message.ContentParts[i].ImageUrl = &imgCopy
+			}
+		}
 	}
🤖 Prompt for AI Agents
In `@pkg/aiusechat/openaichat/openaichat-types.go` around lines 262 - 265, The
current shallow copy of m.Message.ContentParts (copying into
copied.Message.ContentParts) leaves ChatContentPart.ImageUrl pointers shared;
update the clone so each ChatContentPart is deep-copied: after allocating
copied.Message.ContentParts and copying the slice, loop over each index and if
m.Message.ContentParts[i].ImageUrl != nil, allocate a new ChatImageUrl, copy its
fields (e.g., Detail, any other fields) into it and assign to
copied.Message.ContentParts[i].ImageUrl; mirror the pattern used in the
ToolCalls/ToolUseData deep-copy to ensure no shared pointers remain.

@sawka sawka merged commit f95d781 into main Feb 10, 2026
7 checks passed
@sawka sawka deleted the sawka/chat-images branch February 10, 2026 05:48
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