mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-04 23:00:30 +08:00
Implement OpenAI chat completions in GO (#16177)
### What problem does this PR solve? Implement OpenAI chat completions in GO POST /api/v1/openai/<chat_id>/chat/completions OpenAI chat cli: internal/development.md ### Type of change - [x] Refactoring
This commit is contained in:
351
internal/entity/models/chat_tools.go
Normal file
351
internal/entity/models/chat_tools.go
Normal file
@@ -0,0 +1,351 @@
|
||||
//
|
||||
// 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"ragflow/internal/tokenizer"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMaxRetries = 3
|
||||
defaultMaxRounds = 5
|
||||
)
|
||||
|
||||
// ChatWithTools runs the non-streaming tool-calling loop.
|
||||
func (cm *ChatModel) ChatWithTools(ctx context.Context, system string, history []Message, chatCfg *ChatConfig) (string, int, error) {
|
||||
tc := cm.ToolConfig
|
||||
if tc == nil {
|
||||
return "", 0, fmt.Errorf("ChatWithTools called without bound tools")
|
||||
}
|
||||
|
||||
var toolsList interface{}
|
||||
if err := json.Unmarshal([]byte(tc.Tools), &toolsList); err != nil {
|
||||
return "", 0, fmt.Errorf("failed to parse tools JSON: %w", err)
|
||||
}
|
||||
|
||||
maxRounds := tc.MaxRounds
|
||||
if maxRounds <= 0 {
|
||||
maxRounds = defaultMaxRounds
|
||||
}
|
||||
maxRetries := tc.MaxRetries
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = defaultMaxRetries
|
||||
}
|
||||
|
||||
if system != "" && len(history) > 0 && history[0].Role != "system" {
|
||||
history = append([]Message{{Role: "system", Content: system}}, history...)
|
||||
}
|
||||
|
||||
baseHistory := make([]Message, len(history))
|
||||
copy(baseHistory, history)
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", 0, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
h := make([]Message, len(baseHistory))
|
||||
copy(h, baseHistory)
|
||||
|
||||
answer, tokens, err := runToolLoop(ctx, cm, h, toolsList, chatCfg, maxRounds)
|
||||
if err == nil {
|
||||
return answer, tokens, nil
|
||||
}
|
||||
}
|
||||
return "", 0, fmt.Errorf("ChatWithTools failed after %d retries", maxRetries)
|
||||
}
|
||||
|
||||
func runToolLoop(ctx context.Context, cm *ChatModel, history []Message, toolsList interface{}, chatCfg *ChatConfig, maxRounds int) (string, int, error) {
|
||||
var totalTokens int
|
||||
|
||||
for round := 0; round <= maxRounds; round++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", totalTokens, ctx.Err()
|
||||
default:
|
||||
}
|
||||
cfg := *chatCfg
|
||||
cfg.Tools = toolsList
|
||||
tcChoice := "auto"
|
||||
cfg.ToolChoice = &tcChoice
|
||||
|
||||
resp, err := cm.ModelDriver.ChatWithMessages(*cm.ModelName, history, cm.APIConfig, &cfg)
|
||||
if err != nil {
|
||||
return "", totalTokens, fmt.Errorf("round %d: %w", round, err)
|
||||
}
|
||||
if resp == nil {
|
||||
return "", totalTokens, fmt.Errorf("round %d: nil response", round)
|
||||
}
|
||||
|
||||
if len(resp.ToolCalls) == 0 {
|
||||
answer := ""
|
||||
if resp.Answer != nil {
|
||||
answer = *resp.Answer
|
||||
}
|
||||
if resp.ReasonContent != nil && *resp.ReasonContent != "" {
|
||||
answer = "<think>" + *resp.ReasonContent + "</think>" + answer
|
||||
}
|
||||
totalTokens += tokenizer.NumTokensFromString(answer)
|
||||
return answer, totalTokens, nil
|
||||
}
|
||||
|
||||
history = appendToolResults(history, resp.ToolCalls, cm.ToolConfig.ToolCallSession)
|
||||
}
|
||||
|
||||
// Exceeded max rounds
|
||||
history = append(history, Message{
|
||||
Role: "user",
|
||||
Content: fmt.Sprintf("Exceed max rounds: %d", maxRounds),
|
||||
})
|
||||
cfg := *chatCfg
|
||||
resp, err := cm.ModelDriver.ChatWithMessages(*cm.ModelName, history, cm.APIConfig, &cfg)
|
||||
if err != nil {
|
||||
return "", totalTokens, fmt.Errorf("final call: %w", err)
|
||||
}
|
||||
if resp == nil || resp.Answer == nil {
|
||||
return "", totalTokens, fmt.Errorf("final call: no answer")
|
||||
}
|
||||
totalTokens += tokenizer.NumTokensFromString(*resp.Answer)
|
||||
return *resp.Answer, totalTokens, nil
|
||||
}
|
||||
|
||||
// ChatStreamlyWithTools runs the streaming tool-calling loop.
|
||||
func (cm *ChatModel) ChatStreamlyWithTools(ctx context.Context, system string, history []Message, chatCfg *ChatConfig, sender func(*string, *string) error) (int, error) {
|
||||
tc := cm.ToolConfig
|
||||
if tc == nil {
|
||||
return 0, fmt.Errorf("ChatStreamlyWithTools called without bound tools")
|
||||
}
|
||||
|
||||
var toolsList interface{}
|
||||
if err := json.Unmarshal([]byte(tc.Tools), &toolsList); err != nil {
|
||||
return 0, fmt.Errorf("failed to parse tools JSON: %w", err)
|
||||
}
|
||||
|
||||
maxRounds := tc.MaxRounds
|
||||
if maxRounds <= 0 {
|
||||
maxRounds = defaultMaxRounds
|
||||
}
|
||||
maxRetries := tc.MaxRetries
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = defaultMaxRetries
|
||||
}
|
||||
|
||||
if system != "" && len(history) > 0 && history[0].Role != "system" {
|
||||
history = append([]Message{{Role: "system", Content: system}}, history...)
|
||||
}
|
||||
|
||||
baseHistory := make([]Message, len(history))
|
||||
copy(baseHistory, history)
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
h := make([]Message, len(baseHistory))
|
||||
copy(h, baseHistory)
|
||||
|
||||
totalTokens, err := runStreamToolLoop(ctx, cm, h, toolsList, chatCfg, maxRounds, sender)
|
||||
if err == nil {
|
||||
return totalTokens, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ChatStreamlyWithTools failed after %d retries", maxRetries)
|
||||
}
|
||||
|
||||
func runStreamToolLoop(ctx context.Context, cm *ChatModel, history []Message, toolsList interface{}, chatCfg *ChatConfig, maxRounds int, sender func(*string, *string) error) (int, error) {
|
||||
var totalTokens int
|
||||
|
||||
for round := 0; round <= maxRounds; round++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return totalTokens, ctx.Err()
|
||||
default:
|
||||
}
|
||||
cfg := *chatCfg
|
||||
cfg.Tools = toolsList
|
||||
tcChoice := "auto"
|
||||
cfg.ToolChoice = &tcChoice
|
||||
cfg.Stream = boolPtr(true)
|
||||
var tcs []map[string]interface{}
|
||||
cfg.ToolCallsResult = &tcs
|
||||
|
||||
reasoningStarted := false
|
||||
var answer string
|
||||
var pendingThinkClose bool
|
||||
|
||||
err := cm.ModelDriver.ChatStreamlyWithSender(*cm.ModelName, history, cm.APIConfig, &cfg, func(delta *string, reason *string) error {
|
||||
if reason != nil && *reason != "" {
|
||||
if !reasoningStarted {
|
||||
reasoningStarted = true
|
||||
thinkOpen := "<think>"
|
||||
if e := sender(&thinkOpen, nil); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
pendingThinkClose = true
|
||||
return sender(reason, nil)
|
||||
}
|
||||
// Reasoning ended, close the think block if open
|
||||
if pendingThinkClose {
|
||||
pendingThinkClose = false
|
||||
thinkClose := "</think>"
|
||||
if e := sender(&thinkClose, nil); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
if delta != nil && *delta != "" {
|
||||
if *delta == "[DONE]" {
|
||||
return nil
|
||||
}
|
||||
totalTokens += tokenizer.NumTokensFromString(*delta)
|
||||
answer += *delta
|
||||
if e := sender(delta, nil); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
// Close any unclosed think block after stream completes
|
||||
if pendingThinkClose {
|
||||
pendingThinkClose = false
|
||||
thinkClose := "</think>"
|
||||
if e := sender(&thinkClose, nil); e != nil {
|
||||
return totalTokens, e
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return totalTokens, fmt.Errorf("round %d: %w", round, err)
|
||||
}
|
||||
|
||||
var toolCalls []map[string]interface{}
|
||||
if cfg.ToolCallsResult != nil {
|
||||
toolCalls = *cfg.ToolCallsResult
|
||||
}
|
||||
|
||||
if answer != "" && len(toolCalls) == 0 {
|
||||
return totalTokens, nil
|
||||
}
|
||||
if len(toolCalls) == 0 {
|
||||
return totalTokens, fmt.Errorf("round %d: no content and no tool_calls", round)
|
||||
}
|
||||
|
||||
history = appendToolResults(history, toolCalls, cm.ToolConfig.ToolCallSession)
|
||||
}
|
||||
|
||||
// Exceeded max rounds
|
||||
history = append(history, Message{
|
||||
Role: "user",
|
||||
Content: fmt.Sprintf("Exceed max rounds: %d", maxRounds),
|
||||
})
|
||||
cfg := *chatCfg
|
||||
cfg.Stream = boolPtr(true)
|
||||
return totalTokens, cm.ModelDriver.ChatStreamlyWithSender(*cm.ModelName, history, cm.APIConfig, &cfg, sender)
|
||||
}
|
||||
|
||||
// appendToolResults executes tool calls concurrently, appends the assistant
|
||||
// message with tool_calls and individual tool result messages to history.
|
||||
func appendToolResults(history []Message, toolCalls []map[string]interface{}, session ToolCallSession) []Message {
|
||||
if session == nil {
|
||||
history = append(history, Message{
|
||||
Role: "assistant",
|
||||
Content: nil,
|
||||
ToolCalls: toolCalls,
|
||||
})
|
||||
for _, tc := range toolCalls {
|
||||
tcID, _ := tc["id"].(string)
|
||||
history = append(history, Message{
|
||||
Role: "tool",
|
||||
Content: "Error: no tool session configured",
|
||||
ToolCallID: tcID,
|
||||
})
|
||||
}
|
||||
return history
|
||||
}
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
type toolResult struct {
|
||||
index int
|
||||
tcID string
|
||||
content string
|
||||
}
|
||||
results := make([]toolResult, len(toolCalls))
|
||||
|
||||
for i, tc := range toolCalls {
|
||||
wg.Add(1)
|
||||
go func(idx int, tcMap map[string]interface{}) {
|
||||
defer wg.Done()
|
||||
var result toolResult
|
||||
result.index = idx
|
||||
fn, ok := tcMap["function"].(map[string]interface{})
|
||||
if !ok {
|
||||
mu.Lock()
|
||||
results[idx] = result
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
name, _ := fn["name"].(string)
|
||||
argsStr, _ := fn["arguments"].(string)
|
||||
result.tcID, _ = tcMap["id"].(string)
|
||||
|
||||
var args map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(argsStr), &args); err != nil {
|
||||
args = map[string]interface{}{"raw_arguments": argsStr}
|
||||
}
|
||||
|
||||
res, err := session.ToolCall(name, args)
|
||||
if err != nil {
|
||||
result.content = fmt.Sprintf("Error: %s", err.Error())
|
||||
} else {
|
||||
result.content = res
|
||||
}
|
||||
mu.Lock()
|
||||
results[idx] = result
|
||||
mu.Unlock()
|
||||
}(i, tc)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
history = append(history, Message{
|
||||
Role: "assistant",
|
||||
Content: nil,
|
||||
ToolCalls: toolCalls,
|
||||
})
|
||||
|
||||
for _, r := range results {
|
||||
history = append(history, Message{
|
||||
Role: "tool",
|
||||
Content: r.content,
|
||||
ToolCallID: r.tcID,
|
||||
})
|
||||
}
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
@@ -73,21 +73,27 @@ func (o *OpenAIModel) ChatWithMessages(modelName string, messages []Message, api
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
url := fmt.Sprintf("%s/%s", baseURL, o.baseModel.URLSuffix.Chat)
|
||||
|
||||
// Convert messages to the format expected by the API
|
||||
// Convert messages to API format (supports multimodal content)
|
||||
apiMessages := make([]map[string]interface{}, len(messages))
|
||||
for i, msg := range messages {
|
||||
apiMessages[i] = map[string]interface{}{
|
||||
apiMsg := map[string]interface{}{
|
||||
"role": msg.Role,
|
||||
"content": msg.Content,
|
||||
}
|
||||
if msg.ToolCallID != "" {
|
||||
apiMsg["tool_call_id"] = msg.ToolCallID
|
||||
}
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
apiMsg["tool_calls"] = msg.ToolCalls
|
||||
}
|
||||
apiMessages[i] = apiMsg
|
||||
}
|
||||
|
||||
// Build request body
|
||||
reqBody := map[string]interface{}{
|
||||
"model": modelName,
|
||||
"messages": apiMessages,
|
||||
"stream": false,
|
||||
"temperature": 1,
|
||||
"model": modelName,
|
||||
"messages": apiMessages,
|
||||
"stream": false,
|
||||
}
|
||||
|
||||
if chatModelConfig != nil {
|
||||
@@ -106,6 +112,21 @@ func (o *OpenAIModel) ChatWithMessages(modelName string, messages []Message, api
|
||||
if chatModelConfig.Stop != nil {
|
||||
reqBody["stop"] = *chatModelConfig.Stop
|
||||
}
|
||||
|
||||
if chatModelConfig.Tools != nil {
|
||||
reqBody["tools"] = chatModelConfig.Tools
|
||||
tc := "auto"
|
||||
if chatModelConfig.ToolChoice != nil {
|
||||
tc = *chatModelConfig.ToolChoice
|
||||
}
|
||||
reqBody["tool_choice"] = tc
|
||||
}
|
||||
}
|
||||
|
||||
// Qwen3 family: disable thinking by default (matches Python's
|
||||
// _apply_model_family_policies in rag/llm/chat_model.py:119-121).
|
||||
if strings.Contains(strings.ToLower(modelName), "qwen3") && (chatModelConfig == nil || chatModelConfig.Thinking == nil) {
|
||||
reqBody["enable_thinking"] = false
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
@@ -160,9 +181,9 @@ func (o *OpenAIModel) ChatWithMessages(modelName string, messages []Message, api
|
||||
return nil, fmt.Errorf("invalid message format")
|
||||
}
|
||||
|
||||
content, ok := messageMap["content"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid content format")
|
||||
var content string
|
||||
if c, ok := messageMap["content"].(string); ok {
|
||||
content = c
|
||||
}
|
||||
|
||||
// OpenAI reasoning models (o-series and similar) return reasoning text in
|
||||
@@ -175,9 +196,19 @@ func (o *OpenAIModel) ChatWithMessages(modelName string, messages []Message, api
|
||||
}
|
||||
}
|
||||
|
||||
var toolCalls []map[string]interface{}
|
||||
if tcs, ok := messageMap["tool_calls"].([]interface{}); ok {
|
||||
for _, tc := range tcs {
|
||||
if tcMap, ok := tc.(map[string]interface{}); ok {
|
||||
toolCalls = append(toolCalls, tcMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chatResponse := &ChatResponse{
|
||||
Answer: &content,
|
||||
ReasonContent: &reasonContent,
|
||||
ToolCalls: toolCalls,
|
||||
}
|
||||
|
||||
return chatResponse, nil
|
||||
@@ -200,13 +231,20 @@ func (o *OpenAIModel) ChatStreamlyWithSender(modelName string, messages []Messag
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
url := fmt.Sprintf("%s/%s", baseURL, o.baseModel.URLSuffix.Chat)
|
||||
|
||||
// Convert messages to API format (supports multimodal content)
|
||||
// Convert messages to API format (supports multimodal content and tool messages)
|
||||
apiMessages := make([]map[string]interface{}, len(messages))
|
||||
for i, msg := range messages {
|
||||
apiMessages[i] = map[string]interface{}{
|
||||
apiMsg := map[string]interface{}{
|
||||
"role": msg.Role,
|
||||
"content": msg.Content,
|
||||
}
|
||||
if msg.ToolCallID != "" {
|
||||
apiMsg["tool_call_id"] = msg.ToolCallID
|
||||
}
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
apiMsg["tool_calls"] = msg.ToolCalls
|
||||
}
|
||||
apiMessages[i] = apiMsg
|
||||
}
|
||||
|
||||
// Build request body with streaming on by default
|
||||
@@ -236,6 +274,20 @@ func (o *OpenAIModel) ChatStreamlyWithSender(modelName string, messages []Messag
|
||||
if chatModelConfig.Stop != nil {
|
||||
reqBody["stop"] = *chatModelConfig.Stop
|
||||
}
|
||||
|
||||
if chatModelConfig.Tools != nil {
|
||||
reqBody["tools"] = chatModelConfig.Tools
|
||||
tc := "auto"
|
||||
if chatModelConfig.ToolChoice != nil {
|
||||
tc = *chatModelConfig.ToolChoice
|
||||
}
|
||||
reqBody["tool_choice"] = tc
|
||||
}
|
||||
}
|
||||
|
||||
// Qwen3 family: disable thinking by default.
|
||||
if strings.Contains(strings.ToLower(modelName), "qwen3") && (chatModelConfig == nil || chatModelConfig.Thinking == nil) {
|
||||
reqBody["enable_thinking"] = false
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
@@ -263,20 +315,75 @@ func (o *OpenAIModel) ChatStreamlyWithSender(modelName string, messages []Messag
|
||||
}
|
||||
|
||||
sawTerminal := false
|
||||
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
|
||||
accumulatedToolCalls := make(map[int]map[string]interface{})
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// SSE data line starts with "data:"
|
||||
if !strings.HasPrefix(line, "data:") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract JSON after "data:"
|
||||
data := strings.TrimSpace(line[5:])
|
||||
|
||||
// [DONE] marks the end of the stream
|
||||
if data == "[DONE]" {
|
||||
sawTerminal = true
|
||||
break
|
||||
}
|
||||
|
||||
// Parse the JSON event
|
||||
var event map[string]interface{}
|
||||
if err = json.Unmarshal([]byte(data), &event); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
choices, ok := event["choices"].([]interface{})
|
||||
if !ok || len(choices) == 0 {
|
||||
return nil
|
||||
continue
|
||||
}
|
||||
|
||||
firstChoice, ok := choices[0].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
continue
|
||||
}
|
||||
|
||||
delta, ok := firstChoice["delta"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
continue
|
||||
}
|
||||
|
||||
// Accumulate streaming tool_call deltas (mirrors Python's
|
||||
// async_chat_streamly_with_tools in rag/llm/chat_model.py:500-509).
|
||||
if tcs, ok := delta["tool_calls"].([]interface{}); ok {
|
||||
for _, tc := range tcs {
|
||||
if tcMap, ok := tc.(map[string]interface{}); ok {
|
||||
idxF, ok := tcMap["index"].(float64)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
idx := int(idxF)
|
||||
existing, hasExisting := accumulatedToolCalls[idx]
|
||||
if hasExisting {
|
||||
if fn, ok := tcMap["function"].(map[string]interface{}); ok {
|
||||
if args, ok := fn["arguments"].(string); ok {
|
||||
if ef, ok := existing["function"].(map[string]interface{}); ok {
|
||||
if ea, ok := ef["arguments"].(string); ok {
|
||||
ef["arguments"] = ea + args
|
||||
} else {
|
||||
ef["arguments"] = args
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
accumulatedToolCalls[idx] = cloneMap(tcMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue // tool_call deltas don't carry content
|
||||
}
|
||||
|
||||
reasoningContent, ok := delta["reasoning_content"].(string)
|
||||
@@ -297,15 +404,23 @@ func (o *OpenAIModel) ChatStreamlyWithSender(modelName string, messages []Messag
|
||||
if ok && finishReason != "" {
|
||||
sawTerminal = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("failed to scan response body: %w", err)
|
||||
}
|
||||
if !done && !sawTerminal {
|
||||
if !sawTerminal {
|
||||
return fmt.Errorf("openai: stream ended before [DONE] or finish_reason")
|
||||
}
|
||||
|
||||
// Populate ToolCallsResult with accumulated streaming tool_calls.
|
||||
if len(accumulatedToolCalls) > 0 && chatModelConfig != nil {
|
||||
tcs := make([]map[string]interface{}, 0, len(accumulatedToolCalls))
|
||||
for _, tc := range accumulatedToolCalls {
|
||||
tcs = append(tcs, tc)
|
||||
}
|
||||
chatModelConfig.ToolCallsResult = &tcs
|
||||
}
|
||||
|
||||
// Send the [DONE] marker for OpenAI compatibility
|
||||
endOfStream := "[DONE]"
|
||||
if err := sender(&endOfStream, nil); err != nil {
|
||||
@@ -907,3 +1022,11 @@ func (o *OpenAIModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error)
|
||||
func (o *OpenAIModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) {
|
||||
return nil, fmt.Errorf("%s, no such method", o.Name())
|
||||
}
|
||||
|
||||
func cloneMap(m map[string]interface{}) map[string]interface{} {
|
||||
cp := make(map[string]interface{}, len(m))
|
||||
for k, v := range m {
|
||||
cp[k] = v
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
@@ -93,10 +93,9 @@ func (s *SiliconflowModel) ChatWithMessages(modelName string, messages []Message
|
||||
|
||||
// Build request body
|
||||
reqBody := map[string]interface{}{
|
||||
"model": modelName,
|
||||
"messages": apiMessages,
|
||||
"stream": false,
|
||||
"temperature": 1,
|
||||
"model": modelName,
|
||||
"messages": apiMessages,
|
||||
"stream": false,
|
||||
}
|
||||
|
||||
if chatModelConfig != nil {
|
||||
@@ -119,18 +118,12 @@ func (s *SiliconflowModel) ChatWithMessages(modelName string, messages []Message
|
||||
if chatModelConfig.Stop != nil {
|
||||
reqBody["stop"] = *chatModelConfig.Stop
|
||||
}
|
||||
}
|
||||
|
||||
if chatModelConfig.Thinking != nil {
|
||||
if *chatModelConfig.Thinking {
|
||||
reqBody["thinking"] = map[string]interface{}{
|
||||
"type": "enabled",
|
||||
}
|
||||
} else {
|
||||
reqBody["thinking"] = map[string]interface{}{
|
||||
"type": "disabled",
|
||||
}
|
||||
}
|
||||
}
|
||||
// Qwen3 family: disable thinking by default (matches Python's
|
||||
// _apply_model_family_policies in rag/llm/chat_model.py:119-121).
|
||||
if strings.Contains(strings.ToLower(modelName), "qwen3") && (chatModelConfig == nil || chatModelConfig.Thinking == nil) {
|
||||
reqBody["enable_thinking"] = false
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
@@ -243,10 +236,9 @@ func (s *SiliconflowModel) ChatStreamlyWithSender(modelName string, messages []M
|
||||
|
||||
// Build request body with streaming enabled
|
||||
reqBody := map[string]interface{}{
|
||||
"model": modelName,
|
||||
"messages": apiMessages,
|
||||
"stream": true,
|
||||
"temperature": 1,
|
||||
"model": modelName,
|
||||
"messages": apiMessages,
|
||||
"stream": true,
|
||||
}
|
||||
|
||||
if chatModelConfig != nil {
|
||||
@@ -273,18 +265,12 @@ func (s *SiliconflowModel) ChatStreamlyWithSender(modelName string, messages []M
|
||||
if chatModelConfig.Stop != nil {
|
||||
reqBody["stop"] = *chatModelConfig.Stop
|
||||
}
|
||||
}
|
||||
|
||||
if chatModelConfig.Thinking != nil {
|
||||
if *chatModelConfig.Thinking {
|
||||
reqBody["thinking"] = map[string]interface{}{
|
||||
"type": "enabled",
|
||||
}
|
||||
} else {
|
||||
reqBody["thinking"] = map[string]interface{}{
|
||||
"type": "disabled",
|
||||
}
|
||||
}
|
||||
}
|
||||
// Qwen3 family: disable thinking by default (matches Python's
|
||||
// _apply_model_family_policies in rag/llm/chat_model.py:119-121).
|
||||
if strings.Contains(strings.ToLower(modelName), "qwen3") && (chatModelConfig == nil || chatModelConfig.Thinking == nil) {
|
||||
reqBody["enable_thinking"] = false
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package models
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// Message represents a chat message with role and content
|
||||
//
|
||||
// Content is interface{} to support different formats:
|
||||
@@ -7,8 +9,15 @@ package models
|
||||
// - []interface{}: multimodal content array where each element is map[string]interface{}
|
||||
// (e.g., [{"type": "text", "text": "..."}, {"type": "image_url", "image_url": {"url": "..."}}])
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content interface{} `json:"content"`
|
||||
Role string `json:"role"`
|
||||
Content interface{} `json:"content"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolCalls []map[string]interface{} `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
// ToolCallSession mirrors Python's common.mcp_tool_call_conn.ToolCallSession protocol.
|
||||
type ToolCallSession interface {
|
||||
ToolCall(name string, arguments map[string]interface{}) (string, error)
|
||||
}
|
||||
|
||||
// EmbeddingModel interface for embedding models
|
||||
@@ -48,8 +57,9 @@ type ModelDriver interface {
|
||||
}
|
||||
|
||||
type ChatResponse struct {
|
||||
Answer *string `json:"answer"`
|
||||
ReasonContent *string `json:"reason_content"`
|
||||
Answer *string `json:"answer"`
|
||||
ReasonContent *string `json:"reason_content"`
|
||||
ToolCalls []map[string]interface{} `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
type EmbeddingData struct {
|
||||
@@ -130,17 +140,20 @@ type URLSuffix struct {
|
||||
}
|
||||
|
||||
type ChatConfig struct {
|
||||
Stream *bool
|
||||
Vision *bool
|
||||
Thinking *bool
|
||||
MaxTokens *int
|
||||
Temperature *float64
|
||||
TopP *float64
|
||||
DoSample *bool
|
||||
Stop *[]string
|
||||
ModelClass *string
|
||||
Effort *string
|
||||
Verbosity *string
|
||||
Stream *bool
|
||||
Vision *bool
|
||||
Thinking *bool
|
||||
MaxTokens *int
|
||||
Temperature *float64
|
||||
TopP *float64
|
||||
DoSample *bool
|
||||
Stop *[]string
|
||||
ModelClass *string
|
||||
Effort *string
|
||||
Verbosity *string
|
||||
Tools interface{} `json:"tools,omitempty"`
|
||||
ToolChoice *string `json:"tool_choice,omitempty"`
|
||||
ToolCallsResult *[]map[string]interface{} `json:"-"`
|
||||
}
|
||||
|
||||
type APIConfig struct {
|
||||
@@ -211,11 +224,20 @@ func (r *RerankModel) Rerank(query string, texts []string, apiConfig *APIConfig,
|
||||
return r.ModelDriver.Rerank(r.ModelName, query, texts, apiConfig, rerankConfig)
|
||||
}
|
||||
|
||||
// ToolConfig bundles tool-calling configuration for a ChatModel.
|
||||
type ToolConfig struct {
|
||||
Tools string // JSON-encoded tools list
|
||||
MaxRounds int // max tool-calling rounds (default: 5)
|
||||
MaxRetries int // max retries on failure (default: 3)
|
||||
ToolCallSession ToolCallSession // session that executes tool calls
|
||||
}
|
||||
|
||||
// ChatModel wraps a ModelDriver with chat-specific configuration
|
||||
type ChatModel struct {
|
||||
ModelDriver ModelDriver
|
||||
ModelName *string
|
||||
APIConfig *APIConfig
|
||||
ToolConfig *ToolConfig
|
||||
}
|
||||
|
||||
// NewChatModel creates a new ChatModel
|
||||
@@ -226,3 +248,26 @@ func NewChatModel(driver ModelDriver, modelName *string, apiConfig *APIConfig) *
|
||||
APIConfig: apiConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// BindTools registers tools for the ChatModel to call.
|
||||
// Mirrors Python's Base.bind_tools() in rag/llm/chat_model.py.
|
||||
func (cm *ChatModel) BindTools(session ToolCallSession, tools interface{}) {
|
||||
// Serialize tools to JSON if it's a list/map.
|
||||
toolsJSON := ""
|
||||
switch v := tools.(type) {
|
||||
case string:
|
||||
toolsJSON = v
|
||||
case []byte:
|
||||
toolsJSON = string(v)
|
||||
default:
|
||||
if b, err := json.Marshal(tools); err == nil {
|
||||
toolsJSON = string(b)
|
||||
}
|
||||
}
|
||||
cm.ToolConfig = &ToolConfig{
|
||||
Tools: toolsJSON,
|
||||
MaxRounds: defaultMaxRounds,
|
||||
MaxRetries: defaultMaxRetries,
|
||||
ToolCallSession: session,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user