Skip to content

feat: enhance OpenAI responses handling and compatibility - #2817

Closed
mrhuangyong wants to merge 5073 commits into
QuantumNous:mainfrom
mrhuangyong:mrhua
Closed

feat: enhance OpenAI responses handling and compatibility#2817
mrhuangyong wants to merge 5073 commits into
QuantumNous:mainfrom
mrhuangyong:mrhua

Conversation

@mrhuangyong

@mrhuangyong mrhuangyong commented Feb 3, 2026

Copy link
Copy Markdown
  • Updated ResponsesStreamResponse structure to include new fields: ResponseID, Text, Arguments, and ContentIndex.
  • Implemented conversion functions for OpenAI responses in the adaptor, enabling compatibility with chat completions requests.
  • Enhanced stream response handling to support OpenAI responses format, ensuring proper state management and event handling.
  • Added new service functions for converting between OpenAI and Claude response formats.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added OpenAI-compatible streaming support for Claude, enabling proper streaming response handling with structured metadata and function call support.
    • Enhanced streaming responses with improved content indexing and nested output handling.

Calcium-Ion and others added 30 commits December 13, 2025 16:43
…-tts

feat: support gpt tts series model quota calculate
…itelist-cidr

feat(auth): enhance IP restriction handling with CIDR support
… meta when token count is disabled

Clamp request body size (including post-decompression) to avoid memory exhaustion caused by huge payloads/zip bombs, especially with large-context Claude requests. Add a configurable `MAX_REQUEST_BODY_MB` (default `32`) and document it.

- Enforce max request body size after gzip/br decompression via `http.MaxBytesReader`
- Add a secondary size guard in `common.GetRequestBody` and cache-safe handling
- Return **413 Request Entity Too Large** on oversized bodies in relay entry
- Avoid building large `TokenCountMeta.CombineText` when both token counting and sensitive check are disabled (use lightweight meta for pricing)
- Update READMEs (CN/EN/FR/JA) with `MAX_REQUEST_BODY_MB`
- Fix a handful of vet/formatting issues encountered during the change
- `go test ./...` passes
Tighten oversized request handling across relay paths and make error matching reliable.

- Align `MAX_REQUEST_BODY_MB` fallback to `32` in request body reader and decompression middleware
- Stop ignoring `GetRequestBody` errors in relay retry paths; return consistent **413** on oversized bodies (400 for other read errors)
- Add `Unwrap()` to `types.NewAPIError` so `errors.Is/As` can match wrapped underlying errors
- `go test ./...` passes
Updated web/src/i18n/locales/fr.json to improve French translations for the user interface.

Removed verbose prefixes like 'Gestion des...' and 'Paramètres de...' to prevent truncation in sidebars and menus.

Harmonized terms for consistency (e.g., 'Tâches', 'Journaux', 'Dessins').

Renamed 'Place du marché' to 'Marché des modèles'.
  - Add minimal case to clampThinkingBudgetByEffort to avoid defaulting to full thinking budget
Calcium-Ion and others added 26 commits January 29, 2026 23:24
…t-price

fix: /v1/responses/compact default billing
* feat: 引入通用 HTTP BodyStorage/DiskCache 缓存配置与管理

- 新增 common/body_storage.go 提供 HTTP 请求体存储抽象和文件缓存能力
- 增加 common/disk_cache_config.go 支持全局磁盘缓存配置
- main.go 挂载缓存初始化流程
- 新增和补充 controller/performance.go (及 unix/windows) 用于缓存性能监控接口
- middleware/body_cleanup.go 自动清理缓存文件
- router 挂载相关接口
- 前端 settings 页面新增性能监控设置 PerformanceSetting
- 优化缓存开关状态和模块热插拔能力
- 其他相关文件同步适配缓存扩展

* fix: 修复 BodyStorage 并发安全和错误处理问题

- 修复 diskStorage.Close() 竞态条件,先获取锁再执行 CAS
- 为 memoryStorage 添加互斥锁和 closed 状态检查
- 修复 CreateBodyStorageFromReader 在磁盘存储失败时的回退逻辑
- 添加缓存命中统计调用 (IncrementDiskCacheHits/IncrementMemoryCacheHits)
- 修复 gin.go 中 Seek 错误被忽略的问题
- 在 api-router 添加 BodyStorageCleanup 中间件
- 修复前端 formatBytes 对异常值的处理

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
…uantumNous#2793)

Explicitly cast Blocks, Bavail, and Bfree to uint64 for cross-platform compatibility,
as these fields are int64 on FreeBSD but uint64 on Linux.
feat: Support customizing the success and cancel url of Stripe.
…65bf72572ff8684dd7ef068e576

feat: doubao add first and last image to video
…d7331d0987d96dc78bae181e331

feat: task pre consume modelPrice default use setting value
…3b7b97dbdbd98ade9b372bd6f63

feat: CodeViewer click link and auto wrap
…ling

feat(gemini): support cached token billing
fix(ui): use distinct color palette for group tags
* fix: channel affinity log styles

* fix: Issue with incorrect data storage when switching key sources

* feat: support not retrying after a single rule configuration fails

* fix: render channel affinity tooltip as multiline content

* feat: channel affinity cache hit

* fix: prevent ChannelAffinityUsageCacheModal infinite loading and hide data before fetch

* chore: format backend with gofmt and frontend with prettier/eslint autofix
…st-override-take-effect

fix: make channel Host override take effect
feat: /v1/responses qwen3 max && perplexity
…5ce3426cd8ea0d13f38e2f4eb81

feat: auto-adapt video modal
…de-cache-usage

fix: openrouter claude cache usage
- Updated ResponsesStreamResponse structure to include new fields: ResponseID, Text, Arguments, and ContentIndex.
- Implemented conversion functions for OpenAI responses in the adaptor, enabling compatibility with chat completions requests.
- Enhanced stream response handling to support OpenAI responses format, ensuring proper state management and event handling.
- Added new service functions for converting between OpenAI and Claude response formats.
@coderabbitai

coderabbitai Bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Add OpenAI-compatible streaming support for Claude by introducing bidirectional converters between OpenAI Responses and Chat Completions formats, implementing stateful stream processing to emit OpenAI-style events from streaming deltas, and extending Claude's relay pathway to support the Responses API.

Changes

Cohort / File(s) Summary
DTO Enhancements
dto/openai_response.go
Extended ResponsesStreamResponse struct with five new fields: ResponseID, Text, Arguments, ContentIndex, and Part to support richer streaming metadata and nested content handling.
Claude Relay Integration
relay/channel/claude/adaptor.go, relay/channel/claude/relay-claude.go
Implemented ConvertOpenAIResponsesRequest for translating Responses API requests to chat completions; added ResponsesStreamState to ClaudeResponseInfo to manage stateful streaming events and route stream data through OpenAI-compatible response paths.
Service Wrapper Layer
service/openai_chat_responses_compat.go
Added two wrapper functions (ResponsesRequestToChatCompletionsRequest and ChatCompletionsResponseToResponsesResponse) to delegate to openaicompat layer with RelayInfo integration.
OpenAI Compatibility Converters
service/openaicompat/chat_stream_to_responses_stream.go, service/openaicompat/chat_to_responses_response.go, service/openaicompat/responses_to_chat_request.go
Introduced comprehensive bidirectional conversion layer: stateful stream processor for delta→event transformation with tool-call lifecycle handling (~424 lines), response converter for text→responses translation (~161 lines), and request converter for Responses→ChatCompletions format mapping (~375 lines).

Sequence Diagram

sequenceDiagram
    participant Client
    participant Claude Relay
    participant Converter Service
    participant Stream State
    participant OpenAI Compat

    Client->>Claude Relay: Responses API Request
    Claude Relay->>Converter Service: ResponsesRequestToChatCompletionsRequest()
    Converter Service->>OpenAI Compat: ResponsesRequestToChatCompletionsRequest()
    OpenAI Compat-->>Converter Service: GeneralOpenAIRequest
    Converter Service-->>Claude Relay: Chat Completions Request
    
    Claude Relay->>Stream State: NewChatToResponsesStreamState()
    Stream State-->>Claude Relay: StreamState initialized
    
    loop For each streaming chunk
        Claude Relay->>Stream State: HandleChatChunk(delta)
        Stream State->>Stream State: Process delta, track indices
        Stream State-->>Claude Relay: ResponsesStreamResponse events
        Claude Relay-->>Client: Stream chunk (created/in_progress/output_item/etc)
    end
    
    Claude Relay->>Stream State: FinalEvents(usage)
    Stream State-->>Claude Relay: Final response events
    Claude Relay-->>Client: Completed response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • seefs001
  • creamlike1024
  • Calcium-Ion

Poem

🐰 A rabbit hops through streaming dreams,
Converting Claude to OpenAI's schemes,
With states and chunks and responses so neat,
The relay pipeline bounces to beat,
Now Responses bloom where Chat once stood! 🌸

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: enhance OpenAI responses handling and compatibility' accurately summarizes the main changes: adding new fields to ResponsesStreamResponse, implementing conversion functions for OpenAI responses, and enhancing stream response handling with OpenAI responses format support.

✏️ 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

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@service/openaicompat/chat_stream_to_responses_stream.go`:
- Around line 48-117: HandleChatChunk currently assumes incoming
call.Function.Arguments are always cumulative (startsWith previous) and discards
true delta fragments; change the accumulation logic so when args is non-empty
you compute deltaArgs as the new bytes to append: if prev == "" then deltaArgs =
args; else if strings.HasPrefix(args, prev) then deltaArgs = args[len(prev):];
else /* upstream sent a pure delta/fragment */ deltaArgs = args and treat it as
an appended fragment; then update s.ToolCallArgs[callID] = prev + deltaArgs (not
simply args) and emit the same response with Delta = deltaArgs; keep existing
behavior that skips when deltaArgs == "". Apply this change inside
HandleChatChunk where ToolCallArgs is processed (use the existing callID,
s.ToolCallArgs, and outputIndex logic).

In `@service/openaicompat/chat_to_responses_response.go`:
- Around line 13-51: The ChatCompletionsResponseToResponsesResponse function
currently overwrites existing prompt/completion token counts when
usage.TotalTokens == 0; change the logic so you only fill missing values: if
usage.PromptTokens is zero or nil then set it from
info.GetEstimatePromptTokens() (or estimate), and if usage.CompletionTokens is
zero then set it using estimateTokenFallback(extractChatText(chat)); compute
usage.TotalTokens as the sum of the (possibly-original, possibly-filled)
PromptTokens and CompletionTokens and preserve existing PromptTokensDetails and
CompletionTokenDetails when present. Ensure the referenced symbols are adjusted:
ChatCompletionsResponseToResponsesResponse, usage, chat.Usage.*,
info.GetEstimatePromptTokens, estimateTokenFallback, and extractChatText.

In `@service/openaicompat/responses_to_chat_request.go`:
- Around line 252-281: The function responsesInputItemToMessageContent currently
ignores "input_video" items; update it to handle the "input_video" case by
calling responsesContentItemToMediaContent(item), checking for error, and if
media.Type != "" returning []dto.MediaContent{media}, nil (same pattern used for
"input_audio" and "input_file") so top-level video inputs are not dropped.
- Around line 193-303: The code lets empty/absent image URLs slip through
because parseResponsesImageURL returns a MessageImageUrl with an empty Url;
update validation so we fail fast: in responsesContentItemToMediaContent's
"input_image" case call parseResponsesImageURL(part["image_url"],
part["detail"]) and if the returned MessageImageUrl.Url == "" return a
descriptive error (instead of producing a MediaContent with an empty URL), and
also change responsesInputItemToMessageContent's "input_image" branch to use
responsesContentItemToMediaContent (or perform the same Url non-empty check) and
propagate the error so callers receive a failure rather than silent empty data;
reference functions: parseResponsesImageURL, responsesContentItemToMediaContent,
responsesInputItemToMessageContent.

Comment on lines +48 to +117
func (s *ChatToResponsesStreamState) HandleChatChunk(chunk *dto.ChatCompletionsStreamResponse) []dto.ResponsesStreamResponse {
if chunk == nil || len(chunk.Choices) == 0 {
return nil
}

if chunk.Model != "" {
s.Model = chunk.Model
}
if s.CreatedAt == 0 && chunk.Created != 0 {
s.CreatedAt = chunk.Created
}

events := s.baseEvents()

delta := chunk.Choices[0].Delta
if delta.Content != nil {
content := *delta.Content
if content != "" {
events = append(events, s.ensureMessageItemEvents()...)
events = append(events, s.ensureContentPartEvents()...)
s.OutputText.WriteString(content)
events = append(events, s.outputTextDeltaEvent(content))
}
}

if len(delta.ToolCalls) > 0 {
for idx, call := range delta.ToolCalls {
callID := strings.TrimSpace(call.ID)
if callID == "" {
if len(s.ToolCallOrder) > 0 {
callID = s.ToolCallOrder[len(s.ToolCallOrder)-1]
} else {
callID = fmt.Sprintf("call_%d", idx)
}
}
if call.Function.Name != "" {
s.ToolCallName[callID] = call.Function.Name
}
if !s.ToolCallSent[callID] {
s.ToolCallSent[callID] = true
s.ToolCallOrder = append(s.ToolCallOrder, callID)
outIndex := s.allocOutputIndex(callID)
events = append(events, s.toolItemAddedEvent(callID, outIndex))
}

args := call.Function.Arguments
if args == "" {
continue
}
prev := s.ToolCallArgs[callID]
deltaArgs := args
if prev != "" && strings.HasPrefix(args, prev) {
deltaArgs = args[len(prev):]
}
if deltaArgs == "" {
continue
}
s.ToolCallArgs[callID] = args
events = append(events, dto.ResponsesStreamResponse{
Type: "response.function_call_arguments.delta",
ResponseID: s.ResponseID,
ItemID: callID,
OutputIndex: s.outputIndexPtr(callID),
Delta: deltaArgs,
})
}
}

return events
}

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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

OpenAI streaming API tool_calls function arguments cumulative or delta

💡 Result:

They’re deltas (incremental), not cumulative—you should accumulate/concatenate tool/function arguments across stream chunks/events until you receive the “done”/finalization signal.

Responses API (recommended streaming model)

  • During streaming you’ll receive response.function_call_arguments.delta events whose delta is only the next fragment of the JSON-arguments string. (platform.openai.com)
  • When complete, you’ll receive response.function_call_arguments.done, which contains the final full arguments string. (platform.openai.com)
    So: append each delta in order (keyed by the tool-call item), or just wait for the .done event if you don’t need partial args.

Chat Completions streaming

  • Chunks contain a choices[].delta object; tool calls appear under choices[].delta.tool_calls. (platform.openai.com)
  • In practice, the tool call’s function.arguments often arrives fragmented across multiple chunks, so you must concatenate those fragments per tool call (commonly by tool_calls[].index, and/or id when present). (python.langchain.com)

If you tell me which endpoint/SDK you’re using (Responses vs Chat Completions; Python/JS), I can show the exact accumulator pattern for that client.

Citations:


🌐 Web query:

Anthropic streaming API tool calls arguments behavior

💡 Result:

When you use Anthropic’s Messages API with stream: true and the model decides to call a tool, tool arguments (“input”) arrive incrementally:

  • The tool’s arguments are streamed via content_block_delta events where delta.type = "input_json_delta" and partial_json is a fragment of a JSON string, not necessarily valid JSON on its own. You typically concatenate the fragments and only parse once you receive content_block_stop for that tool block; the final tool_use.input is an object. [1]
  • Current model behavior: Claude generally emits one complete key/value at a time for tool input, which can create gaps/delays between streaming events while it “works” on the next parameter. Once a key/value is ready, it may still be split across multiple input_json_delta chunks. [1]

If you enable Fine-grained tool streaming (beta):

  • Add header anthropic-beta: fine-grained-tool-streaming-2025-05-14 (and stream=true). This streams tool parameters without buffering/JSON validation, so you may receive invalid or incomplete JSON, especially if generation stops due to max_tokens. Chunking also tends to be faster and in larger pieces. [2]

Sources: Anthropic Streaming Messages docs [1] and Fine-grained tool streaming docs [2].


🏁 Script executed:

# Let's also check if there are tests or other files in the repository that might clarify how this should work
fd -t f -e go | xargs grep -l "ToolCall\|tool_call" | head -20

Repository: QuantumNous/new-api

Length of output: 682


🏁 Script executed:

# Check the specific file mentioned in the review comment
cat -n service/openaicompat/chat_stream_to_responses_stream.go | sed -n '48,117p'

Repository: QuantumNous/new-api

Length of output: 2408


🏁 Script executed:

# Look for how other parts of the codebase handle tool call accumulation
rg -A 5 -B 5 "ToolCallArgs" service/openaicompat/chat_stream_to_responses_stream.go

Repository: QuantumNous/new-api

Length of output: 1856


🏁 Script executed:

# Check the relay layer to see how they handle tool calls
cat -n relay/channel/openai/relay-openai.go | head -200

Repository: QuantumNous/new-api

Length of output: 8335


Fix tool call arguments accumulation to handle both cumulative and delta-based streaming.

OpenAI and Anthropic streaming APIs send tool call arguments as fragmented chunks that must be accumulated across multiple events. The current code assumes arguments always arrive as cumulative strings and will truncate them if the upstream sends pure deltas (or fragments that don't start with the previous value).

🛠️ Suggested fix
 prev := s.ToolCallArgs[callID]
 deltaArgs := args
 if prev != "" && strings.HasPrefix(args, prev) {
     deltaArgs = args[len(prev):]
-}
-if deltaArgs == "" {
-    continue
-}
-s.ToolCallArgs[callID] = args
+    s.ToolCallArgs[callID] = args
+} else {
+    // Treat as delta and append
+    s.ToolCallArgs[callID] = prev + args
+}
+if deltaArgs == "" {
+    continue
+}
 events = append(events, dto.ResponsesStreamResponse{
🤖 Prompt for AI Agents
In `@service/openaicompat/chat_stream_to_responses_stream.go` around lines 48 -
117, HandleChatChunk currently assumes incoming call.Function.Arguments are
always cumulative (startsWith previous) and discards true delta fragments;
change the accumulation logic so when args is non-empty you compute deltaArgs as
the new bytes to append: if prev == "" then deltaArgs = args; else if
strings.HasPrefix(args, prev) then deltaArgs = args[len(prev):]; else /*
upstream sent a pure delta/fragment */ deltaArgs = args and treat it as an
appended fragment; then update s.ToolCallArgs[callID] = prev + deltaArgs (not
simply args) and emit the same response with Delta = deltaArgs; keep existing
behavior that skips when deltaArgs == "". Apply this change inside
HandleChatChunk where ToolCallArgs is processed (use the existing callID,
s.ToolCallArgs, and outputIndex logic).

Comment on lines +13 to +51
func ChatCompletionsResponseToResponsesResponse(chat *dto.OpenAITextResponse, info *relaycommon.RelayInfo, responseID string) (*dto.OpenAIResponsesResponse, *dto.Usage, error) {
if chat == nil {
return nil, nil, errors.New("response is nil")
}

responseID = normalizeResponsesID(responseID)
createdAt := coerceCreatedAt(chat.Created)

output := chatMessageToResponsesOutput(chat)

usage := &dto.Usage{
PromptTokens: chat.Usage.PromptTokens,
CompletionTokens: chat.Usage.CompletionTokens,
TotalTokens: chat.Usage.TotalTokens,
}
usage.PromptTokensDetails = chat.Usage.PromptTokensDetails
usage.CompletionTokenDetails = chat.Usage.CompletionTokenDetails

if usage.TotalTokens == 0 {
text := extractChatText(chat)
if info != nil {
usage.PromptTokens = info.GetEstimatePromptTokens()
}
usage.CompletionTokens = estimateTokenFallback(text)
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
}

resp := &dto.OpenAIResponsesResponse{
ID: responseID,
Object: "response",
CreatedAt: createdAt,
Status: "completed",
Model: chat.Model,
Output: output,
Usage: usage,
}

return resp, usage, 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

Don’t overwrite real usage values when only total_tokens is missing.

If prompt/completion tokens are already present, the current fallback replaces them with estimates. That can skew billing and analytics.

🛠️ Suggested fix
 if usage.TotalTokens == 0 {
-    text := extractChatText(chat)
-    if info != nil {
-        usage.PromptTokens = info.GetEstimatePromptTokens()
-    }
-    usage.CompletionTokens = estimateTokenFallback(text)
-    usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
+    if usage.PromptTokens == 0 && info != nil {
+        usage.PromptTokens = info.GetEstimatePromptTokens()
+    }
+    if usage.CompletionTokens == 0 {
+        text := extractChatText(chat)
+        usage.CompletionTokens = estimateTokenFallback(text)
+    }
+    if usage.PromptTokens > 0 || usage.CompletionTokens > 0 {
+        usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
+    }
 }
🤖 Prompt for AI Agents
In `@service/openaicompat/chat_to_responses_response.go` around lines 13 - 51, The
ChatCompletionsResponseToResponsesResponse function currently overwrites
existing prompt/completion token counts when usage.TotalTokens == 0; change the
logic so you only fill missing values: if usage.PromptTokens is zero or nil then
set it from info.GetEstimatePromptTokens() (or estimate), and if
usage.CompletionTokens is zero then set it using
estimateTokenFallback(extractChatText(chat)); compute usage.TotalTokens as the
sum of the (possibly-original, possibly-filled) PromptTokens and
CompletionTokens and preserve existing PromptTokensDetails and
CompletionTokenDetails when present. Ensure the referenced symbols are adjusted:
ChatCompletionsResponseToResponsesResponse, usage, chat.Usage.*,
info.GetEstimatePromptTokens, estimateTokenFallback, and extractChatText.

Comment on lines +193 to +303
func responsesContentItemToMediaContent(part map[string]any) (dto.MediaContent, error) {
t, _ := part["type"].(string)
switch t {
case "input_text":
text, _ := part["text"].(string)
return dto.MediaContent{
Type: dto.ContentTypeText,
Text: text,
}, nil
case "input_image":
return dto.MediaContent{
Type: dto.ContentTypeImageURL,
ImageUrl: parseResponsesImageURL(part["image_url"], part["detail"]),
}, nil
case "input_audio":
if audio, ok := part["input_audio"].(map[string]any); ok {
data, _ := audio["data"].(string)
format, _ := audio["format"].(string)
return dto.MediaContent{
Type: dto.ContentTypeInputAudio,
InputAudio: &dto.MessageInputAudio{
Data: data,
Format: format,
},
}, nil
}
case "input_file":
if file, ok := part["file"].(map[string]any); ok {
msgFile := &dto.MessageFile{}
if fileID, ok := file["file_id"].(string); ok {
msgFile.FileId = fileID
}
if fileName, ok := file["filename"].(string); ok {
msgFile.FileName = fileName
}
if fileData, ok := file["file_data"].(string); ok {
msgFile.FileData = fileData
}
if fileURL, ok := file["file_url"].(string); ok && msgFile.FileData == "" {
msgFile.FileData = fileURL
}
return dto.MediaContent{
Type: dto.ContentTypeFile,
File: msgFile,
}, nil
}
case "input_video":
if url, ok := part["video_url"].(string); ok {
return dto.MediaContent{
Type: dto.ContentTypeVideoUrl,
VideoUrl: &dto.MessageVideoUrl{
Url: url,
},
}, nil
}
}
return dto.MediaContent{}, nil
}

func responsesInputItemToMessageContent(itemType string, item map[string]any) (any, error) {
switch itemType {
case "input_text":
if text, ok := item["text"].(string); ok {
return text, nil
}
case "input_image":
media := dto.MediaContent{
Type: dto.ContentTypeImageURL,
ImageUrl: parseResponsesImageURL(item["image_url"], item["detail"]),
}
return []dto.MediaContent{media}, nil
case "input_audio":
media, err := responsesContentItemToMediaContent(item)
if err != nil {
return nil, err
}
if media.Type != "" {
return []dto.MediaContent{media}, nil
}
case "input_file":
media, err := responsesContentItemToMediaContent(item)
if err != nil {
return nil, err
}
if media.Type != "" {
return []dto.MediaContent{media}, nil
}
}
return nil, nil
}

func parseResponsesImageURL(image any, detail any) *dto.MessageImageUrl {
msg := &dto.MessageImageUrl{}
switch v := image.(type) {
case string:
msg.Url = v
case map[string]any:
if url, ok := v["url"].(string); ok {
msg.Url = url
}
if det, ok := v["detail"].(string); ok {
msg.Detail = det
}
}
if msg.Detail == "" {
if det, ok := detail.(string); ok {
msg.Detail = det
}
}
return msg
}

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the file referenced in the review
fd -t f responses_to_chat_request.go | head -5

Repository: QuantumNous/new-api

Length of output: 112


🏁 Script executed:

# Check the learning's reference file
fd -t f task_video.go | head -5

Repository: QuantumNous/new-api

Length of output: 87


🏁 Script executed:

# Let's search for patterns related to data: URL validation
rg "data:" --type go -B 2 -A 2 | head -50

Repository: QuantumNous/new-api

Length of output: 2846


🏁 Script executed:

# Search for image_url handling and validation patterns
rg "image_url" --type go -B 1 -A 1 | head -50

Repository: QuantumNous/new-api

Length of output: 2518


🏁 Script executed:

# Let's examine controller/task_video.go to understand the data: URL validation pattern
cat -n controller/task_video.go | head -100

Repository: QuantumNous/new-api

Length of output: 4108


🏁 Script executed:

# Search for FailReason assignment to see the pattern
rg "FailReason" --type go -B 3 -A 3 | head -80

Repository: QuantumNous/new-api

Length of output: 4214


🏁 Script executed:

# Check what happens downstream with the returned MediaContent
rg "MediaContent" --type go -B 2 -A 2 | grep -A 5 -B 5 "ImageUrl\|input_image" | head -60

Repository: QuantumNous/new-api

Length of output: 3517


🏁 Script executed:

# Look at the actual function context and imports to understand the data flow
sed -n '1,50p' service/openaicompat/responses_to_chat_request.go

Repository: QuantumNous/new-api

Length of output: 1353


🏁 Script executed:

# Check if there are any validations on image URLs elsewhere in the codebase
rg "TrimSpace\|Url.*==" --type go | grep -i image | head -20

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Verify if parseResponsesImageURL is called with null/nil and what it returns
sed -n '280,320p' service/openaicompat/responses_to_chat_request.go

Repository: QuantumNous/new-api

Length of output: 952


🏁 Script executed:

# Check the MessageImageUrl struct to understand what fields are required
rg "MessageImageUrl" --type go -A 5 | head -30

Repository: QuantumNous/new-api

Length of output: 1523


Validate input_image URLs before emitting media content.

When image_url is missing or empty, parseResponsesImageURL returns a MessageImageUrl with an empty Url field. Downstream code checks if vv.Url != "" and silently skips processing, allowing invalid data to propagate silently. Failing fast here makes input validation explicit and prevents silent failures.

🛠️ Suggested fix
 case "input_image":
-    return dto.MediaContent{
-        Type:     dto.ContentTypeImageURL,
-        ImageUrl: parseResponsesImageURL(part["image_url"], part["detail"]),
-    }, nil
+    img := parseResponsesImageURL(part["image_url"], part["detail"])
+    if img == nil || strings.TrimSpace(img.Url) == "" {
+        return dto.MediaContent{}, fmt.Errorf("input_image requires image_url")
+    }
+    return dto.MediaContent{
+        Type:     dto.ContentTypeImageURL,
+        ImageUrl: img,
+    }, nil
🤖 Prompt for AI Agents
In `@service/openaicompat/responses_to_chat_request.go` around lines 193 - 303,
The code lets empty/absent image URLs slip through because
parseResponsesImageURL returns a MessageImageUrl with an empty Url; update
validation so we fail fast: in responsesContentItemToMediaContent's
"input_image" case call parseResponsesImageURL(part["image_url"],
part["detail"]) and if the returned MessageImageUrl.Url == "" return a
descriptive error (instead of producing a MediaContent with an empty URL), and
also change responsesInputItemToMessageContent's "input_image" branch to use
responsesContentItemToMediaContent (or perform the same Url non-empty check) and
propagate the error so callers receive a failure rather than silent empty data;
reference functions: parseResponsesImageURL, responsesContentItemToMediaContent,
responsesInputItemToMessageContent.

Comment on lines +252 to +281
func responsesInputItemToMessageContent(itemType string, item map[string]any) (any, error) {
switch itemType {
case "input_text":
if text, ok := item["text"].(string); ok {
return text, nil
}
case "input_image":
media := dto.MediaContent{
Type: dto.ContentTypeImageURL,
ImageUrl: parseResponsesImageURL(item["image_url"], item["detail"]),
}
return []dto.MediaContent{media}, nil
case "input_audio":
media, err := responsesContentItemToMediaContent(item)
if err != nil {
return nil, err
}
if media.Type != "" {
return []dto.MediaContent{media}, nil
}
case "input_file":
media, err := responsesContentItemToMediaContent(item)
if err != nil {
return nil, err
}
if media.Type != "" {
return []dto.MediaContent{media}, nil
}
}
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the file and examine the switch statement
fd "responses_to_chat_request.go" --type f

Repository: QuantumNous/new-api

Length of output: 112


🏁 Script executed:

# Search for responsesContentItemToMediaContent definition
rg "func responsesContentItemToMediaContent" -A 20

Repository: QuantumNous/new-api

Length of output: 1812


🏁 Script executed:

# Search for input_video references in the codebase
rg "input_video" -C 3

Repository: QuantumNous/new-api

Length of output: 1056


🏁 Script executed:

# Check the responsesInputItemToMessageContent function more completely
rg "func responsesInputItemToMessageContent" -A 50

Repository: QuantumNous/new-api

Length of output: 3829


🏁 Script executed:

# Get the complete responsesContentItemToMediaContent function
rg "func responsesContentItemToMediaContent" -A 100 | head -120

Repository: QuantumNous/new-api

Length of output: 7786


Add input_video handling in item-to-content conversion.

responsesContentItemToMediaContent supports input_video, but top‑level input_video items are silently ignored here, causing video inputs to be dropped.

🛠️ Suggested fix
 case "input_file":
     media, err := responsesContentItemToMediaContent(item)
     if err != nil {
         return nil, err
     }
     if media.Type != "" {
         return []dto.MediaContent{media}, nil
     }
+case "input_video":
+    media, err := responsesContentItemToMediaContent(item)
+    if err != nil {
+        return nil, err
+    }
+    if media.Type != "" {
+        return []dto.MediaContent{media}, nil
+    }
 }
🤖 Prompt for AI Agents
In `@service/openaicompat/responses_to_chat_request.go` around lines 252 - 281,
The function responsesInputItemToMessageContent currently ignores "input_video"
items; update it to handle the "input_video" case by calling
responsesContentItemToMediaContent(item), checking for error, and if media.Type
!= "" returning []dto.MediaContent{media}, nil (same pattern used for
"input_audio" and "input_file") so top-level video inputs are not dropped.

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.