From 7e13a01a9684be07b1da69d661e68b9790786558 Mon Sep 17 00:00:00 2001 From: Seefs Date: Wed, 4 Feb 2026 22:39:35 +0800 Subject: [PATCH] fix: map Responses reasoning stream to chat completion deltas fix: default summary = detailed fix ReasoningContent fix ReasoningContent fix ReasoningContent fix ReasoningContent Revert "fix ReasoningContent" This reverts commit 45a88f78b91ce2376bca68745d19374bb9e95e88. fix ReasoningContent fix ReasoningContent --- dto/openai_response.go | 12 ++- relay/channel/openai/chat_via_responses.go | 112 +++++++++++++++++++++ service/openaicompat/chat_to_responses.go | 5 +- 3 files changed, 125 insertions(+), 4 deletions(-) diff --git a/dto/openai_response.go b/dto/openai_response.go index 19ca92905..a405b9743 100644 --- a/dto/openai_response.go +++ b/dto/openai_response.go @@ -352,6 +352,11 @@ type ResponsesOutputContent struct { Annotations []interface{} `json:"annotations"` } +type ResponsesReasoningSummaryPart struct { + Type string `json:"type"` + Text string `json:"text"` +} + const ( BuildInToolWebSearchPreview = "web_search_preview" BuildInToolFileSearch = "file_search" @@ -374,8 +379,11 @@ type ResponsesStreamResponse struct { Item *ResponsesOutput `json:"item,omitempty"` // - response.function_call_arguments.delta // - response.function_call_arguments.done - OutputIndex *int `json:"output_index,omitempty"` - ItemID string `json:"item_id,omitempty"` + OutputIndex *int `json:"output_index,omitempty"` + ContentIndex *int `json:"content_index,omitempty"` + SummaryIndex *int `json:"summary_index,omitempty"` + ItemID string `json:"item_id,omitempty"` + Part *ResponsesReasoningSummaryPart `json:"part,omitempty"` } // GetOpenAIError 从动态错误类型中提取OpenAIError结构 diff --git a/relay/channel/openai/chat_via_responses.go b/relay/channel/openai/chat_via_responses.go index 83f9734c9..d00b53907 100644 --- a/relay/channel/openai/chat_via_responses.go +++ b/relay/channel/openai/chat_via_responses.go @@ -18,6 +18,26 @@ import ( "github.com/gin-gonic/gin" ) +func responsesStreamIndexKey(itemID string, idx *int) string { + if itemID == "" { + return "" + } + if idx == nil { + return itemID + } + return fmt.Sprintf("%s:%d", itemID, *idx) +} + +func stringDeltaFromPrefix(prev string, next string) string { + if next == "" { + return "" + } + if prev != "" && strings.HasPrefix(next, prev) { + return next[len(prev):] + } + return next +} + func OaiResponsesToChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) { if resp == nil || resp.Body == nil { return nil, types.NewOpenAIError(fmt.Errorf("invalid response"), types.ErrorCodeBadResponse, http.StatusInternalServerError) @@ -86,6 +106,7 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo toolCallArgsByID := make(map[string]string) toolCallNameSent := make(map[string]bool) toolCallCanonicalIDByItemID := make(map[string]string) + //reasoningSummaryTextByKey := make(map[string]string) sendStartIfNeeded := func() bool { if sentStart { @@ -99,6 +120,66 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo return true } + //sendReasoningDelta := func(delta string) bool { + // if delta == "" { + // return true + // } + // if !sendStartIfNeeded() { + // return false + // } + // + // usageText.WriteString(delta) + // chunk := &dto.ChatCompletionsStreamResponse{ + // Id: responseId, + // Object: "chat.completion.chunk", + // Created: createAt, + // Model: model, + // Choices: []dto.ChatCompletionsStreamResponseChoice{ + // { + // Index: 0, + // Delta: dto.ChatCompletionsStreamResponseChoiceDelta{ + // ReasoningContent: &delta, + // }, + // }, + // }, + // } + // if err := helper.ObjectData(c, chunk); err != nil { + // streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) + // return false + // } + // return true + //} + + sendReasoningSummaryDelta := func(delta string) bool { + if delta == "" { + return true + } + if !sendStartIfNeeded() { + return false + } + + usageText.WriteString(delta) + chunk := &dto.ChatCompletionsStreamResponse{ + Id: responseId, + Object: "chat.completion.chunk", + Created: createAt, + Model: model, + Choices: []dto.ChatCompletionsStreamResponseChoice{ + { + Index: 0, + Delta: dto.ChatCompletionsStreamResponseChoiceDelta{ + ReasoningContent: &delta, + }, + }, + }, + } + if err := helper.ObjectData(c, chunk); err != nil { + streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) + return false + } + return true + } + sendToolCallDelta := func(callID string, name string, argsDelta string) bool { if callID == "" { return true @@ -188,6 +269,37 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo } } + //case "response.reasoning_text.delta": + //if !sendReasoningDelta(streamResp.Delta) { + // return false + //} + + //case "response.reasoning_text.done": + + case "response.reasoning_summary_text.delta": + if !sendReasoningSummaryDelta(streamResp.Delta) { + return false + } + + case "response.reasoning_summary_text.done": + + //case "response.reasoning_summary_part.added", "response.reasoning_summary_part.done": + // key := responsesStreamIndexKey(strings.TrimSpace(streamResp.ItemID), streamResp.SummaryIndex) + // if key == "" || streamResp.Part == nil { + // break + // } + // // Only handle summary text parts, ignore other part types. + // if streamResp.Part.Type != "" && streamResp.Part.Type != "summary_text" { + // break + // } + // prev := reasoningSummaryTextByKey[key] + // next := streamResp.Part.Text + // delta := stringDeltaFromPrefix(prev, next) + // reasoningSummaryTextByKey[key] = next + // if !sendReasoningSummaryDelta(delta) { + // return false + // } + case "response.output_text.delta": if !sendStartIfNeeded() { return false diff --git a/service/openaicompat/chat_to_responses.go b/service/openaicompat/chat_to_responses.go index 3779db934..76aa6d25d 100644 --- a/service/openaicompat/chat_to_responses.go +++ b/service/openaicompat/chat_to_responses.go @@ -346,9 +346,10 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d Metadata: req.Metadata, } - if req.ReasoningEffort != "" && req.ReasoningEffort != "none" { + if req.ReasoningEffort != "" { out.Reasoning = &dto.Reasoning{ - Effort: req.ReasoningEffort, + Effort: req.ReasoningEffort, + Summary: "detailed", } }