fix(Go): rewrite chat, listmodels, embed for Ollama (#15213)

### What problem does this PR solve?

IDK how to implement **`Ollama`** on #14580 but it's totally wrong.
This is the rewrite version for **`Ollama`**

**Verified from CLI**
```
# Embed
RAGFlow(user)> embed text 'what is rag' 'who are you' with 'nomic-embed-text:latest@test12@ollama' dimension 1024;
+-----------+-------+
| dimension | index |
+-----------+-------+
| 768       | 0     |
| 768       | 1     |
+-----------+-------+

# Chat
RAGFlow(user)> think chat with 'qwen3:0.6b@test12@ollama' message 'who r u'
Thinking: Okay, the user asked, "Who r u?" I need to respond appropriately. First, I should acknowledge their question. Since I'm an AI, I don't have a physical form, but I can confirm that I'm a large language model. I should keep the response friendly and offer help. Let me make sure I'm not making up any information and that the response is natural. Also, I should check for any typos and ensure clarity. Alright, that should cover it.

Answer: I'm an AI language model, and I don't have a physical form. However, I can tell you that I'm designed to assist with questions and tasks. How can I help you today?
Time: 2.914285


RAGFlow(user)> stream think chat with 'qwen3:0.6b@test12@ollama' message 'who r u'
Thinking: , the user asked, "Who are you?" I need to respond appropriately. Since I'm an AI assistant, I should mention that I don't have a physical form or a mind. I should also clarify that I can help with various tasks like answering questions or providing information. It's important to keep the response friendly and informative while maintaining the correct tone.
Answer:  don't have a physical form or a mind, but I'm here to help with your questions or tasks! What can I do for you today?
Time: 1.740047

# LisyModels
RAGFlow(user)> list supported models from 'ollama' 'test12'
+-------------------------+
| model_name              |
+-------------------------+
| nomic-embed-text:latest |
| qwen3:0.6b              |
+-------------------------+
```

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
This commit is contained in:
Haruko386
2026-05-25 18:55:03 +08:00
committed by GitHub
parent 0f92353bd9
commit 4783ce9951
3 changed files with 79 additions and 108 deletions

View File

@@ -1,9 +1,9 @@
{
"name": "ollama",
"url_suffix": {
"chat": "chat/completions",
"models": "models",
"embedding": "embeddings"
"chat": "api/chat",
"models": "api/ps",
"embedding": "api/embed"
},
"class": "local"
}

View File

@@ -4,7 +4,9 @@
"chat": "v1/chat/completions",
"embedding": "v1/embeddings",
"models": "v1/models",
"rerank": "v1/rerank"
"rerank": "v1/rerank",
"asr": "v1/audio/transcriptions",
"tts": "v1/audio/speech"
},
"class": "local"
}

View File

@@ -8,7 +8,6 @@ import (
"fmt"
"io"
"net/http"
"ragflow/internal/common"
"strings"
"time"
)
@@ -78,9 +77,15 @@ func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, api
// Convert messages to API format
apiMessages := make([]map[string]interface{}, len(messages))
for i, msg := range messages {
arr, _ := msg.Content.([]interface{})
first, _ := arr[0].(map[string]interface{})
text, _ := first["text"].(string)
apiMessages[i] = map[string]interface{}{
"role": msg.Role,
"content": msg.Content,
"content": text,
}
}
@@ -113,15 +118,13 @@ func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, api
reqBody["stop"] = *chatModelConfig.Stop
}
if chatModelConfig.Thinking != nil {
if chatModelConfig.Effort != nil && *chatModelConfig.Effort != "" {
if strings.HasPrefix(strings.ToLower(modelName), "gpt-oss") {
reqBody["think"] = *chatModelConfig.Effort
}
} else if chatModelConfig.Thinking != nil {
if *chatModelConfig.Thinking {
reqBody["thinking"] = map[string]interface{}{
"type": "enabled",
}
} else {
reqBody["thinking"] = map[string]interface{}{
"type": "disabled",
}
reqBody["think"] = true
}
}
}
@@ -137,7 +140,6 @@ func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, api
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
resp, err := o.httpClient.Do(req)
if err != nil {
@@ -160,35 +162,19 @@ func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, api
return nil, fmt.Errorf("failed to parse response: %w", err)
}
choices, ok := result["choices"].([]interface{})
if !ok || len(choices) == 0 {
return nil, fmt.Errorf("no choices in response")
}
firstChoice, ok := choices[0].(map[string]interface{})
message, ok := result["message"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid choice format")
return nil, fmt.Errorf("failed to parse response: message not found")
}
messageMap, ok := firstChoice["message"].(map[string]interface{})
content, ok := message["content"].(string)
if !ok {
return nil, fmt.Errorf("invalid message format")
return nil, fmt.Errorf("failed to parse response: content not found")
}
content, ok := messageMap["content"].(string)
reasonContent, ok := message["thinking"].(string)
if !ok {
return nil, fmt.Errorf("invalid content format")
}
var reasonContent string
if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking {
reasonContent, ok = messageMap["reasoning_content"].(string)
if !ok {
return nil, fmt.Errorf("invalid content format")
}
if reasonContent != "" && reasonContent[0] == '\n' {
reasonContent = reasonContent[1:]
}
return nil, fmt.Errorf("failed to parse response: thinking not found")
}
chatResponse := &ChatResponse{
@@ -218,9 +204,15 @@ func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Messag
// Convert messages to API format (supporting multimodal content)
apiMessages := make([]map[string]interface{}, len(messages))
for i, msg := range messages {
arr, _ := msg.Content.([]interface{})
first, _ := arr[0].(map[string]interface{})
text, _ := first["text"].(string)
apiMessages[i] = map[string]interface{}{
"role": msg.Role,
"content": msg.Content,
"content": text,
}
}
@@ -255,15 +247,13 @@ func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Messag
reqBody["stop"] = *modelConfig.Stop
}
if modelConfig.Thinking != nil {
if modelConfig.Effort != nil && *modelConfig.Effort != "" {
if strings.HasPrefix(strings.ToLower(modelName), "gpt-oss") {
reqBody["think"] = *modelConfig.Effort
}
} else if modelConfig.Thinking != nil {
if *modelConfig.Thinking {
reqBody["thinking"] = map[string]interface{}{
"type": "enabled",
}
} else {
reqBody["thinking"] = map[string]interface{}{
"type": "disabled",
}
reqBody["think"] = true
}
}
@@ -278,7 +268,6 @@ func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Messag
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
resp, err := o.httpClient.Do(req)
if err != nil {
@@ -294,66 +283,40 @@ func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Messag
// SSE parsing: read line by line
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
common.Info(line)
line := strings.TrimSpace(scanner.Text())
// SSE data line starts with "data:"
if !strings.HasPrefix(line, "data:") {
// ignore the blank
if line == "" {
continue
}
// Extract JSON after "data:"
data := strings.TrimSpace(line[5:])
// [DONE] marks the end of stream
if data == "[DONE]" {
break
}
// Parse the JSON event
var event map[string]interface{}
if err = json.Unmarshal([]byte(data), &event); err != nil {
if err = json.Unmarshal([]byte(line), &event); err != nil {
continue
}
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
continue
}
firstChoice, ok := choices[0].(map[string]interface{})
if !ok {
continue
}
delta, ok := firstChoice["delta"].(map[string]interface{})
if !ok {
continue
}
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
return err
if messageMap, ok := event["message"].(map[string]interface{}); ok {
if reasoningContent, exists := messageMap["thinking"].(string); exists && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
return err
}
}
if content, exists := messageMap["content"].(string); exists && content != "" {
if err := sender(&content, nil); err != nil {
return err
}
}
}
content, ok := delta["content"].(string)
if ok && content != "" {
if err := sender(&content, nil); err != nil {
return err
}
}
finishReason, ok := firstChoice["finish_reason"].(string)
if ok && finishReason != "" {
if done, ok := event["done"].(bool); ok && done {
break
}
}
// Send [DONE] marker for OpenAI compatibility
// Send [DONE] marker for OpenAI compatibility with RAGFlow frontend
endOfStream := "[DONE]"
if err = sender(&endOfStream, nil); err != nil {
if err := sender(&endOfStream, nil); err != nil {
return err
}
@@ -425,17 +388,30 @@ func (o *OllamaModel) Embed(modelName *string, texts []string, apiConfig *APICon
return nil, fmt.Errorf("Ollama embeddings API error: %s, body: %s", resp.Status, string(body))
}
var parsed openaiEmbeddingResponse
if err = json.Unmarshal(body, &parsed); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
var embedResp struct {
Model string `json:"model"`
Embeddings [][]float64 `json:"embeddings"`
}
var embeddings []EmbeddingData
for _, dataElem := range parsed.Data {
var embeddingData EmbeddingData
embeddingData.Embedding = dataElem.Embedding
embeddingData.Index = dataElem.Index
embeddings = append(embeddings, embeddingData)
if err = json.Unmarshal(body, &embedResp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
if len(embedResp.Embeddings) == 0 {
return nil, fmt.Errorf("no embeddings returned")
}
embeddings := make([]EmbeddingData, 0, len(embedResp.Embeddings))
for i, emb := range embedResp.Embeddings {
if len(emb) == 0 {
return nil, fmt.Errorf("empty embedding at index %d", i)
}
embeddings = append(embeddings, EmbeddingData{
Embedding: emb,
Index: i,
})
}
return embeddings, nil
@@ -489,7 +465,6 @@ func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]string, error) {
}
url := fmt.Sprintf("%s/%s", baseURL, o.URLSuffix.Models)
reqBody := map[string]interface{}{}
jsonData, err := json.Marshal(reqBody)
@@ -503,12 +478,6 @@ func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]string, error) {
}
req.Header.Set("Content-Type", "application/json")
// Ollama is a local provider and the API key is optional. Only set
// the Authorization header when a non-empty key was supplied. This
// also avoids a nil-pointer dereference on apiConfig or ApiKey.
if apiConfig != nil && apiConfig.ApiKey != nil && *apiConfig.ApiKey != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
}
resp, err := o.httpClient.Do(req)
if err != nil {
@@ -533,9 +502,9 @@ func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]string, error) {
// convert result["data"] to []map[string]interface{}
models := make([]string, 0)
for _, model := range result["data"].([]interface{}) {
for _, model := range result["models"].([]interface{}) {
modelMap := model.(map[string]interface{})
modelName := modelMap["id"].(string)
modelName := modelMap["name"].(string)
models = append(models, modelName)
}