// // Copyright 2026 The InfiniFlow Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // package models import ( "encoding/json" "fmt" "io" "ragflow/internal/common" "go.uber.org/zap" ) // HandleNonStreamingResponse processes a complete non-streaming chat // response using the ParserConfig's ResponseParser to extract usage. func HandleNonStreamingResponse( body []byte, modelUsage *common.ModelUsage, chatConfig *ChatConfig, cfg *ParserConfig, ) (*ChatResponse, error) { var result map[string]any if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } // Extract usage via the protocol-specific parser. var usage *TokenUsage if u, ok := cfg.ResponseParser(result); ok { usage = u } // Extract content / reasoning_content / tool_calls. content, reasonContent, toolCalls := extractContentAndChoices(result) if content == nil && len(toolCalls) == 0 { return nil, fmt.Errorf("no choices in response") } if usage != nil { recordResponseUsage(modelUsage, extractRequestID(result), usage, "chat") if chatConfig != nil { chatConfig.UsageResult = usage model, _ := result["model"].(string) common.Info("StreamUsage", zap.String("model", model), zap.Int("prompt", usage.PromptTokens), zap.Int("completion", usage.CompletionTokens), zap.Int("total", usage.TotalTokens)) } } return &ChatResponse{ Answer: content, ReasonContent: reasonContent, ToolCalls: toolCalls, Usage: usage, }, nil } // HandleStreamingResponse processes a streaming chat response using the // ParserConfig's StreamParser to extract usage from each event. func HandleStreamingResponse( body io.Reader, modelUsage *common.ModelUsage, chatConfig *ChatConfig, cfg *ParserConfig, sender func(*string, *string) error, ) error { if sender == nil { return fmt.Errorf("sender is required") } var streamUsage *TokenUsage accumulatedToolCalls := make(map[int]map[string]any) sawTerminal := false var streamModel string done, err := ParseSSEStream[map[string]any](body, func(event map[string]any) error { if u, ok := cfg.StreamParser(event); ok { streamUsage = u } if m, ok := event["model"].(string); ok { streamModel = m } if apiErr, ok := event["error"]; ok && apiErr != nil { return fmt.Errorf("upstream stream error: %v", apiErr) } choices, ok := event["choices"].([]any) if !ok || len(choices) == 0 { return nil } firstChoice, ok := choices[0].(map[string]any) if !ok { return nil } delta, ok := firstChoice["delta"].(map[string]any) if !ok { return nil } accumulateToolCallDeltas(delta, accumulatedToolCalls) if reasoningContent, ok := delta["reasoning_content"].(string); ok && reasoningContent != "" { if err := sender(nil, &reasoningContent); err != nil { return err } } if content, ok := delta["content"].(string); ok && content != "" { if err := sender(&content, nil); err != nil { return err } } if finishReason, ok := firstChoice["finish_reason"].(string); ok && finishReason != "" { sawTerminal = true } return nil }) if err != nil { return fmt.Errorf("failed to scan response body: %w", err) } if chatConfig != nil { setSortedToolCallsResult(chatConfig, accumulatedToolCalls) } if !done && !sawTerminal { return fmt.Errorf("stream ended before [DONE] or finish_reason") } if streamUsage != nil { recordResponseUsage(modelUsage, "", streamUsage, "chat") if chatConfig != nil { chatConfig.UsageResult = streamUsage common.Info("StreamUsage", zap.String("model", streamModel), zap.Int("prompt", streamUsage.PromptTokens), zap.Int("completion", streamUsage.CompletionTokens), zap.Int("total", streamUsage.TotalTokens)) } } endOfStream := "[DONE]" return sender(&endOfStream, nil) } // extractContentAndChoices extracts content, reasoning_content, and tool_calls // from a parsed non-streaming response. func extractContentAndChoices(result map[string]any) (*string, *string, []map[string]any) { choices, ok := result["choices"].([]any) if !ok || len(choices) == 0 { return nil, nil, nil } firstChoice, ok := choices[0].(map[string]any) if !ok { return nil, nil, nil } messageMap, ok := firstChoice["message"].(map[string]any) if !ok { return nil, nil, nil } var content *string if c, ok := messageMap["content"].(string); ok { cc := c content = &cc } else if _, ok := messageMap["tool_calls"].([]any); ok { // content may be nil when the response only carries tool calls. // Return an empty-string pointer so callers can rely on a non-nil Answer. empty := "" content = &empty } else if rc, ok := messageMap["reasoning_content"].(string); ok && rc != "" { // content may be nil when the response only carries reasoning_content. empty := "" content = &empty } var reasonContent *string if rc, ok := messageMap["reasoning_content"].(string); ok { reason := rc if reason != "" && reason[0] == '\n' { reason = reason[1:] } reasonContent = &reason } else { // Always return a non-nil pointer so callers can rely on it. empty := "" reasonContent = &empty } // Some providers (e.g. SiliconFlow) embed thinking inline via tags. // Extract reasoning into ReasonContent and strip it from Answer. if content != nil { if reasoning, answer := extractThinkContent(content); reasoning != nil { rc := *reasoning reasonContent = &rc a := *answer content = &a } } var toolCalls []map[string]any if tcs, ok := messageMap["tool_calls"].([]any); ok { for _, tc := range tcs { if tcMap, ok := tc.(map[string]any); ok { toolCalls = append(toolCalls, tcMap) } } } return content, reasonContent, toolCalls } // extractRequestID extracts the request ID from a parsed response. func extractRequestID(result map[string]any) string { if id, ok := result["id"].(string); ok { return id } return "" }