Skip to content

feat: 新增 Responses → ChatCompletions 协议降级兼容配置 - #5209

Open
Rosons wants to merge 19 commits into
QuantumNous:mainfrom
Rosons:main
Open

feat: 新增 Responses → ChatCompletions 协议降级兼容配置#5209
Rosons wants to merge 19 commits into
QuantumNous:mainfrom
Rosons:main

Conversation

@Rosons

@Rosons Rosons commented May 31, 2026

Copy link
Copy Markdown

📝 变更描述

背景

当前项目已支持 ChatCompletions → Responses 的协议兼容配置(将 /v1/chat/completions 转为 Responses 协议发给支持 Responses API 的上游),但缺少反向场景:当 /v1/responses
请求命中了不支持 Responses 协议的渠道(如 DeepSeek、智谱 GLM 等)时,会直接返回错误。

实现方案

在系统设置中新增 Responses → ChatCompletions 兼容配置,与现有的 ChatCompletions → Responses 配置完全对称。用户可通过 JSON 策略配置哪些渠道/模型启用协议降级。

当启用的请求到达时,在 ResponsesHelper 中拦截,执行以下转换流程:

  1. 请求转换:将 Responses 请求体(含 inputinstructionstools 等字段)转为 Chat Completions 格式(messages 数组)
  2. 协议切换:临时将中继模式切换为 ChatCompletions,将请求发送到上游的 /v1/chat/completions
  3. 响应转换:将上游的 ChatCompletions 响应(流式或非流式)实时转为 Responses 格式返回给客户端

关键设计要点

  • tool_call 配对:参考 codex-proxy 的 pendingCalls + respondedIDs 机制,通过 call_id 匹配 function_call 和 function_call_output,确保复杂的多轮工具调用对话正确转换
  • 流式事件完整性:按 Responses SSE 协议标准发送完整的事件序列(含 reasoning_summary_part.added/donecontent_part.added/donefunction_call_arguments.done
    等),避免客户端等待
  • 参数兼容developer 角色自动映射为 system;非 function 类型工具自动过滤;工具参数 schema 自动修补
  • DeepSeek 思考模式适配:带 tool_calls 的 assistant 消息自动填充 reasoning_content 兜底值,满足 DeepSeek 思考模式的校验要求
  • 策略三元组:支持 channel_ids(渠道 ID)、channel_types(渠道类型)、model_patterns(模型正则)三种过滤条件

影响范围

  • 后端:新增 4 个文件,修改 5 个文件
  • 前端:default 和 classic 两种主题的设置页面均已添加配置界面,支持 6 种语言

变更类型选 ✨ 新功能。

Summary by CodeRabbit

  • New Features

    • Responses→ChatCompletions compatibility mode (routes Responses via Chat Completions with streaming/non-streaming conversion)
    • Global settings UI to manage compatibility policies with regex model-patterns and example templates
    • Optional strict parameter for function/tool calls
    • Multilingual UI strings for the new settings
  • Chores

    • Default theme changed from "classic" to "default"

Rosons and others added 8 commits May 31, 2026 11:59
新增协议转换引擎,将 /v1/responses 请求转为 ChatCompletions 协议:
- ResponsesRequestToChatCompletionsRequest 请求体转换
- ChatCompletionsResponseToResponsesResponse 非流式响应转换
- OaiChatToResponsesStreamHandler 流式 SSE 转换
- responsesViaChatCompletions 编排函数

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 新增 ResponsesToChatCompletionsPolicy 策略结构体
- 新增 ShouldResponsesUseChatCompletionsGlobal 策略评估
- 在 ResponsesHelper 中插入策略检查和降级分发

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- FunctionRequest 新增 Strict 字段用于工具参数透传
- 前端默认主题从 classic 改为 default

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
在系统设置的 Global Model Configuration 中添加协议降级策略配置区块,
支持通过 JSON 配置 enabled、all_channels、channel_ids、model_patterns 等参数

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
更新所有 6 个语言文件,添加 Responses→ChatCompletions 兼容配置
的描述文字和 model_patterns 说明的翻译

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
在 classic 主题的模型设置中添加协议降级策略配置区块,
更新所有 8 个语言文件的翻译

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
为协议转换的核心函数添加了详细的中文注释,包括:
- ResponsesRequestToChatCompletionsRequest 请求转换
- ChatCompletionsResponseToResponsesResponse 非流式响应转换
- OaiChatToResponsesStreamHandler 流式 SSE 转换
- responsesViaChatCompletions 编排函数
- toolCallState 流式工具调用追踪
- convertResponsesInputToMessages input→messages 转换
- convertResponsesToolsToChatTools 工具格式转换

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds routing that can downgrade OpenAI Responses requests to chat completions and convert requests/responses bidirectionally, with streaming translation, policy gating, relay orchestration, UI settings, and localization.

Changes

Responses→ChatCompletions Downgrade

Layer / File(s) Summary
Policy configuration and decision logic
setting/model_setting/global.go, service/openaicompat/policy.go, service/openai_chat_responses_mode.go
New ResponsesToChatCompletionsPolicy type with channel enablement and model-pattern regex matching; added to GlobalSettings and exposed policy/global evaluation functions.
Request/Response format converters
service/openaicompat/responses_to_chat_request.go, service/openaicompat/chat_to_responses_response.go, service/openai_chat_responses_compat.go
Converts Responses requests to Chat Completions requests (messages, tools, media, formats) and converts Chat Completions responses back to Responses format (output items, reasoning, function calls, usage).
Streaming response handlers
relay/channel/openai/chat_to_responses_stream.go
Non-streaming handler for full responses and streaming SSE handler that emits Responses API SSE events incrementally, tracking reasoning/text/tool-call state and finalizing with usage.
Relay orchestration and routing
relay/responses_via_chat_completions.go, relay/responses_handler.go
ResponsesHelper optionally routes to responsesViaChatCompletions, which converts the request, temporarily reroutes upstream path/mode, sends the Chat Completions request, handles errors/status mapping, detects streaming, and converts responses back.
Classic UI settings and localization
web/classic/src/pages/Setting/Model/SettingGlobalModel.jsx, web/classic/src/i18n/locales/*
Adds a JSON policy editor for Responses→ChatCompletions with templates, regex guidance, format helpers, and i18n entries in classic UI locales.
Default web UI settings and localization
web/default/src/features/system-settings/models/..., web/default/src/features/models/..., web/default/src/i18n/locales/*
Adds default model-setting key, Zod validation, examples, formatting helpers, drawer defaults, and translations for the new policy.
Supporting changes
common/constants.go, dto/openai_request.go, Dockerfile, docker-compose.yml
Theme default initialization changed to "default", FunctionRequest gains optional Strict field, Dockerfile build stage adds GOPROXY ARG/env and BuildKit cache mounts, and docker-compose new-api switched to local build: ..

Sequence Diagram

sequenceDiagram
  participant Client as Responses API Client
  participant Relay as Relay (ResponsesHelper)
  participant Converter as ResponsesToChatCompletionsRequest
  participant Upstream as OpenAI Chat Completions
  participant StreamHandler as OaiChatToResponsesStreamHandler / Converter
  Client->>Relay: /v1/responses request
  Relay->>Converter: convert to Chat Completions request
  Converter->>Upstream: send chat/completions (stream or non-stream)
  Upstream->>StreamHandler: stream chunks or final response
  StreamHandler->>Client: emit Responses-format events / final response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels

ready to merge

Suggested reviewers

  • seefs001
  • Calcium-Ion

🐰 I hopped through code with nimble paws,
Downgraded Responses with thoughtful laws,
Streams and converters stitched in a row,
Settings and locales now nicely in tow,
The rabbit cheers — deployment carrots aglow!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.93% 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 PR title accurately describes the main feature: adding a new Responses→ChatCompletions protocol downgrade compatibility configuration. It is specific, concise, and reflects the primary change across backend and frontend.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@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: 9

Caution

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

⚠️ Outside diff range comments (1)
web/default/src/features/system-settings/models/global-settings-card.tsx (1)

98-115: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restrict policy fields to JSON objects.

jsonString only checks that the input parses, so the new policy field will accept values like [], "foo", or 1 even though the form treats policies as object configs ({} fallback in Line 147 and object examples in the new section). That makes it easy to save an invalid policy and push the failure downstream instead of catching it in the form.

Suggested fix
+import { validateJsonString } from './utils'
+
-const jsonString = z.string().refine((value) => {
-  const trimmed = value.trim()
-  if (!trimmed) return true
-  try {
-    JSON.parse(trimmed)
-    return true
-  } catch {
-    return false
-  }
-}, 'Invalid JSON format')
+const jsonString = z.string().superRefine((value, ctx) => {
+  const result = validateJsonString(value, {
+    predicate: (parsed) =>
+      parsed !== null &&
+      typeof parsed === 'object' &&
+      !Array.isArray(parsed),
+    predicateMessage: 'Policy JSON must be a JSON object',
+  })
+
+  if (!result.valid) {
+    ctx.addIssue({
+      code: z.ZodIssueCode.custom,
+      message: result.message ?? 'Policy JSON must be a JSON object',
+    })
+  }
+})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/system-settings/models/global-settings-card.tsx`
around lines 98 - 115, The current jsonString refine only validates that the
value parses as JSON, allowing arrays, strings, numbers, etc., so fields like
thinking_model_blacklist, chat_completions_to_responses_policy, and
responses_to_chat_completions_policy can be non-object values; update jsonString
(or create a new jsonObjectString) used in the z.object schema to parse the
trimmed value and then ensure the parsed result is a plain object (typeof ===
'object', not null, and not Array.isArray(parsed)), returning false otherwise so
the schema only accepts JSON objects consistent with the form's expected {}
defaults and examples.
🧹 Nitpick comments (1)
web/default/src/i18n/locales/fr.json (1)

679-679: ⚡ Quick win

Use hierarchical i18n keys for these new entries.

Line 679 and Lines 1363-1364 introduce sentence-style keys. Please switch these newly added keys to namespaced hierarchical keys (for example, settings.responsesToChatCompletions.title and settings.responsesToChatCompletions.modelPatternsHelp.*) to keep key naming consistent and maintainable.

As per coding guidelines: “Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency”.

Also applies to: 1363-1364

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/fr.json` at line 679, Replace the sentence-style
translation keys with hierarchical, namespaced keys: change "Responses ->
ChatCompletions Compatibility" to a namespaced key such as
settings.responsesToChatCompletions.title and convert the related keys at lines
1363-1364 (e.g., model patterns help entries) to something like
settings.responsesToChatCompletions.modelPatternsHelp.*; update the JSON object
keys accordingly and ensure any code that references the old strings (key
lookups) is updated to use the new keys (search for the exact string "Responses
-> ChatCompletions Compatibility" and the two sentence-style keys at 1363-1364
to locate usages).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/channel/openai/chat_to_responses_stream.go`:
- Line 4: Remove the direct encoding/json usage and make tool-call finalization
deterministic: replace the import of "encoding/json" and the call argsJSON, _ :=
json.Marshal(buf.args) with common.Marshal(buf.args) (referencing argsJSON and
buf.args), and in finalizeAllToolCalls ensure you sort the tcBuf slice by each
buf.itemIdx before iterating so emission of
response.function_call_arguments.done and response.output_item.done occurs in
deterministic order consistent with output_index; update the
finalizeAllToolCalls loop to iterate the sorted list instead of ranging the
unsorted map iteration.
- Around line 424-439: finalizeAllToolCalls currently iterates the map tcBuf
which yields nondeterministic order; change finalizeAllToolCalls to collect the
keys (buf.itemIdx or map keys), sort them (ascending by itemIdx/output_index),
then iterate the sorted list and for each index retrieve tcBuf[index], set
outputItems[index].Status = "completed" and call sendResponsesEvent for
"response.function_call_arguments.done" and "response.output_item.done" in that
deterministic order, and finally reset tcBuf = make(map[int]*toolCallState).

In `@relay/responses_handler.go`:
- Around line 81-85: The branch in responses_handler.go that decides between
PostAudioConsumeQuota and PostTextConsumeQuota should stop relying solely on
info.OriginModelName and instead mirror the usage-based audio detection used in
the compatibility handler: check the request/response audio token details (e.g.,
info.AudioTokens > 0) and the audio/text ratio availability (e.g.,
info.AudioTextRatio != nil or >0) before calling
service.PostAudioConsumeQuota(c, info, usage, ""); otherwise call
service.PostTextConsumeQuota(c, info, usage, nil). Replace the current
strings.HasPrefix(info.OriginModelName, "gpt-4o-audio") conditional with that
combined audio-token + ratio check so aliased or future audio-capable models are
correctly billed.
- Around line 74-87: The downgrade branch wrongly applies to compact responses;
update the conditional that routes to responsesViaChatCompletions (the block
calling service.ShouldResponsesUseChatCompletionsGlobal and
responsesViaChatCompletions) to skip when the request is compact by adding an
explicit check excluding RelayModeResponsesCompact (e.g. ensure info.RelayMode
!= RelayModeResponsesCompact or equivalent) so compact `/v1/responses/compact`
callers are not downgraded and still follow the compact payload/billing path
handled later.

In `@relay/responses_via_chat_completions.go`:
- Around line 99-100: The nil-response branch currently calls
types.NewOpenAIError(nil, ...) which causes a panic because NewOpenAIError
dereferences the error; update the resp == nil branch to create and pass a
concrete error (e.g. errors.New("empty response from DoRequest" or similar)
instead of nil when calling types.NewOpenAIError, ensuring the function (where
the check lives) returns types.NewOpenAIError(err, types.ErrorCodeBadResponse,
http.StatusInternalServerError) with that concrete err; reference the resp ==
nil check and the types.NewOpenAIError call to locate the change.

In `@service/openaicompat/chat_to_responses_response.go`:
- Around line 4-10: The converter in chat_to_responses_response.go is using
encoding/json directly and ignores json.Unmarshal errors for msg.ToolCalls;
replace direct json usage with common.Unmarshal/common.Marshal and ensure any
unmarshal/marshal error (especially when decoding msg.ToolCalls) is returned
from the converter so OaiChatToResponsesHandler can surface a bad-response
error; specifically, swap json.Unmarshal/json.Marshal calls to
common.Unmarshal/common.Marshal for msg.ToolCalls and any payload serialization,
check their returned error values, and propagate those errors from the converter
instead of discarding them.

In `@service/openaicompat/responses_to_chat_request.go`:
- Around line 150-153: The code currently drops non-array "input" values by
returning nil,nil when jsonType != "array"; instead, modify the handling in the
function that converts Responses to a Chat request (the block checking jsonType
and variable jsonType) to return a clear error for unsupported input shapes
(HTTP 400 / bad request) rather than silently returning nil. Change the branch
that now does "if jsonType != \"array\" { return nil, nil }" to construct and
return an appropriate error describing the unsupported input type so callers
fail fast with a 400-like validation error.

In `@web/classic/src/pages/Setting/Model/SettingGlobalModel.jsx`:
- Around line 328-330: The helper text passed to extraText for the model pattern
field (the t('model_patterns ...') string in SettingGlobalModel.jsx) uses mixed
examples (deepseek/glm) that conflict with the placeholders elsewhere in the
ChatCompletions→Responses section (which use gpt-*); update the example regexes
in that helper string to be direction-consistent with the placeholders (e.g.,
change ["^deepseek-.*$", "^glm-.*$"] to match the gpt-* style like ["^gpt-.*$",
"^gpt-4.*$"] or vice versa) so admins see consistent examples; edit the t
key/string accordingly in SettingGlobalModel.jsx where extraText is set.
- Around line 411-413: The Tag component in SettingGlobalModel.jsx currently
contains hardcoded Chinese text "测试版"; replace this with a translatable string
by calling the app's localization helper (e.g., use intl.formatMessage({ id:
'setting.beta', defaultMessage: 'Beta' }) or the project's t('setting.beta')
helper) instead of the literal characters, update/create the corresponding
locale key ("setting.beta") in the locale files, and ensure the Tag markup (the
<Tag ...> ... </Tag> instance) uses the translated value so the label renders
correctly for all locales.

---

Outside diff comments:
In `@web/default/src/features/system-settings/models/global-settings-card.tsx`:
- Around line 98-115: The current jsonString refine only validates that the
value parses as JSON, allowing arrays, strings, numbers, etc., so fields like
thinking_model_blacklist, chat_completions_to_responses_policy, and
responses_to_chat_completions_policy can be non-object values; update jsonString
(or create a new jsonObjectString) used in the z.object schema to parse the
trimmed value and then ensure the parsed result is a plain object (typeof ===
'object', not null, and not Array.isArray(parsed)), returning false otherwise so
the schema only accepts JSON objects consistent with the form's expected {}
defaults and examples.

---

Nitpick comments:
In `@web/default/src/i18n/locales/fr.json`:
- Line 679: Replace the sentence-style translation keys with hierarchical,
namespaced keys: change "Responses -> ChatCompletions Compatibility" to a
namespaced key such as settings.responsesToChatCompletions.title and convert the
related keys at lines 1363-1364 (e.g., model patterns help entries) to something
like settings.responsesToChatCompletions.modelPatternsHelp.*; update the JSON
object keys accordingly and ensure any code that references the old strings (key
lookups) is updated to use the new keys (search for the exact string "Responses
-> ChatCompletions Compatibility" and the two sentence-style keys at 1363-1364
to locate usages).
🪄 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: 6a161b35-37b1-436d-a667-d44b49490efa

📥 Commits

Reviewing files that changed from the base of the PR and between 230a359 and 0f9ead6.

📒 Files selected for processing (31)
  • common/constants.go
  • dto/openai_request.go
  • relay/channel/openai/chat_to_responses_stream.go
  • relay/responses_handler.go
  • relay/responses_via_chat_completions.go
  • service/openai_chat_responses_compat.go
  • service/openai_chat_responses_mode.go
  • service/openaicompat/chat_to_responses_response.go
  • service/openaicompat/policy.go
  • service/openaicompat/responses_to_chat_request.go
  • setting/model_setting/global.go
  • web/classic/src/i18n/locales/en.json
  • web/classic/src/i18n/locales/fr.json
  • web/classic/src/i18n/locales/ja.json
  • web/classic/src/i18n/locales/ru.json
  • web/classic/src/i18n/locales/vi.json
  • web/classic/src/i18n/locales/zh-CN.json
  • web/classic/src/i18n/locales/zh-TW.json
  • web/classic/src/i18n/locales/zh.json
  • web/classic/src/pages/Setting/Model/SettingGlobalModel.jsx
  • web/default/src/features/models/components/drawers/model-mutate-drawer.tsx
  • web/default/src/features/system-settings/models/global-settings-card.tsx
  • web/default/src/features/system-settings/models/index.tsx
  • web/default/src/features/system-settings/models/section-registry.tsx
  • web/default/src/features/system-settings/types.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json

package openai

import (
"encoding/json"

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the target file
FILE="relay/channel/openai/chat_to_responses_stream.go"
test -f "$FILE" && echo "FOUND: $FILE" || { echo "MISSING: $FILE"; exit 1; }

echo "---- imports (first ~80 lines) ----"
sed -n '1,120p' "$FILE" | nl -ba

echo "---- encoding/json usage ----"
rg -n --no-heading 'encoding/json|json\.(Marshal|Unmarshal|MarshalIndent|NewEncoder|NewDecoder|Valid)' "$FILE" || true

echo "---- toolCallState definition and iteration block ----"
rg -n --no-heading 'type toolCallState|toolCallState|tcBuf' "$FILE"

# Print the suspected finalization loop area (around reported lines)
# Use a window around 380-480 to cover both Marshal and iteration.
echo "---- window 380-480 ----"
nl -ba "$FILE" | sed -n '380,480p'

Repository: QuantumNous/new-api

Length of output: 216


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="relay/channel/openai/chat_to_responses_stream.go"
test -f "$FILE" && echo "FOUND: $FILE" || { echo "MISSING: $FILE"; exit 1; }

echo "---- imports (first ~120 lines) ----"
sed -n '1,120p' "$FILE" | cat -n

echo "---- encoding/json usage ----"
rg -n --no-heading 'encoding/json|json\.(Marshal|Unmarshal|MarshalIndent|NewEncoder|NewDecoder|Valid)' "$FILE" || true

echo "---- toolCallState definition and iteration markers ----"
rg -n --no-heading 'type toolCallState|toolCallState|tcBuf' "$FILE" || true

echo "---- window 380-480 ----"
sed -n '380,480p' "$FILE" | cat -n

Repository: QuantumNous/new-api

Length of output: 9275


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="relay/channel/openai/chat_to_responses_stream.go"

echo "---- other encoding/json usage ----"
rg -n --no-heading 'encoding/json|json\.(Marshal|Unmarshal|MarshalIndent|NewEncoder|NewDecoder|Valid)' "$FILE" || true

echo "---- range loops over tcBuf (map iteration) ----"
rg -n --no-heading 'for\s+_,\s+\w+\s*:=\s*range\s+tcBuf\s*' "$FILE" || true

echo "---- any range loops over maps other than slices ----"
rg -n --no-heading 'for\s+.*:=\s*range\s+\w+' "$FILE" | head -n 80

Repository: QuantumNous/new-api

Length of output: 409


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="relay/channel/openai/chat_to_responses_stream.go"
echo "---- tcBuf creation & tcIndex/itemIdx assignment (120-390) ----"
sed -n '120,390p' "$FILE" | cat -n

echo "---- delta.ToolCalls loop (520-590) ----"
sed -n '500,590p' "$FILE" | cat -n

Repository: QuantumNous/new-api

Length of output: 10808


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="common/json.go"
test -f "$FILE" && echo "FOUND: $FILE" || { echo "MISSING: $FILE"; exit 1; }

echo "---- common/json.go (first 220 lines) ----"
sed -n '1,220p' "$FILE" | cat -n

echo "---- find Marshal implementation ----"
rg -n --no-heading 'func\s+Marshal\s*\(|func\s+Unmarshal\s*\(|func\s+MarshalJsonStr\s*\(|func\s+DecodeJson\s*\(|func\s+GetJsonType\s*\(' "$FILE" || true

Repository: QuantumNous/new-api

Length of output: 1981


Fix JSON wrapper rule and make tool-call finalization event order deterministic

  • Remove the direct encoding/json import and replace argsJSON, _ := json.Marshal(buf.args) with common.Marshal(buf.args) (lines 4, 412-413).
  • In finalizeAllToolCalls, for _, buf := range tcBuf iterates a map, so response.function_call_arguments.done / response.output_item.done can be emitted in nondeterministic order vs output_index (around line 425); sort tool calls by buf.itemIdx before emitting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/openai/chat_to_responses_stream.go` at line 4, Remove the
direct encoding/json usage and make tool-call finalization deterministic:
replace the import of "encoding/json" and the call argsJSON, _ :=
json.Marshal(buf.args) with common.Marshal(buf.args) (referencing argsJSON and
buf.args), and in finalizeAllToolCalls ensure you sort the tcBuf slice by each
buf.itemIdx before iterating so emission of
response.function_call_arguments.done and response.output_item.done occurs in
deterministic order consistent with output_index; update the
finalizeAllToolCalls loop to iterate the sorted list instead of ranging the
unsorted map iteration.

Comment on lines +424 to +439
finalizeAllToolCalls := func() {
for _, buf := range tcBuf {
item := &outputItems[buf.itemIdx]
item.Status = "completed"

sendResponsesEvent("response.function_call_arguments.done", map[string]any{
"output_index": buf.itemIdx,
"call_id": buf.callID,
"arguments": buf.args,
})
sendResponsesEvent("response.output_item.done", map[string]any{
"output_index": buf.itemIdx,
"item": *item,
})
}
tcBuf = make(map[int]*toolCallState)

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file
FILE="relay/channel/openai/chat_to_responses_stream.go"
test -f "$FILE" || { echo "Missing $FILE"; exit 1; }

# Show the relevant section with line numbers (424-460 approximate)
nl -ba "$FILE" | sed -n '380,470p'

# Extract tcBuf declaration and nearby context
rg -n "tcBuf" "$FILE"
echo "---- tcBuf definition sites ----"
# Try to show the lines around the first definition
first_def=$(rg -n "tcBuf\\s*[:=]" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${first_def:-}" ]; then
  start=$((first_def-30)); end=$((first_def+30));
  nl -ba "$FILE" | sed -n "${start},${end}p"
fi

# Determine whether finalizeAllToolCalls is called and from where
rg -n "finalizeAllToolCalls" "$FILE"
rg -n "response\\.function_call_arguments\\.done|response\\.output_item\\.done" "$FILE"

# Search repository for ordering expectations (tests/log assertions) around these event names
rg -n "response\\.function_call_arguments\\.done|response\\.output_item\\.done" --glob='**/*.{go,ts,js,py,md}' . | head -n 50

Repository: QuantumNous/new-api

Length of output: 106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="relay/channel/openai/chat_to_responses_stream.go"
test -f "$FILE" || { echo "Missing $FILE"; exit 1; }

# Relevant code window (approx)
cat -n "$FILE" | sed -n '380,470p'

# Find tcBuf definition/usage in the file
echo "---- tcBuf related lines ----"
rg -n "tcBuf" "$FILE"

echo "---- finalizeAllToolCalls related lines ----"
rg -n "finalizeAllToolCalls" "$FILE"

echo "---- emitted event names in the file ----"
rg -n "response\\.function_call_arguments\\.done|response\\.output_item\\.done" "$FILE"

echo "---- repo-wide ordering expectations around emitted events ----"
rg -n "response\\.function_call_arguments\\.done|response\\.output_item\\.done" --glob='**/*.{go,ts,js,py,md}' . | head -n 80

Repository: QuantumNous/new-api

Length of output: 4880


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect downstream handling for the done events
FILE="relay/channel/openai/chat_via_responses.go"
test -f "$FILE" || { echo "Missing $FILE"; exit 1; }

echo "---- Inspect chat_via_responses.go around event handling ----"
rg -n "response\\.output_item\\.added|response\\.output_item\\.done|response\\.function_call_arguments\\.done" "$FILE"
echo "---- Context around those cases ----"
# Show a broader window around the switch/case
line=$(rg -n "case \"response\\.output_item\\.added\"|case \"response\\.function_call_arguments\\.done\"" "$FILE" | head -n 1 | cut -d: -f1)
start=$((line-40)); end=$((line+80))
cat -n "$FILE" | sed -n "${start},${end}p"

# Also inspect dto/openai_response.go notes about ordering (if any)
FILE2="dto/openai_response.go"
test -f "$FILE2" && {
  echo "---- Inspect dto/openai_response.go around ResponsesOutputTypeItemDone / comments ----"
  rg -n "ResponsesOutputTypeItemDone|function_call_arguments.done" "$FILE2"
  sed -n '360,430p' "$FILE2" | cat -n
}

# Inspect sendResponsesEvent implementation to see whether it buffers/serializes ordering
FILE3="relay/channel/openai/chat_to_responses_stream.go"
echo "---- Inspect sendResponsesEvent definition/behavior in same file ----"
rg -n "func sendResponsesEvent|sendResponsesEvent\\(" "$FILE3" | head -n 20
defline=$(rg -n "func sendResponsesEvent" "$FILE3" | head -n 1 | cut -d: -f1 || true)
if [ -n "${defline:-}" ]; then
  start=$((defline-30)); end=$((defline+80))
  cat -n "$FILE3" | sed -n "${start},${end}p"
fi

Repository: QuantumNous/new-api

Length of output: 8951


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Repo-wide search for consumers of these event names
rg -n "response\\.function_call_arguments\\.done" --glob='**/*.go' .
rg -n "response\\.output_item\\.done" --glob='**/*.go' .

# Look for any ordering/sorting logic around output_index or done events
rg -n "output_index" --glob='**/*.go' relay dto . | head -n 200
rg -n "Sort\\(|sort\\.Ints|order|ordering|determin" --glob='**/*.go' relay dto . | head -n 200

# Specifically inspect sendResponsesEvent implementation and whether it serializes writes in-order
FILE="relay/channel/openai/chat_to_responses_stream.go"
rg -n "func sendResponsesEvent" "$FILE"
line=$(rg -n "func sendResponsesEvent" "$FILE" | head -n1 | cut -d: -f1)
start=$((line-30)); end=$((line+120))
cat -n "$FILE" | sed -n "${start},${end}p"

Repository: QuantumNous/new-api

Length of output: 21994


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="relay/channel/openai/chat_to_responses_stream.go"
test -f "$FILE" || { echo "Missing $FILE"; exit 1; }

echo "---- Imports in chat_to_responses_stream.go ----"
sed -n '1,120p' "$FILE" | cat -n

echo "---- sendResponsesEvent implementation (if in this file) ----"
rg -n "func sendResponsesEvent" "$FILE"
line=$(rg -n "func sendResponsesEvent" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-20)); end=$((line+120))
  sed -n "${start},${end}p" "$FILE" | cat -n
fi

echo "---- check whether tcBuf is keyed by output index or something deterministic ----"
sed -n '100,180p' "$FILE" | cat -n

Repository: QuantumNous/new-api

Length of output: 5316


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find where sendResponsesEvent is defined
rg -n "func sendResponsesEvent\\b" --glob='**/*.go' . 

# 2) Inspect ordering behavior around output_item.done / function_call_arguments.done
FILE="relay/channel/openai/chat_via_responses.go"
test -f "$FILE" || { echo "Missing $FILE"; exit 1; }

# Show the whole switch cases area
rg -n "case \"response\\.output_item\\.added\"|case \"response\\.output_item\\.done\"|case \"response\\.function_call_arguments\\.done\"" "$FILE"
# show around those lines
line1=$(rg -n "case \"response\\.output_item\\.added\", \"response\\.output_item\\.done\"" "$FILE" | head -n1 | cut -d: -f1)
line2=$(rg -n "case \"response\\.function_call_arguments\\.done\"" "$FILE" | head -n1 | cut -d: -f1)
start1=$((line1-40)); end1=$((line1+120))
start2=$((line2-30)); end2=$((line2+80))
cat -n "$FILE" | sed -n "${start1},${end1}p"
echo "----"
cat -n "$FILE" | sed -n "${start2},${end2}p"

# 3) Look for any consumer that depends on event ordering rather than output_index
rg -n "output_item\\.done\"|function_call_arguments\\.done\"" --glob='**/*.go' relay dto .

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- sendResponsesEvent definition(s) ----"
rg -n "sendResponsesEvent\\b" --glob='**/*.go' relay/channel/openai/chat_to_responses_stream.go
rg -n "func sendResponsesEvent\\b" --glob='**/*.go' . || true

echo "---- Inspect chat_via_responses.go around cases ----"
FILE="relay/channel/openai/chat_via_responses.go"
test -f "$FILE" || { echo "Missing $FILE"; exit 1; }

# Print a focused window around the case labels
cat -n "$FILE" | sed -n '360,520p'

echo "---- Repo-wide any ordering/buffering logic based on these event names ----"
rg -n "response\\.output_item\\.done|response\\.function_call_arguments\\.done" --glob='**/*.go' . || true

Repository: QuantumNous/new-api

Length of output: 8479


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect sendResponsesEvent closure body
FILE="relay/channel/openai/chat_to_responses_stream.go"
echo "---- sendResponsesEvent closure (lines ~120-210) ----"
sed -n '120,210p' "$FILE" | cat -n

# Repo-wide search for any consumers/tests that may assert ordering
echo "---- occurrences of event names outside the translator (all files) ----"
rg -n "response\\.output_item\\.done|response\\.function_call_arguments\\.done" . | head -n 200

echo "---- check for ordering/sorting expectations near these event names (go tests/other) ----"
rg -n "output_item\\.done|function_call_arguments\\.done" --glob='**/*.{go,ts,js,py,md,json,yml,yaml}' . | head -n 200

echo "---- check for test assertions involving event sequences (look for arrays/snapshots/order keywords near event names) ----"
rg -n "response\\.output_item\\.done.*(require|assert|Equal|diff|snapshot|order|sequence)" --glob='**/*.{go,ts,js,py}' . || true

Repository: QuantumNous/new-api

Length of output: 5739


Finalize tool call events deterministically in finalizeAllToolCalls
finalizeAllToolCalls iterates tcBuf (map[int]*toolCallState), so emission order for response.function_call_arguments.done and response.output_item.done is nondeterministic when multiple tool calls are active; sort the tool calls before sending (e.g., by itemIdx/output_index) to keep a stable event sequence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/openai/chat_to_responses_stream.go` around lines 424 - 439,
finalizeAllToolCalls currently iterates the map tcBuf which yields
nondeterministic order; change finalizeAllToolCalls to collect the keys
(buf.itemIdx or map keys), sort them (ascending by itemIdx/output_index), then
iterate the sorted list and for each index retrieve tcBuf[index], set
outputItems[index].Status = "completed" and call sendResponsesEvent for
"response.function_call_arguments.done" and "response.output_item.done" in that
deterministic order, and finally reset tcBuf = make(map[int]*toolCallState).

Comment on lines +74 to +87
passThroughGlobal := model_setting.GetGlobalSettings().PassThroughRequestEnabled
if !passThroughGlobal && !info.ChannelSetting.PassThroughBodyEnabled &&
service.ShouldResponsesUseChatCompletionsGlobal(info.ChannelId, info.ChannelType, info.OriginModelName) {
usage, newApiErr := responsesViaChatCompletions(c, info, request)
if newApiErr != nil {
return newApiErr
}
if strings.HasPrefix(info.OriginModelName, "gpt-4o-audio") {
service.PostAudioConsumeQuota(c, info, usage, "")
} else {
service.PostTextConsumeQuota(c, info, usage, nil)
}
return 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 | ⚡ Quick win

Exclude RelayModeResponsesCompact from this downgrade branch.

This condition also matches /v1/responses/compact, but responsesViaChatCompletions always writes standard Responses output via OaiChatToResponsesHandler / OaiChatToResponsesStreamHandler. That bypasses the compact-specific response and billing path at Lines 159-173, so compact callers can receive the wrong payload shape.

Suggested fix
-	if !passThroughGlobal && !info.ChannelSetting.PassThroughBodyEnabled &&
+	if info.RelayMode != relayconstant.RelayModeResponsesCompact &&
+		!passThroughGlobal && !info.ChannelSetting.PassThroughBodyEnabled &&
 		service.ShouldResponsesUseChatCompletionsGlobal(info.ChannelId, info.ChannelType, info.OriginModelName) {
📝 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
passThroughGlobal := model_setting.GetGlobalSettings().PassThroughRequestEnabled
if !passThroughGlobal && !info.ChannelSetting.PassThroughBodyEnabled &&
service.ShouldResponsesUseChatCompletionsGlobal(info.ChannelId, info.ChannelType, info.OriginModelName) {
usage, newApiErr := responsesViaChatCompletions(c, info, request)
if newApiErr != nil {
return newApiErr
}
if strings.HasPrefix(info.OriginModelName, "gpt-4o-audio") {
service.PostAudioConsumeQuota(c, info, usage, "")
} else {
service.PostTextConsumeQuota(c, info, usage, nil)
}
return nil
}
passThroughGlobal := model_setting.GetGlobalSettings().PassThroughRequestEnabled
if info.RelayMode != relayconstant.RelayModeResponsesCompact &&
!passThroughGlobal && !info.ChannelSetting.PassThroughBodyEnabled &&
service.ShouldResponsesUseChatCompletionsGlobal(info.ChannelId, info.ChannelType, info.OriginModelName) {
usage, newApiErr := responsesViaChatCompletions(c, info, request)
if newApiErr != nil {
return newApiErr
}
if strings.HasPrefix(info.OriginModelName, "gpt-4o-audio") {
service.PostAudioConsumeQuota(c, info, usage, "")
} else {
service.PostTextConsumeQuota(c, info, usage, nil)
}
return nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/responses_handler.go` around lines 74 - 87, The downgrade branch
wrongly applies to compact responses; update the conditional that routes to
responsesViaChatCompletions (the block calling
service.ShouldResponsesUseChatCompletionsGlobal and responsesViaChatCompletions)
to skip when the request is compact by adding an explicit check excluding
RelayModeResponsesCompact (e.g. ensure info.RelayMode !=
RelayModeResponsesCompact or equivalent) so compact `/v1/responses/compact`
callers are not downgraded and still follow the compact payload/billing path
handled later.

Comment on lines +81 to +85
if strings.HasPrefix(info.OriginModelName, "gpt-4o-audio") {
service.PostAudioConsumeQuota(c, info, usage, "")
} else {
service.PostTextConsumeQuota(c, info, 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 | ⚡ Quick win

Use the same usage-based audio billing check here as the compatibility handler.

This new path classifies audio solely from the model-name prefix. relay/compatible_handler.go:80-93 already uses audio token details plus ratio availability, which is safer for aliased/model-mapped names and future audio-capable models. Otherwise this branch can settle audio traffic as text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/responses_handler.go` around lines 81 - 85, The branch in
responses_handler.go that decides between PostAudioConsumeQuota and
PostTextConsumeQuota should stop relying solely on info.OriginModelName and
instead mirror the usage-based audio detection used in the compatibility
handler: check the request/response audio token details (e.g., info.AudioTokens
> 0) and the audio/text ratio availability (e.g., info.AudioTextRatio != nil or
>0) before calling service.PostAudioConsumeQuota(c, info, usage, ""); otherwise
call service.PostTextConsumeQuota(c, info, usage, nil). Replace the current
strings.HasPrefix(info.OriginModelName, "gpt-4o-audio") conditional with that
combined audio-token + ratio check so aliased or future audio-capable models are
correctly billed.

Comment on lines +99 to +100
if resp == nil {
return nil, types.NewOpenAIError(nil, types.ErrorCodeBadResponse, http.StatusInternalServerError)

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 | ⚡ Quick win

Don't pass nil into NewOpenAIError.

If DoRequest returns nil, this branch panics while building the error message because types.NewOpenAIError dereferences err.Error(). Return a concrete error instead.

Suggested fix
+import "fmt"
+
 if resp == nil {
-	return nil, types.NewOpenAIError(nil, types.ErrorCodeBadResponse, http.StatusInternalServerError)
+	return nil, types.NewOpenAIError(fmt.Errorf("upstream returned nil response"), types.ErrorCodeBadResponse, http.StatusInternalServerError)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/responses_via_chat_completions.go` around lines 99 - 100, The
nil-response branch currently calls types.NewOpenAIError(nil, ...) which causes
a panic because NewOpenAIError dereferences the error; update the resp == nil
branch to create and pass a concrete error (e.g. errors.New("empty response from
DoRequest" or similar) instead of nil when calling types.NewOpenAIError,
ensuring the function (where the check lives) returns types.NewOpenAIError(err,
types.ErrorCodeBadResponse, http.StatusInternalServerError) with that concrete
err; reference the resp == nil check and the types.NewOpenAIError call to locate
the change.

Comment on lines +4 to +10
"encoding/json"
"fmt"
"strings"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/dto"
)

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect direct encoding/json imports and calls in the touched converter files:"
rg -n -C2 'encoding/json|json\.(Marshal|Unmarshal|NewDecoder|NewEncoder)\s*\(' \
  service/openaicompat/chat_to_responses_response.go \
  service/openaicompat/responses_to_chat_request.go

Repository: QuantumNous/new-api

Length of output: 1608


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find converter function and how its errors are handled:"
rg -n "chat_to_responses_response\.go|ChatToResponses|converter|toolCalls|OaiChatToResponsesHandler" \
  service/openaicompat/chat_to_responses_response.go \
  service/openaicompat | head -n 50

echo "Locate where chat_to_responses_response converter is called (handler/stream):"
rg -n "New.*ChatToResponses|OaiChatToResponsesHandler|chat_to_responses_response" \
  service | head -n 50

echo "Inspect relay path mentioned in the review comment:"
rg -n "chat_to_responses_stream\.go" -S service/relay || true
rg -n "converter.*Responses|tool_calls|toolCalls|failed to decode tool calls" \
  service/relay || true

Repository: QuantumNous/new-api

Length of output: 2216


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find converter function and how its errors are handled:"
rg -n "ToolCalls|json\.Unmarshal|json\.Marshal|common\.Unmarshal|common\.Marshal|OaiChatToResponsesHandler" \
  service/openaicompat/chat_to_responses_response.go \
  service/openaicompat | head -n 80

echo "Locate where chat_to_responses_response converter is called (handler/stream):"
rg -n "chat_to_responses_response|OaiChatToResponsesHandler" service | head -n 80

echo "Inspect relay path mentioned in the review comment:"
rg -n "chat_to_responses_stream\.go" -S service/relay || true
rg -n "tool_calls|toolCalls|converter|ChatToResponses" \
  service/relay/channel/openai/chat_to_responses_stream.go || true

Repository: QuantumNous/new-api

Length of output: 5067


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find converter function and how its errors are handled:"
rg -n "ToolCalls|json\.Unmarshal|json\.Marshal|common\.Unmarshal|common\.Marshal|OaiChatToResponsesHandler" \
  service/openaicompat/chat_to_responses_response.go \
  service/openaicompat | head -n 80

echo "Locate where chat_to_responses_response converter is called (handler/stream):"
rg -n "chat_to_responses_response|OaiChatToResponsesHandler" service | head -n 80

echo "Inspect relay path mentioned in the review comment:"
rg -n "chat_to_responses_stream\.go" -S service/relay || true
rg -n "tool_calls|toolCalls|converter|ChatToResponses" \
  service/relay/channel/openai/chat_to_responses_stream.go || true

Repository: QuantumNous/new-api

Length of output: 5067


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== converter function(s) in chat_to_responses_response.go =="
rg -n "func " service/openaicompat/chat_to_responses_response.go
echo
echo "== find references to chat_to_responses_response converter =="
rg -n "chat_to_responses_response" service | head -n 50
echo
echo "== locate call to converter in openai stream =="
rg -n "toResponsesResponse|ResponsesResponse|chat.*responses.*response|tool_calls" service/relay/channel/openai/chat_to_responses_stream.go || true
echo
echo "== show openai stream error handling section =="
# Print a focused window around likely conversion/error code
sed -n '1,220p' service/relay/channel/openai/chat_to_responses_stream.go | nl -ba | sed -n '1,220p' >/tmp/stream_head.txt
head -n 220 /tmp/stream_head.txt
echo
echo "== show the relevant converter implementation =="
wc -l service/openaicompat/chat_to_responses_response.go
sed -n '1,220p' service/openaicompat/chat_to_responses_response.go | nl -ba | sed -n '1,220p'

Repository: QuantumNous/new-api

Length of output: 312


🏁 Script executed:

#!/bin/bash
set -u

echo "== find call sites for ChatCompletionsResponseToResponsesResponse =="
rg -n "ChatCompletionsResponseToResponsesResponse\s*\(" service || true

echo
echo "== inspect openai stream file for conversion usage =="
rg -n "ChatCompletionsResponseToResponsesResponse\s*\(|toResponsesResponse|ResponsesResponse" service/relay/channel/openai/chat_to_responses_stream.go || true

echo
echo "== show converter call + error handling area in chat_to_responses_stream.go (first 250 lines with line numbers) =="
nl -ba service/relay/channel/openai/chat_to_responses_stream.go | sed -n '1,250p'

echo
echo "== show rest of stream file around any conversion/errors (250-520) =="
nl -ba service/relay/channel/openai/chat_to_responses_stream.go | sed -n '250,520'

echo
echo "== locate OaiChatToResponsesHandler and inspect its error propagation (search and print relevant windows) =="
rg -n "OaiChatToResponsesHandler" service || true
rg -n "ChatCompletionsResponseToResponsesResponse\s*\(" service/relay/channel/openai || true

Repository: QuantumNous/new-api

Length of output: 1257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate chat_to_responses_stream.go =="
fd -a "chat_to_responses_stream.go" service . || true
fd -a "*chat_to_responses*stream*.go" service . || true

echo
echo "== locate OaiChatToResponsesHandler =="
rg -n "OaiChatToResponsesHandler" service . || true

echo
echo "== locate ChatCompletionsResponseToResponsesResponse usage (any file) =="
rg -n "ChatCompletionsResponseToResponsesResponse\s*\(" service . || true

echo
echo "== locate openai responses compat/handler file(s) =="
rg -n "responses_compat|toResponsesResponse|OpenAIResponses" service . | head -n 50 || true

Repository: QuantumNous/new-api

Length of output: 9574


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== converter implementation (service/openaicompat/chat_to_responses_response.go) =="
wc -l service/openaicompat/chat_to_responses_response.go
sed -n '1,220p' service/openaicompat/chat_to_responses_response.go | cat -n

echo
echo "== openai stream handler (relay/channel/openai/chat_to_responses_stream.go) =="
wc -l relay/channel/openai/chat_to_responses_stream.go
sed -n '1,220p' relay/channel/openai/chat_to_responses_stream.go | cat -n

echo
echo "== openai stream handler later section around conversion usage (lines 220-320 if exists) =="
sed -n '220,360p' relay/channel/openai/chat_to_responses_stream.go | cat -n

Repository: QuantumNous/new-api

Length of output: 18250


Use common JSON wrappers and propagate tool_calls decode failures
In service/openaicompat/chat_to_responses_response.go, this converter imports encoding/json and calls json.Unmarshal/json.Marshal directly (lines 4-10, 71-73, 118-125), discarding the json.Unmarshal error for msg.ToolCalls. Malformed upstream tool_calls can therefore yield a successful but incomplete Responses payload with missing function_call output items, while OaiChatToResponsesHandler only returns an error when the converter returns a non-nil err (relay/channel/openai/chat_to_responses_stream.go around 44-47). Switch to common.Unmarshal/common.Marshal and return the failure so the handler can surface a bad-response error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/openaicompat/chat_to_responses_response.go` around lines 4 - 10, The
converter in chat_to_responses_response.go is using encoding/json directly and
ignores json.Unmarshal errors for msg.ToolCalls; replace direct json usage with
common.Unmarshal/common.Marshal and ensure any unmarshal/marshal error
(especially when decoding msg.ToolCalls) is returned from the converter so
OaiChatToResponsesHandler can surface a bad-response error; specifically, swap
json.Unmarshal/json.Marshal calls to common.Unmarshal/common.Marshal for
msg.ToolCalls and any payload serialization, check their returned error values,
and propagate those errors from the converter instead of discarding them.

Comment on lines +150 to +153
// Array of items
if jsonType != "array" {
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 | ⚡ Quick win

Reject unsupported input shapes instead of dropping them.

This branch silently strips any non-string/non-array input, so an invalid or currently unsupported Responses payload can be downgraded into an empty Chat Completions request instead of failing fast with a clear 400. Returning an error here keeps the contract failure local and avoids confusing upstream errors.

Suggested fix
 	// Array of items
 	if jsonType != "array" {
-		return nil, nil
+		return nil, fmt.Errorf("unsupported input type: %s", jsonType)
 	}
📝 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
// Array of items
if jsonType != "array" {
return nil, nil
}
// Array of items
if jsonType != "array" {
return nil, fmt.Errorf("unsupported input type: %s", jsonType)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/openaicompat/responses_to_chat_request.go` around lines 150 - 153,
The code currently drops non-array "input" values by returning nil,nil when
jsonType != "array"; instead, modify the handling in the function that converts
Responses to a Chat request (the block checking jsonType and variable jsonType)
to return a clear error for unsupported input shapes (HTTP 400 / bad request)
rather than silently returning nil. Change the branch that now does "if jsonType
!= \"array\" { return nil, nil }" to construct and return an appropriate error
describing the unsupported input type so callers fail fast with a 400-like
validation error.

Comment on lines +328 to +330
extraText={t(
'model_patterns 支持正则匹配模型名称,例如 ["^deepseek-.*$", "^glm-.*$"],留空表示匹配所有模型',
)}

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 | ⚡ Quick win

Use direction-consistent model pattern examples in helper text.

Line 329 shows deepseek/glm examples under ChatCompletions→Responses, while the placeholders in this same section use gpt-*. This is confusing for admins copying examples.

Suggested fix
-                    extraText={t(
-                      'model_patterns 支持正则匹配模型名称,例如 ["^deepseek-.*$", "^glm-.*$"],留空表示匹配所有模型',
-                    )}
+                    extraText={t(
+                      'model_patterns 支持正则匹配模型名称,例如 ["^gpt-4o.*$", "^gpt-5.*$"],留空表示匹配所有模型',
+                    )}
📝 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
extraText={t(
'model_patterns 支持正则匹配模型名称,例如 ["^deepseek-.*$", "^glm-.*$"],留空表示匹配所有模型',
)}
extraText={t(
'model_patterns 支持正则匹配模型名称,例如 ["^gpt-4o.*$", "^gpt-5.*$"],留空表示匹配所有模型',
)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/classic/src/pages/Setting/Model/SettingGlobalModel.jsx` around lines 328
- 330, The helper text passed to extraText for the model pattern field (the
t('model_patterns ...') string in SettingGlobalModel.jsx) uses mixed examples
(deepseek/glm) that conflict with the placeholders elsewhere in the
ChatCompletions→Responses section (which use gpt-*); update the example regexes
in that helper string to be direction-consistent with the placeholders (e.g.,
change ["^deepseek-.*$", "^glm-.*$"] to match the gpt-* style like ["^gpt-.*$",
"^gpt-4.*$"] or vice versa) so admins see consistent examples; edit the t
key/string accordingly in SettingGlobalModel.jsx where extraText is set.

Comment on lines +411 to +413
<Tag color='orange' size='small'>
测试版
</Tag>

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 | ⚡ Quick win

Avoid hardcoded 测试版; keep the new section fully translatable.

Line 412 uses hardcoded Chinese text in a newly added UI block. This breaks locale consistency in non-Chinese classic UI.

Suggested fix
-                  <Tag color='orange' size='small'>
-                    测试版
-                  </Tag>
+                  <Tag color='orange' size='small'>{t('测试版')}</Tag>
📝 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
<Tag color='orange' size='small'>
测试版
</Tag>
<Tag color='orange' size='small'>{t('测试版')}</Tag>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/classic/src/pages/Setting/Model/SettingGlobalModel.jsx` around lines 411
- 413, The Tag component in SettingGlobalModel.jsx currently contains hardcoded
Chinese text "测试版"; replace this with a translatable string by calling the app's
localization helper (e.g., use intl.formatMessage({ id: 'setting.beta',
defaultMessage: 'Beta' }) or the project's t('setting.beta') helper) instead of
the literal characters, update/create the corresponding locale key
("setting.beta") in the locale files, and ensure the Tag markup (the <Tag ...>
... </Tag> instance) uses the translated value so the label renders correctly
for all locales.

将 docker-compose.yml 中 PostgreSQL 的数据卷从 Docker 命名卷
(pg_data) 改为宿主机目录挂载 (./postgresql),方便数据备份和管理

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docker-compose.yml`:
- Around line 92-93: Update the commented MySQL section so the instructions
explicitly require uncommenting both the top-level "volumes:" header and the
"mysql_data:" entry (referencing the commented "volumes:" and "mysql_data:"
symbols) to avoid producing invalid YAML; modify the quick-start comment text
that surrounds the commented block to state "Uncomment both 'volumes:' and
'mysql_data:'" and ensure the sample shows both lines uncommented together.
- Line 72: The docker-compose bind-mount entry (-
./postgresql:/var/lib/postgresql/data) requires users to pre-create the
./postgresql directory with permissions/ownership compatible with the Postgres
container user; update the README (or docs) to add a short setup note
instructing users to create ./postgresql and chown it to the Postgres UID/GID
used by the image (e.g., 999:999 or the value of PG_UID/PG_GID) or adjust
permissions accordingly before running docker-compose, and add postgresql/ to
the repository .gitignore so the data directory is not committed; reference the
docker-compose.yml bind-mount line to locate the change, update README.md (or
docs/setup.md) to include the steps, and add the postgresql/ pattern to
.gitignore.
🪄 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: c2bdc2dc-db05-4356-ae8f-2e29a25eb6ad

📥 Commits

Reviewing files that changed from the base of the PR and between 0f9ead6 and 6846681.

📒 Files selected for processing (1)
  • docker-compose.yml

Comment thread docker-compose.yml Outdated
Comment thread docker-compose.yml Outdated
Comment on lines 92 to 93
#volumes:
# mysql_data:

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 | ⚡ Quick win

Clarify MySQL enablement comments to include volumes: header.

Line 92 leaves volumes: commented. The quick-start notes should explicitly tell users to uncomment both volumes: and mysql_data:; otherwise they can end up with invalid YAML while following the switch-to-MySQL steps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.yml` around lines 92 - 93, Update the commented MySQL section
so the instructions explicitly require uncommenting both the top-level
"volumes:" header and the "mysql_data:" entry (referencing the commented
"volumes:" and "mysql_data:" symbols) to avoid producing invalid YAML; modify
the quick-start comment text that surrounds the commented block to state
"Uncomment both 'volumes:' and 'mysql_data:'" and ensure the sample shows both
lines uncommented together.

Rosons and others added 7 commits May 31, 2026 13:43
将 PostgreSQL 数据卷从本地目录挂载改回命名卷 pg_data,
恢复 volumes 定义中的 pg_data 条目。`.dockerignore` 已保持原始状态。
默认使用 goproxy.cn 加速 Go 依赖下载,可通过
--build-arg GOPROXY=xxx 自定义覆盖

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
注释掉远程镜像 calciumion/new-api:latest,
改为 build: . 使用本地代码构建,便于开发测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
使用 RUN --mount=type=cache,target=/go/pkg/mod 缓存
Go 模块,避免每次构建都重新下载所有依赖

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
使用 --mount=type=cache 缓存 Go 模块和编译产物,
GOCACHE 设为独立路径 /cache/go-build,不依赖用户目录

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
将 Go 编译临时目录(GOTMPDIR)也指向 cache mount,
确保所有编译临时文件都不写入容器可写层

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
使用 --mount=type=cache 缓存 Go 模块与编译产物,
设置 GOCACHE/GOTMPDIR 将临时文件定向到缓存挂载点
而非容器可写层,避免根分区磁盘空间不足

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Nuctori

Nuctori commented Jun 3, 2026

Copy link
Copy Markdown

补充一个我这边用真实 Codex CLI -> 本地 new-api -> DeepSeek 渠道联调时遇到的兼容细节:

当前这里已经支持把 Responses 的 function tool 顶层结构转换成 Chat Completions 的 function 格式,这点没有问题。
但如果真实请求里出现了 type == "function"name 为空字符串的工具项,当前实现看起来会继续透传,可能导致上游直接返回 400。

我这边真实遇到的上游错误是:

Invalid 'tools[0].function.name': empty string. Expected a string with minimum length 1

本地做法是:

  • 继续保留顶层 Responses function tool 的兼容转换
  • 但在转换后对空 name 的 function tool 直接过滤掉,不再透传给上游

修完后重新跑真实 codex exec -> /v1/responses -> DeepSeek,请求已经可以成功返回 OK

如果这个 PR 的目标是覆盖真实 Codex 流量,这里建议补一个空 function.name 的保护逻辑,避免 compat 层把无效工具继续转发给不兼容的上游。

Rosons and others added 2 commits June 3, 2026 23:26
当Codex CLI通过Responses API传入type为function但name为空的工具项时,
convertResponsesToolsToChatTools会将其透传给上游Chat Completions接口,
导致DeepSeek等上游返回400错误(function.name最小长度为1)。

在转换后对name为空或仅空白字符的function tool直接continue跳过,
确保无效工具不会转发给不兼容的上游。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Rosons

Rosons commented Jun 3, 2026

Copy link
Copy Markdown
Author

好的,已经修复好并提交了

@xiaodingchen

Copy link
Copy Markdown

为何还不合并呀

Comment thread Dockerfile

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

请不要在 Dockerfile 中引入无意义的部分 (配置镜像/配置缓存) 或者说请不要提交本地测试用的 Dockerfile

@wuxushun

Copy link
Copy Markdown

这个配置放在渠道管理里是不是更好?

@wuxushun

Copy link
Copy Markdown

{
"enabled": true,
"all_channels": false,
"channel_ids": [
1
],
"channel_types": [
1
],
"model_patterns": [
"^qwen.$",
"^glm.
$",
"^kimi.*$"
]
}

不生效呀??

@Deali-Axy

Copy link
Copy Markdown

什么时候会合并这个功能呀?
或者大家都是如何解决 Response 到 chat completions 的转换的?

@PaleNeutron

Copy link
Copy Markdown

什么时候会合并这个功能呀? 或者大家都是如何解决 Response 到 chat completions 的转换的?

再挂一层litellm 😢

@Coean

Coean commented Jul 2, 2026

Copy link
Copy Markdown

什么时候会合并这个功能呀?
最近JD送的GLM5.2继续转换api

lzc256 added a commit to lzc256/build that referenced this pull request Jul 23, 2026
Based on PR QuantumNous/new-api#5209.
Adapted for upstream refactoring (openaicompat -> relayconvert).
- developer role auto-mapped to system
- non-function tools filtered out
- tool parameter schema patched
- DeepSeek thinking mode adapted
- Streaming and non-streaming response conversion

Excludes Dockerfile/docker-compose.yml (fork-specific build config).
@thomasgogo

Copy link
Copy Markdown

是的,这个到底什么时候合并到master?
现在基于openapi的调用太用了

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.

9 participants