Skip to content

fix: add basic inline file support for Claude relay#3505

Merged
Calcium-Ion merged 2 commits intoQuantumNous:mainfrom
seefs001:fix/claude-media-support
Apr 2, 2026
Merged

fix: add basic inline file support for Claude relay#3505
Calcium-Ion merged 2 commits intoQuantumNous:mainfrom
seefs001:fix/claude-media-support

Conversation

@seefs001
Copy link
Copy Markdown
Collaborator

@seefs001 seefs001 commented Mar 30, 2026

fix #3481

Summary by CodeRabbit

  • New Features

    • Improved parsing and media handling for PDF and text file inputs (file relay path currently disabled).
    • Safer handling of image and other media types to avoid nil/unsupported items.
  • Tests

    • Added unit tests validating file content transformation and preservation of surrounding text.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

Walkthrough

Added helpers to parse and convert file attachments into Claude-compatible media (PDF → document, text files → text), refactored message parsing to a switch over content types, and added nil guards to avoid panics when non-image media lack image metadata.

Changes

Cohort / File(s) Summary
File Media Support
relay/channel/claude/relay-claude.go
Added createClaudeFileSource and buildClaudeFileMessage to transform dto.MessageFile into Claude media (PDF → document, text → text); refactored message parsing to a switch on media type and added nil guard for image media.
Test Coverage
relay/channel/claude/relay_claude_test.go
Added unit tests covering omission of unsupported files, PDF → Claude document conversion with base64 source, and text-file decoding into Claude text media. Updated imports for base64 handling and assertions.

Sequence Diagram

sequenceDiagram
    participant Client
    participant MessageParser as Message Parser
    participant ContentSwitch as Content Type Handler
    participant FileService as File Service
    participant ClaudeFormatter as Claude Formatter
    participant Claude

    Client->>MessageParser: Send message with content[]
    MessageParser->>ContentSwitch: Iterate content items

    alt Text item
        ContentSwitch->>ClaudeFormatter: Pass text
        ClaudeFormatter->>Claude: Add text content
    else Image URL item
        ContentSwitch->>FileService: GetImageMedia()
        FileService-->>ContentSwitch: imageUrl or nil
        alt imageUrl present
            ContentSwitch->>FileService: service.GetBase64Data(url)
            FileService-->>ContentSwitch: base64 data
            ContentSwitch->>ClaudeFormatter: Create image source
            ClaudeFormatter->>Claude: Add image content
        else nil
            ContentSwitch->>ContentSwitch: continue (skip)
        end
    else File item
        ContentSwitch->>FileService: service.GetBase64Data(fileRef)
        FileService-->>ContentSwitch: base64 data
        ContentSwitch->>ContentSwitch: detect MIME from name or data
        alt application/pdf
            ContentSwitch->>ClaudeFormatter: Build document media (base64)
            ClaudeFormatter->>Claude: Add document media
        else text/plain
            ContentSwitch->>ContentSwitch: decode base64 to text
            ContentSwitch->>ClaudeFormatter: Build text media
            ClaudeFormatter->>Claude: Add text media
        else Unsupported MIME
            ContentSwitch->>ContentSwitch: log & skip
        end
    end

    Claude-->>Client: Return response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • Calcium-Ion

Poem

🐰 I found a file and gave it a whirl,

PDFs now dance, and notes unfurl,
Nil guards stitched so nothing will trip,
Claude reads papers on my little trip,
Hooray — no panics on this rabbit's ship! 📄✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: add basic inline file support for Claude relay' directly relates to the main changeset, which adds file handling support (PDF and text files) to the Claude relay while fixing the nil pointer dereference panic reported in issue #3481.
Linked Issues check ✅ Passed The PR addresses issue #3481's primary requirement: adding a nil guard after GetImageMedia() to prevent panic when handling non-image media. Additional file support (PDF documents and text files) is implemented via new helper functions. Both the panic fix and file handling enhancements are covered by test cases.
Out of Scope Changes check ✅ Passed All changes are scoped to the Claude relay file handling: helper functions for file conversion and media building, refactored media parsing with nil guards, and comprehensive test coverage. No unrelated refactoring or out-of-scope modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@seefs001 seefs001 marked this pull request as draft March 30, 2026 11:42
Copy link
Copy Markdown
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
relay/channel/claude/relay-claude.go (1)

401-461: ⚠️ Potential issue | 🟠 Major

Reject messages that become empty after media filtering.

If every part is skipped (unsupported file, nil imageUrl, etc.), claudeMediaMessages stays empty and still gets assigned to claudeMessage.Content. That bypasses the fallback at Lines 318-320, so a request with only an unsupported attachment no longer panics but still leaves this relay with an empty Claude message instead of a clear local error.

🛠️ Suggested patch
 				if message.ToolCalls != nil {
 					for _, toolCall := range message.ParseToolCalls() {
 						inputObj := make(map[string]any)
 						if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &inputObj); err != nil {
 							common.SysLog("tool call function arguments is not a map[string]any: " + fmt.Sprintf("%v", toolCall.Function.Arguments))
 							continue
 						}
 						claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{
 							Type:  "tool_use",
 							Id:    toolCall.ID,
 							Name:  toolCall.Function.Name,
 							Input: inputObj,
 						})
 					}
 				}
+				if len(claudeMediaMessages) == 0 {
+					return nil, fmt.Errorf("message contains no Claude-compatible content")
+				}
 				claudeMessage.Content = claudeMediaMessages
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/claude/relay-claude.go` around lines 401 - 461, After filtering
media into claudeMediaMessages, ensure you reject/return an error when all parts
were skipped so we don't assign an empty slice to claudeMessage.Content;
specifically, after the loop that builds claudeMediaMessages (which uses
message.ParseContent(), message.ParseToolCalls(), buildClaudeFileMessage, and
service.GetBase64Data), check if len(claudeMediaMessages) == 0 and return a
clear local error (or trigger the existing fallback) instead of assigning the
empty slice to claudeMessage.Content; this prevents creating an empty Claude
payload when the original message contained only unsupported or nil media.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@relay/channel/claude/relay-claude.go`:
- Around line 49-102: createClaudeFileSource currently returns
types.NewURLFileSource(file.FileData) without preserving a filename-derived MIME
and buildClaudeFileMessage matches mime strings strictly (so values like
"text/plain; charset=utf-8" are skipped); fix by populating the
FileSource.MimeType for URL-backed files in createClaudeFileSource (create the
URL source into a variable, detect extension via service.GetMimeTypeByExtension
using filepath.Ext(file.FileName) and set source.MimeType when not
"application/octet-stream", then return that source) and update
buildClaudeFileMessage to normalize the mimeType from service.GetBase64Data by
trimming and lowercasing and stripping any parameters after ';' before the
switch so "text/plain; charset=utf-8" and similar values match the supported
branches.
- Around line 3-10: Replace the direct uses of encoding/json's json.Marshal and
json.Unmarshal in relay-claude.go with the codebase wrapper functions
common.Marshal and common.Unmarshal: find the four call sites that currently
call json.Unmarshal(...) and json.Marshal(...) and swap them to
common.Unmarshal(...) and common.Marshal(...), keep the same error handling and
argument shapes (pass the byte slice and target pointer to common.Unmarshal and
use the returned []byte from common.Marshal), add an import for the common
package and remove encoding/json if it becomes unused so the file compiles.

---

Outside diff comments:
In `@relay/channel/claude/relay-claude.go`:
- Around line 401-461: After filtering media into claudeMediaMessages, ensure
you reject/return an error when all parts were skipped so we don't assign an
empty slice to claudeMessage.Content; specifically, after the loop that builds
claudeMediaMessages (which uses message.ParseContent(),
message.ParseToolCalls(), buildClaudeFileMessage, and service.GetBase64Data),
check if len(claudeMediaMessages) == 0 and return a clear local error (or
trigger the existing fallback) instead of assigning the empty slice to
claudeMessage.Content; this prevents creating an empty Claude payload when the
original message contained only unsupported or nil media.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 498da071-2726-4be8-8dae-47b3b59e8485

📥 Commits

Reviewing files that changed from the base of the PR and between fbf235d and 263b9bc.

📒 Files selected for processing (2)
  • relay/channel/claude/relay-claude.go
  • relay/channel/claude/relay_claude_test.go

Comment on lines 3 to 10
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"
"strings"
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.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Direct encoding/json usage in relay/channel/claude/relay-claude.go:"
rg -n -C1 'encoding/json|json\.(Marshal|Unmarshal)\b' relay/channel/claude/relay-claude.go

echo
echo "Approved wrappers in common/json.go:"
rg -n -C1 'func (Marshal|Unmarshal|UnmarshalJsonStr|DecodeJson|GetJsonType)\b' common/json.go

Repository: QuantumNous/new-api

Length of output: 1442


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== All direct encoding/json calls in relay/channel/claude/ ==="
rg 'json\.(Marshal|Unmarshal|MarshalIndent|UnmarshalString)\b' relay/channel/claude/ -n || true

echo
echo "=== Checking specific lines in relay-claude.go ==="
# Show context around each flagged line
for line_num in 145 449 583 932; do
  echo "--- Line $line_num ---"
  sed -n "$((line_num-2)),$((line_num+2))p" relay/channel/claude/relay-claude.go
done

echo
echo "=== Count of direct json calls ==="
rg 'json\.(Marshal|Unmarshal)\b' relay/channel/claude/relay-claude.go | wc -l

Repository: QuantumNous/new-api

Length of output: 1790


Replace direct json.Marshal and json.Unmarshal calls with wrapper functions from common/json.go.

Lines 145, 449, 583, and 932 use direct json.Unmarshal and json.Marshal calls. Replace with common.Unmarshal() and common.Marshal() respectively to align with codebase standards.

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

In `@relay/channel/claude/relay-claude.go` around lines 3 - 10, Replace the direct
uses of encoding/json's json.Marshal and json.Unmarshal in relay-claude.go with
the codebase wrapper functions common.Marshal and common.Unmarshal: find the
four call sites that currently call json.Unmarshal(...) and json.Marshal(...)
and swap them to common.Unmarshal(...) and common.Marshal(...), keep the same
error handling and argument shapes (pass the byte slice and target pointer to
common.Unmarshal and use the returned []byte from common.Marshal), add an import
for the common package and remove encoding/json if it becomes unused so the file
compiles.

Comment on lines +49 to +102
func createClaudeFileSource(file *dto.MessageFile) *types.FileSource {
if file == nil || file.FileData == "" {
return nil
}
if strings.HasPrefix(file.FileData, "http://") || strings.HasPrefix(file.FileData, "https://") {
return types.NewURLFileSource(file.FileData)
}
mimeType := ""
if ext := strings.TrimPrefix(strings.ToLower(filepath.Ext(file.FileName)), "."); ext != "" {
if detected := service.GetMimeTypeByExtension(ext); detected != "application/octet-stream" {
mimeType = detected
}
}
return types.NewBase64FileSource(file.FileData, mimeType)
}

func buildClaudeFileMessage(c *gin.Context, file *dto.MessageFile) (*dto.ClaudeMediaMessage, error) {
source := createClaudeFileSource(file)
if source == nil {
return nil, nil
}
base64Data, mimeType, err := service.GetBase64Data(c, source, "formatting document for Claude")
if err != nil {
return nil, fmt.Errorf("get file data failed: %w", err)
}
switch strings.ToLower(mimeType) {
case "application/pdf":
return &dto.ClaudeMediaMessage{
Type: "document",
Source: &dto.ClaudeMessageSource{
Type: "base64",
MediaType: mimeType,
Data: base64Data,
},
}, nil
case "text/plain":
decodedData, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return nil, fmt.Errorf("decode text file data failed: %w", err)
}
return &dto.ClaudeMediaMessage{
Type: "text",
Text: common.GetPointer(string(decodedData)),
}, nil
default:
msg := fmt.Sprintf("claude: skip unsupported file content, filename=%q, mime=%q", file.FileName, mimeType)
if c != nil {
logger.LogInfo(c, msg)
} else {
common.SysLog(msg)
}
return nil, nil
}
}
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.

⚠️ Potential issue | 🟠 Major

Harden MIME detection for URL-backed file content.

Valid URL-backed PDFs/text files can still get dropped here. createClaudeFileSource returns types.NewURLFileSource before the filename-derived MIME is attached, even though types.FileSource already has MimeType in types/file_source.go, Lines 20-31, and types.NewURLFileSource in types/file_source.go, Lines 140-154 only preserves the URL. On top of that, buildClaudeFileMessage compares the raw content type string, so common values like text/plain; charset=utf-8 miss the supported branch and get skipped.

🛠️ Suggested patch
func createClaudeFileSource(file *dto.MessageFile) *types.FileSource {
	if file == nil || file.FileData == "" {
		return nil
	}
-	if strings.HasPrefix(file.FileData, "http://") || strings.HasPrefix(file.FileData, "https://") {
-		return types.NewURLFileSource(file.FileData)
-	}
 	mimeType := ""
 	if ext := strings.TrimPrefix(strings.ToLower(filepath.Ext(file.FileName)), "."); ext != "" {
 		if detected := service.GetMimeTypeByExtension(ext); detected != "application/octet-stream" {
 			mimeType = detected
 		}
 	}
+	if strings.HasPrefix(file.FileData, "http://") || strings.HasPrefix(file.FileData, "https://") {
+		return &types.FileSource{
+			Type:     types.FileSourceTypeURL,
+			URL:      file.FileData,
+			MimeType: mimeType,
+		}
+	}
 	return types.NewBase64FileSource(file.FileData, mimeType)
}
-	switch strings.ToLower(mimeType) {
+	normalizedMimeType := strings.ToLower(strings.TrimSpace(strings.SplitN(mimeType, ";", 2)[0]))
+	switch normalizedMimeType {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/claude/relay-claude.go` around lines 49 - 102,
createClaudeFileSource currently returns types.NewURLFileSource(file.FileData)
without preserving a filename-derived MIME and buildClaudeFileMessage matches
mime strings strictly (so values like "text/plain; charset=utf-8" are skipped);
fix by populating the FileSource.MimeType for URL-backed files in
createClaudeFileSource (create the URL source into a variable, detect extension
via service.GetMimeTypeByExtension using filepath.Ext(file.FileName) and set
source.MimeType when not "application/octet-stream", then return that source)
and update buildClaudeFileMessage to normalize the mimeType from
service.GetBase64Data by trimming and lowercasing and stripping any parameters
after ';' before the switch so "text/plain; charset=utf-8" and similar values
match the supported branches.

Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@relay/channel/claude/relay-claude.go`:
- Around line 434-442: The switch branch for dto.ContentTypeFile is commented
out causing file attachments to be skipped; restore the case for
dto.ContentTypeFile in the same switch where mediaMessage is handled, call
buildClaudeFileMessage(c, mediaMessage.GetFile()), check and return any error,
and if the returned claudeFileMessage is non-nil append it to
claudeMediaMessages (the same slice used for other media). Ensure you use the
existing identifiers dto.ContentTypeFile, buildClaudeFileMessage,
mediaMessage.GetFile(), and claudeMediaMessages so the file handling integrates
with the surrounding conversion flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de480dec-4b0e-428c-b4f9-0e3de98fb31a

📥 Commits

Reviewing files that changed from the base of the PR and between 263b9bc and c04f82b.

📒 Files selected for processing (1)
  • relay/channel/claude/relay-claude.go

Comment on lines +434 to +442
// FIXME
//case dto.ContentTypeFile:
// claudeFileMessage, err := buildClaudeFileMessage(c, mediaMessage.GetFile())
// if err != nil {
// return nil, err
// }
// if claudeFileMessage != nil {
// claudeMediaMessages = append(claudeMediaMessages, *claudeFileMessage)
// }
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.

⚠️ Potential issue | 🟠 Major

dto.ContentTypeFile handling is still disabled.

Line 435-442 is commented out, so file attachments fall through to default (Line 443-444) and are skipped. That leaves inline file support inactive in the main conversion path.

Suggested patch
-					// FIXME
-					//case dto.ContentTypeFile:
-					//	claudeFileMessage, err := buildClaudeFileMessage(c, mediaMessage.GetFile())
-					//	if err != nil {
-					//		return nil, err
-					//	}
-					//	if claudeFileMessage != nil {
-					//		claudeMediaMessages = append(claudeMediaMessages, *claudeFileMessage)
-					//	}
+					case dto.ContentTypeFile:
+						claudeFileMessage, err := buildClaudeFileMessage(c, mediaMessage.GetFile())
+						if err != nil {
+							return nil, err
+						}
+						if claudeFileMessage != nil {
+							claudeMediaMessages = append(claudeMediaMessages, *claudeFileMessage)
+						}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// FIXME
//case dto.ContentTypeFile:
// claudeFileMessage, err := buildClaudeFileMessage(c, mediaMessage.GetFile())
// if err != nil {
// return nil, err
// }
// if claudeFileMessage != nil {
// claudeMediaMessages = append(claudeMediaMessages, *claudeFileMessage)
// }
case dto.ContentTypeFile:
claudeFileMessage, err := buildClaudeFileMessage(c, mediaMessage.GetFile())
if err != nil {
return nil, err
}
if claudeFileMessage != nil {
claudeMediaMessages = append(claudeMediaMessages, *claudeFileMessage)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/claude/relay-claude.go` around lines 434 - 442, The switch
branch for dto.ContentTypeFile is commented out causing file attachments to be
skipped; restore the case for dto.ContentTypeFile in the same switch where
mediaMessage is handled, call buildClaudeFileMessage(c, mediaMessage.GetFile()),
check and return any error, and if the returned claudeFileMessage is non-nil
append it to claudeMediaMessages (the same slice used for other media). Ensure
you use the existing identifiers dto.ContentTypeFile, buildClaudeFileMessage,
mediaMessage.GetFile(), and claudeMediaMessages so the file handling integrates
with the surrounding conversion flow.

@seefs001 seefs001 marked this pull request as ready for review April 2, 2026 05:28
@Calcium-Ion Calcium-Ion merged commit 41cd051 into QuantumNous:main Apr 2, 2026
1 check passed
VeryGoodUser1 pushed a commit to xmz-ai/new-api that referenced this pull request Apr 3, 2026
…pport

fix: add basic inline file support for Claude relay
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.

Claude 转发处理非图片媒体内容时 panic: nil pointer dereference (relay-claude.go:359)

2 participants