feat: support Responses to Chat#5787
Conversation
Add a shared Responses-to-Chat stream state machine and use it from the OpenAI relay path. Preserve assistant text alongside tool calls, bind tool argument deltas by output_index, map incomplete finish reasons, support reasoning/custom tool events, and buffer upstream SSE for non-stream Chat clients. Add deterministic service tests and relay SSE tests for the conversion path. Related to #5745.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
🚧 Files skipped from review as they are similar to previous changes (6)
WalkthroughAdds a new ChangesResponses ↔ Chat Completions Bidirectional Conversion
Sequence Diagram(s)sequenceDiagram
participant Client
participant AdvancedCustomAdaptor
participant relayconvert
participant UpstreamChatAPI
Client->>AdvancedCustomAdaptor: POST /v1/responses (OpenAIResponsesRequest)
AdvancedCustomAdaptor->>relayconvert: ResponsesRequestToChatCompletionsRequest
relayconvert-->>AdvancedCustomAdaptor: GeneralOpenAIRequest
AdvancedCustomAdaptor->>UpstreamChatAPI: POST /v1/chat/completions
UpstreamChatAPI-->>AdvancedCustomAdaptor: ChatCompletions response/stream
AdvancedCustomAdaptor->>relayconvert: ChatCompletionsStreamChunkToResponsesEvents / ChatCompletionsResponseToResponsesResponse
relayconvert-->>AdvancedCustomAdaptor: ResponsesStreamEvents / OpenAIResponsesResponse
AdvancedCustomAdaptor-->>Client: Responses SSE stream or JSON response
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
relay/channel/gemini/relay_responses_test.go (1)
52-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
assertfor the value checks in these tests.After the fatal setup passes, the remaining equality/substring checks should use
assertto match the repo’s Go test convention. As per coding guidelines, “New or substantially rewritten Go backend tests MUST usegithub.com/stretchr/testify/requirefor setup and fatal assertions, andgithub.com/stretchr/testify/assertfor non-fatal value checks.”Also applies to: 130-150
🤖 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/gemini/relay_responses_test.go` around lines 52 - 65, The test in relay_responses_test.go is using require for non-fatal value and substring checks after setup has already passed; switch those assertions in the affected test blocks to assert to match the Go test convention. Keep require only for setup/fatal checks like nil validation, and update the remaining equality/contains/not-contains checks in the relay response tests (including the later similar block) to use assert, referencing the same test functions so the intent stays consistent.Source: Coding guidelines
🤖 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/gemini/relay_responses.go`:
- Around line 21-27: In GeminiResponsesHandler, the upstream response body is
only closed after io.ReadAll succeeds, so a read error can leak the connection.
Move the body cleanup to run before the read, ideally with a deferred call right
after entering GeminiResponsesHandler, and keep the rest of the response
parsing/logging logic unchanged.
In `@relay/chat_completions_via_responses.go`:
- Around line 148-150: The SSE detection in the
`chat_completions_via_responses.go` flow is doing a case-sensitive
`strings.HasPrefix` check on the upstream Content-Type, so valid
`text/event-stream` responses with different casing can be missed. Update the
logic around `info.IsStream` to normalize or parse the
`httpResp.Header.Get("Content-Type")` value before checking for SSE, using the
existing stream detection path in `chat_completions_via_responses.go` so
`upstreamStream` is set correctly regardless of media-type casing.
In `@service/convert.go`:
- Around line 631-636: The fallback in `convert.go` for `claudeContent.Input`
currently lets empty, invalid, or null `toolUse.Function.Arguments` become a
string, which breaks the expected object shape. Update the `tool_use` conversion
logic around `common.Unmarshal` so `claudeContent.Input` always remains an
object/map on fallback, and treat non-parseable or null arguments as an empty
object rather than copying the raw string.
In `@service/relayconvert/responses_request_to_chat.go`:
- Around line 169-172: The conversion logic in
responsesInputTypeFunctionCallOutput and the function-tool handling path should
fail fast when required identifiers are missing instead of emitting invalid Chat
Completions payloads. Add validation before appending dto.Message so
function_call_output without a non-empty call_id and type:"function" tools
without a non-empty name return a converter error immediately. Keep the check
close to the existing responseToolOutputToChatContent and tool-mapping logic so
malformed references are rejected before building the chat payload.
In `@service/relayconvert/responses_to_chat.go`:
- Around line 784-794: The buffered argument accumulator in ProcessEvent is
duplicating tool arguments because
responsesEventFunctionArgsDelta/responsesEventCustomToolInputDelta appends the
same delta to both pendingByOutputIndex and pendingByItemID when both
identifiers are present. Update the ProcessEvent path in responses_to_chat.go to
mirror toolArgumentsDelta’s behavior by preferring OutputIndex and only falling
back to ItemID when OutputIndex is absent, so ensureTool does not drain the same
delta twice into tool.Arguments.
---
Nitpick comments:
In `@relay/channel/gemini/relay_responses_test.go`:
- Around line 52-65: The test in relay_responses_test.go is using require for
non-fatal value and substring checks after setup has already passed; switch
those assertions in the affected test blocks to assert to match the Go test
convention. Keep require only for setup/fatal checks like nil validation, and
update the remaining equality/contains/not-contains checks in the relay response
tests (including the later similar block) to use assert, referencing the same
test functions so the intent stays consistent.
🪄 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: d640591e-3bc7-4b1a-b537-d417ef4e723e
📒 Files selected for processing (35)
.gitignoredto/channel_settings.godto/channel_settings_test.godto/openai_response.gorelay/channel/advancedcustom/adaptor.gorelay/channel/advancedcustom/adaptor_test.gorelay/channel/gemini/adaptor.gorelay/channel/gemini/adaptor_responses.gorelay/channel/gemini/adaptor_responses_test.gorelay/channel/gemini/relay_responses.gorelay/channel/gemini/relay_responses_test.gorelay/channel/openai/chat_via_responses.gorelay/channel/openai/chat_via_responses_test.gorelay/channel/openai/responses_via_chat.gorelay/chat_completions_via_responses.goservice/convert.goservice/openai_chat_responses_compat.goservice/openai_chat_responses_mode.goservice/openaicompat/responses_to_chat.goservice/relayconvert/chat_responses_compat_test.goservice/relayconvert/chat_to_responses.goservice/relayconvert/chat_to_responses_response.goservice/relayconvert/policy.goservice/relayconvert/regex.goservice/relayconvert/responses_request_to_chat.goservice/relayconvert/responses_request_to_chat_test.goservice/relayconvert/responses_to_chat.goweb/default/src/features/channels/lib/advanced-custom.tsweb/default/src/features/channels/types.tsweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/fr.jsonweb/default/src/i18n/locales/ja.jsonweb/default/src/i18n/locales/ru.jsonweb/default/src/i18n/locales/vi.jsonweb/default/src/i18n/locales/zh.json
💤 Files with no reviewable changes (1)
- service/openaicompat/responses_to_chat.go
| case responsesInputTypeFunctionCallOutput: | ||
| callID := strings.TrimSpace(common.Interface2String(item["call_id"])) | ||
| content := responseToolOutputToChatContent(item["output"]) | ||
| return append(messages, dto.Message{Role: "tool", ToolCallId: callID, Content: content}), nil |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reject malformed tool references before building the chat payload.
function_call_output without call_id and type:"function" tools without name currently get converted into Chat Completions shapes the upstream API cannot validate. Failing fast here will return a precise converter error instead of leaking a later 400 from the chat endpoint.
Suggested fix
case responsesInputTypeFunctionCallOutput:
callID := strings.TrimSpace(common.Interface2String(item["call_id"]))
+ if callID == "" {
+ return nil, errors.New("function_call_output item is missing call_id")
+ }
content := responseToolOutputToChatContent(item["output"])
return append(messages, dto.Message{Role: "tool", ToolCallId: callID, Content: content}), nil
}
@@
for _, tool := range tools {
toolType := strings.TrimSpace(common.Interface2String(tool["type"]))
if toolType == "function" {
+ name := strings.TrimSpace(common.Interface2String(tool["name"]))
+ if name == "" {
+ return nil, errors.New("function tool is missing name")
+ }
out = append(out, dto.ToolCallRequest{
Type: "function",
Function: dto.FunctionRequest{
- Name: strings.TrimSpace(common.Interface2String(tool["name"])),
+ Name: name,
Description: common.Interface2String(tool["description"]),
Parameters: tool["parameters"],
},
})Also applies to: 321-330
🤖 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/relayconvert/responses_request_to_chat.go` around lines 169 - 172,
The conversion logic in responsesInputTypeFunctionCallOutput and the
function-tool handling path should fail fast when required identifiers are
missing instead of emitting invalid Chat Completions payloads. Add validation
before appending dto.Message so function_call_output without a non-empty call_id
and type:"function" tools without a non-empty name return a converter error
immediately. Keep the check close to the existing
responseToolOutputToChatContent and tool-mapping logic so malformed references
are rejected before building the chat payload.
8020a18 to
b59eb9e
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/chat_completions_via_responses.go`:
- Around line 183-184: The content-type check in
isResponsesEventStreamContentType should not rely on substring matching, since
it can misclassify non-SSE responses that merely contain “text/event-stream” in
a parameter. Update the helper to parse the header with the MIME media type
logic and compare only the actual media type token case-insensitively, keeping
the existing call sites in chat_completions_via_responses.go unchanged.
🪄 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: fb5a6772-fc5b-4d41-9413-abd27bf1f70c
📒 Files selected for processing (8)
relay/channel/gemini/relay_responses.gorelay/channel/gemini/relay_responses_test.gorelay/chat_completions_via_responses.gorelay/chat_completions_via_responses_test.goservice/convert.goservice/convert_test.goservice/relayconvert/chat_responses_compat_test.goservice/relayconvert/responses_to_chat.go
🚧 Files skipped from review as they are similar to previous changes (4)
- relay/channel/gemini/relay_responses.go
- relay/channel/gemini/relay_responses_test.go
- service/relayconvert/responses_to_chat.go
- service/relayconvert/chat_responses_compat_test.go
| func isResponsesEventStreamContentType(contentType string) bool { | ||
| return strings.Contains(strings.ToLower(contentType), "text/event-stream") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Parse the media type instead of substring-matching it.
strings.Contains(strings.ToLower(contentType), "text/event-stream") will also return true for headers like application/json; note=text/event-stream, so a non-stream response can be routed through the SSE path. Compare the actual media type token case-insensitively (for example via mime.ParseMediaType) rather than searching the whole header string.
Suggested fix
+import "mime"
+
func isResponsesEventStreamContentType(contentType string) bool {
- return strings.Contains(strings.ToLower(contentType), "text/event-stream")
+ mediaType, _, err := mime.ParseMediaType(contentType)
+ return err == nil && strings.EqualFold(mediaType, "text/event-stream")
}📝 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.
| func isResponsesEventStreamContentType(contentType string) bool { | |
| return strings.Contains(strings.ToLower(contentType), "text/event-stream") | |
| import "mime" | |
| func isResponsesEventStreamContentType(contentType string) bool { | |
| mediaType, _, err := mime.ParseMediaType(contentType) | |
| return err == nil && strings.EqualFold(mediaType, "text/event-stream") | |
| } |
🤖 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/chat_completions_via_responses.go` around lines 183 - 184, The
content-type check in isResponsesEventStreamContentType should not rely on
substring matching, since it can misclassify non-SSE responses that merely
contain “text/event-stream” in a parameter. Update the helper to parse the
header with the MIME media type logic and compare only the actual media type
token case-insensitively, keeping the existing call sites in
chat_completions_via_responses.go unchanged.
…finement Upstream commits merged: - feat: support Responses to Chat (QuantumNous#5787) - fix: update section titles and improve layout in channel components - feat: refine channel management UI Changes applied: - Gemini Responses API adaptor and relay handlers - OpenAI Responses↔Chat conversion (responses_via_chat.go) - Package rename: service/openaicompat → service/relayconvert - Channel management UI refactor (batch mode, drawer layout) - Advanced custom converter: openai_responses_to_openai_chat_completions - Brand rename (NewAPIError → AIHubError) applied to all new files Conflicts resolved (8 files): - relay/channel/gemini/adaptor.go: manual merge (keep AIHubError + add Responses mode) - channel-mutate-drawer.tsx: adopt upstream + reapply brand rename - 6 i18n locale files: both sides' new keys retained Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Important
📝 变更描述 / Description
支持 Responses to Chat 兼容转换:新增 Responses request 转 Chat Completions request 的回退能力,并补齐 Chat Completions 响应回转为 Responses 的非流式与流式输出。Advanced Custom 新增 OpenAI Responses to OpenAI Chat 转换选项;Gemini 渠道在客户端请求 /v1/responses 时会将 Gemini 响应转换回 OpenAI Responses 格式,避免上游已有内容但 Responses 客户端无法渲染。
同时修复 Responses 流式工具参数归位中 output_index 与 item_id 双 pending 导致参数重复的问题,保持 output_index 作为主绑定键,item_id 作为兼容补充。
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
已通过以下验证:
Summary by CodeRabbit
New Features
Bug Fixes