Files
ds2api/internal/httpapi/openai/responses/handler.go
CJACK 0378d8c0a9 feat: add empty-output retry and Vercel auto-continue support
- Auto-retry Chat/Responses streams once when upstream output is empty but not content-filtered, reusing session/token/PoW and appending a regeneration suffix to the prompt
- Wire DeepSeek continue API into Vercel streams for multi-round thinking output exhaustion
- Defer empty-output errors in stream finalizers to enable synthetic retry; only surface failure when the retry budget is exhausted
- Track content_filter stops to avoid retry on filtered outputs
- Add comprehensive tests for stream/non-stream retry, Responses retry, and content_filter no-retry
- Update prompt-compatibility.md documentation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 18:00:52 +08:00

139 lines
4.0 KiB
Go

package responses
import (
"context"
"net/http"
"sync"
"ds2api/internal/auth"
"ds2api/internal/chathistory"
"ds2api/internal/httpapi/openai/files"
"ds2api/internal/httpapi/openai/history"
"ds2api/internal/httpapi/openai/shared"
"ds2api/internal/promptcompat"
"ds2api/internal/toolcall"
"ds2api/internal/toolstream"
)
const openAIGeneralMaxSize = shared.GeneralMaxSize
var writeJSON = shared.WriteJSON
type Handler struct {
Store shared.ConfigReader
Auth shared.AuthResolver
DS shared.DeepSeekCaller
ChatHistory *chathistory.Store
responsesMu sync.Mutex
responses *responseStore
}
func (h *Handler) compatStripReferenceMarkers() bool {
if h == nil {
return true
}
return shared.CompatStripReferenceMarkers(h.Store)
}
func (h *Handler) applyHistorySplit(ctx context.Context, a *auth.RequestAuth, stdReq promptcompat.StandardRequest) (promptcompat.StandardRequest, error) {
if h == nil {
return stdReq, nil
}
stdReq = shared.ApplyThinkingInjection(h.Store, stdReq)
svc := history.Service{Store: h.Store, DS: h.DS}
out, err := svc.ApplyCurrentInputFile(ctx, a, stdReq)
if err != nil {
return stdReq, err
}
if out.CurrentInputFileApplied {
return out, nil
}
return svc.Apply(ctx, a, out)
}
func (h *Handler) preprocessInlineFileInputs(ctx context.Context, a *auth.RequestAuth, req map[string]any) error {
if h == nil {
return nil
}
return (&files.Handler{Store: h.Store, Auth: h.Auth, DS: h.DS, ChatHistory: h.ChatHistory}).PreprocessInlineFileInputs(ctx, a, req)
}
func (h *Handler) toolcallFeatureMatchEnabled() bool {
if h == nil {
return shared.ToolcallFeatureMatchEnabled(nil)
}
return shared.ToolcallFeatureMatchEnabled(h.Store)
}
func (h *Handler) toolcallEarlyEmitHighConfidence() bool {
if h == nil {
return shared.ToolcallEarlyEmitHighConfidence(nil)
}
return shared.ToolcallEarlyEmitHighConfidence(h.Store)
}
func writeOpenAIError(w http.ResponseWriter, status int, message string) {
shared.WriteOpenAIError(w, status, message)
}
func writeOpenAIErrorWithCode(w http.ResponseWriter, status int, message, code string) {
shared.WriteOpenAIErrorWithCode(w, status, message, code)
}
func openAIErrorType(status int) string {
return shared.OpenAIErrorType(status)
}
func writeOpenAIInlineFileError(w http.ResponseWriter, err error) {
files.WriteInlineFileError(w, err)
}
func mapHistorySplitError(err error) (int, string) {
return history.MapError(err)
}
func requestTraceID(r *http.Request) string {
return shared.RequestTraceID(r)
}
func cleanVisibleOutput(text string, stripReferenceMarkers bool) string {
return shared.CleanVisibleOutput(text, stripReferenceMarkers)
}
func replaceCitationMarkersWithLinks(text string, links map[int]string) string {
return shared.ReplaceCitationMarkersWithLinks(text, links)
}
func upstreamEmptyOutputDetail(contentFilter bool, text, thinking string) (int, string, string) {
return shared.UpstreamEmptyOutputDetail(contentFilter, text, thinking)
}
func writeUpstreamEmptyOutputError(w http.ResponseWriter, text, thinking string, contentFilter bool) bool {
return shared.WriteUpstreamEmptyOutputError(w, text, thinking, contentFilter)
}
func emptyOutputRetryEnabled() bool {
return shared.EmptyOutputRetryEnabled()
}
func emptyOutputRetryMaxAttempts() int {
return shared.EmptyOutputRetryMaxAttempts()
}
func clonePayloadWithEmptyOutputRetryPrompt(payload map[string]any) map[string]any {
return shared.ClonePayloadWithEmptyOutputRetryPrompt(payload)
}
func usagePromptWithEmptyOutputRetry(originalPrompt string, retryAttempts int) string {
return shared.UsagePromptWithEmptyOutputRetry(originalPrompt, retryAttempts)
}
func filterIncrementalToolCallDeltasByAllowed(deltas []toolstream.ToolCallDelta, seenNames map[int]string) []toolstream.ToolCallDelta {
return shared.FilterIncrementalToolCallDeltasByAllowed(deltas, seenNames)
}
func detectAssistantToolCalls(text, exposedThinking, detectionThinking string, toolNames []string) toolcall.ToolCallParseResult {
return shared.DetectAssistantToolCalls(text, exposedThinking, detectionThinking, toolNames)
}