feat(ai-proxy): add promoteThinkingOnEmpty and hiclawMode config options (#3625)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
澄潭
2026-03-20 00:39:47 +08:00
committed by GitHub
parent 045238944d
commit ca7ee6ef5f
6 changed files with 442 additions and 1 deletions

View File

@@ -385,6 +385,8 @@ func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.Plugin
return chunk
}
promoteThinking := pluginConfig.GetProviderConfig().GetPromoteThinkingOnEmpty()
log.Debugf("[onStreamingResponseBody] provider=%s", activeProvider.GetProviderType())
log.Debugf("[onStreamingResponseBody] isLastChunk=%v chunk: %s", isLastChunk, string(chunk))
@@ -392,6 +394,9 @@ func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.Plugin
apiName, _ := ctx.GetContext(provider.CtxKeyApiName).(provider.ApiName)
modifiedChunk, err := handler.OnStreamingResponseBody(ctx, apiName, chunk, isLastChunk)
if err == nil && modifiedChunk != nil {
if promoteThinking {
modifiedChunk = promoteThinkingInStreamingChunk(ctx, modifiedChunk, isLastChunk)
}
// Convert to Claude format if needed
claudeChunk, convertErr := convertStreamingResponseToClaude(ctx, modifiedChunk)
if convertErr != nil {
@@ -435,6 +440,10 @@ func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.Plugin
result := []byte(responseBuilder.String())
if promoteThinking {
result = promoteThinkingInStreamingChunk(ctx, result, isLastChunk)
}
// Convert to Claude format if needed
claudeChunk, convertErr := convertStreamingResponseToClaude(ctx, result)
if convertErr != nil {
@@ -443,11 +452,12 @@ func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.Plugin
return claudeChunk
}
if !needsClaudeResponseConversion(ctx) {
if !needsClaudeResponseConversion(ctx) && !promoteThinking {
return chunk
}
// If provider doesn't implement any streaming handlers but we need Claude conversion
// or thinking promotion
// First extract complete events from the chunk
events := provider.ExtractStreamingEvents(ctx, chunk)
log.Debugf("[onStreamingResponseBody] %d events received (no handler)", len(events))
@@ -464,6 +474,10 @@ func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.Plugin
result := []byte(responseBuilder.String())
if promoteThinking {
result = promoteThinkingInStreamingChunk(ctx, result, isLastChunk)
}
// Convert to Claude format if needed
claudeChunk, convertErr := convertStreamingResponseToClaude(ctx, result)
if convertErr != nil {
@@ -496,6 +510,16 @@ func onHttpResponseBody(ctx wrapper.HttpContext, pluginConfig config.PluginConfi
finalBody = body
}
// Promote thinking/reasoning to content when content is empty
if pluginConfig.GetProviderConfig().GetPromoteThinkingOnEmpty() {
promoted, err := provider.PromoteThinkingOnEmptyResponse(finalBody)
if err != nil {
log.Warnf("[promoteThinkingOnEmpty] failed: %v", err)
} else {
finalBody = promoted
}
}
// Convert to Claude format if needed (applies to both branches)
convertedBody, err := convertResponseBodyToClaude(ctx, finalBody)
if err != nil {
@@ -544,6 +568,49 @@ func convertStreamingResponseToClaude(ctx wrapper.HttpContext, data []byte) ([]b
return claudeChunk, nil
}
// promoteThinkingInStreamingChunk processes SSE-formatted streaming data, buffering
// reasoning deltas and stripping them from chunks. On the last chunk, if no content
// was ever seen, it appends a flush chunk that emits buffered reasoning as content.
func promoteThinkingInStreamingChunk(ctx wrapper.HttpContext, data []byte, isLastChunk bool) []byte {
// SSE data contains lines like "data: {...}\n\n"
// We need to find and process each data line
lines := strings.Split(string(data), "\n")
modified := false
for i, line := range lines {
if !strings.HasPrefix(line, "data: ") {
continue
}
payload := strings.TrimPrefix(line, "data: ")
if payload == "[DONE]" || payload == "" {
continue
}
stripped, err := provider.PromoteStreamingThinkingOnEmptyChunk(ctx, []byte(payload))
if err != nil {
continue
}
newLine := "data: " + string(stripped)
if newLine != line {
lines[i] = newLine
modified = true
}
}
result := data
if modified {
result = []byte(strings.Join(lines, "\n"))
}
// On last chunk, flush buffered reasoning as content if no content was seen
if isLastChunk {
flushChunk := provider.PromoteStreamingThinkingFlush(ctx)
if flushChunk != nil {
result = append(flushChunk, result...)
}
}
return result
}
// Helper function to convert OpenAI response body to Claude format
func convertResponseBodyToClaude(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
if !needsClaudeResponseConversion(ctx) {