Go: aliyun model provider (#14379)

### What problem does this PR solve?

As title.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
Jin Hai
2026-04-27 14:53:33 +08:00
committed by GitHub
parent 0b46ab07c5
commit c3eac4103a
9 changed files with 474 additions and 20 deletions

31
conf/models/aliyun.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "Aliyun",
"url": {
"default": "https://dashscope.aliyuncs.com",
"singapore": "https://dashscope-intl.aliyuncs.com",
"us": "https://dashscope-us.aliyuncs.com"
},
"url_suffix": {
"chat": "compatible-mode/v1/chat/completions",
"embedding": "compatible-mode/v1/embeddings",
"models": "api/v1/deployments/models"
},
"series": "deepseek",
"models": [
{
"name": "qwen-flash",
"max_tokens": 995904,
"model_types": [
"chat"
]
}
],
"features": {
"thinking": {
"default_value": true,
"supported_models": [
"qwen-flash"
]
}
}
}

View File

@@ -159,7 +159,7 @@ type Model struct {
MaxTokens int `json:"max_tokens"`
ModelTypes []string `json:"model_types"`
Thinking *ModelThinking `json:"thinking"`
Series *string `json:"series"`
Type *string `json:"type"`
ModelTypeMap map[string]bool
}
@@ -170,7 +170,7 @@ type Provider struct {
URLSuffix models.URLSuffix `json:"url_suffix"`
Models []*Model `json:"models"`
Features Features `json:"features"`
Series string `json:"series"`
Type string `json:"type"`
ModelDriver models.ModelDriver
}
@@ -257,12 +257,12 @@ func NewProviderManager(dirPath string) (*ProviderManager, error) {
}
}
if provider.Series == "" {
if provider.Type == "" {
pos := strings.Index(model.Name, "-")
modelSeries := model.Name[0:pos]
model.Series = &modelSeries
modelType := model.Name[0:pos]
model.Type = &modelType
} else {
model.Series = &provider.Name
model.Type = &provider.Name
}
model.ModelTypeMap = make(map[string]bool)

View File

@@ -0,0 +1,421 @@
//
// 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 (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"ragflow/internal/logger"
"strings"
"time"
)
// AliyunModel implements ModelDriver for Aliyun
type AliyunModel struct {
BaseURL map[string]string
URLSuffix URLSuffix
httpClient *http.Client // Reusable HTTP client with connection pool
}
// NewAliyunModel creates a new Aliyun model instance
func NewAliyunModel(baseURL map[string]string, urlSuffix URLSuffix) *AliyunModel {
return &AliyunModel{
BaseURL: baseURL,
URLSuffix: urlSuffix,
httpClient: &http.Client{
Timeout: 120 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
DisableCompression: false,
},
},
}
}
func (z *AliyunModel) Name() string {
return "siliconflow"
}
// Chat sends a message and returns response
func (z *AliyunModel) Chat(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) {
if message == nil {
return nil, fmt.Errorf("message is nil")
}
var region = "default"
if apiConfig.Region != nil {
region = *apiConfig.Region
}
url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat)
// Build request body
reqBody := map[string]interface{}{
"model": modelName,
"messages": []map[string]string{
{"role": "user", "content": *message},
},
"stream": false,
"temperature": 1,
}
if chatModelConfig.Stream != nil {
reqBody["stream"] = *chatModelConfig.Stream
}
if chatModelConfig.MaxTokens != nil {
reqBody["max_tokens"] = *chatModelConfig.MaxTokens
}
if chatModelConfig.Temperature != nil {
reqBody["temperature"] = *chatModelConfig.Temperature
}
if chatModelConfig.TopP != nil {
reqBody["top_p"] = *chatModelConfig.TopP
}
if chatModelConfig.Stop != nil {
reqBody["stop"] = *chatModelConfig.Stop
}
if chatModelConfig.Thinking != nil {
if *chatModelConfig.Thinking {
reqBody["enable_thinking"] = true
} else {
reqBody["enable_thinking"] = false
}
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
resp, err := z.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
// Parse response
var result map[string]interface{}
if err = json.Unmarshal(body, &result); err != nil {
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{})
if !ok {
return nil, fmt.Errorf("invalid choice format")
}
messageMap, ok := firstChoice["message"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid message format")
}
answer, ok := messageMap["content"].(string)
if !ok {
return nil, fmt.Errorf("invalid content format")
}
var reasonContent string
if chatModelConfig.Thinking != nil && *chatModelConfig.Thinking {
reasonContent, ok = messageMap["reasoning_content"].(string)
if !ok {
return nil, fmt.Errorf("invalid content format")
}
// if first char of reasonContent is \n remove the '\n'
if reasonContent != "" && reasonContent[0] == '\n' {
reasonContent = reasonContent[1:]
}
}
//thinking, answer := GetThinkingAndAnswer(chatModelConfig.ModelType, &content)
chatResponse := &ChatResponse{
Answer: &answer,
ReasonContent: &reasonContent,
}
return chatResponse, nil
}
// ChatWithMessages sends multiple messages with roles and returns response
func (z *AliyunModel) ChatWithMessages(modelName string, apiKey *string, messages []Message, chatModelConfig *ChatConfig) (string, error) {
return "", fmt.Errorf("%s, ChatWithMessages not implemented", z.Name())
}
// ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel)
func (z *AliyunModel) ChatStreamlyWithSender(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error {
var region = "default"
if apiConfig.Region != nil {
region = *apiConfig.Region
}
url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat)
// Build request body with streaming enabled
reqBody := map[string]interface{}{
"model": modelName,
"messages": []map[string]string{
{"role": "user", "content": *message},
},
"stream": false,
"temperature": 1,
}
if chatModelConfig.Stream != nil {
reqBody["stream"] = *chatModelConfig.Stream
}
if chatModelConfig.MaxTokens != nil {
reqBody["max_tokens"] = *chatModelConfig.MaxTokens
}
if chatModelConfig.Temperature != nil {
reqBody["temperature"] = *chatModelConfig.Temperature
}
if chatModelConfig.DoSample != nil {
reqBody["do_sample"] = *chatModelConfig.DoSample
}
if chatModelConfig.TopP != nil {
reqBody["top_p"] = *chatModelConfig.TopP
}
if chatModelConfig.Stop != nil {
reqBody["stop"] = *chatModelConfig.Stop
}
if chatModelConfig.Thinking != nil {
if *chatModelConfig.Thinking {
reqBody["enable_thinking"] = true
} else {
reqBody["enable_thinking"] = false
}
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
resp, err := z.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
// SSE parsing: read line by line
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
logger.Info(line)
// 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 stream
if data == "[DONE]" {
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 {
continue
}
firstChoice, ok := choices[0].(map[string]interface{})
if !ok {
continue
}
delta, ok := firstChoice["delta"].(map[string]interface{})
if !ok {
continue
}
content, ok := delta["content"].(string)
if ok && content != "" {
if err := sender(&content, nil); err != nil {
return err
}
}
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
return err
}
}
finishReason, ok := firstChoice["finish_reason"].(string)
if ok && finishReason != "" {
break
}
}
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"
if err = sender(&endOfStream, nil); err != nil {
return err
}
return scanner.Err()
}
// EncodeToEmbedding encodes a list of texts into embeddings
func (z *AliyunModel) EncodeToEmbedding(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([][]float64, error) {
return nil, fmt.Errorf("%s, no such method", z.Name())
}
type AliyunModelItem struct {
ModelName string `json:"model_name"`
BaseCapacity int `json:"base_capacity"`
}
type AliyunModelOutput struct {
Models []AliyunModelItem `json:"models"`
PageNo int `json:"page_no"`
PageSize int `json:"page_size"`
Total int `json:"total"`
}
type AliyunModelList struct {
RequestID string `json:"request_id"`
Output AliyunModelOutput `json:"output"`
}
func (z *AliyunModel) ListModels(apiConfig *APIConfig) ([]string, error) {
var region = "default"
if apiConfig.Region != nil {
region = *apiConfig.Region
}
url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Models)
// Build request body
reqBody := map[string]interface{}{}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
resp, err := z.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
// Parse response
var modelList AliyunModelList
if err = json.Unmarshal(body, &modelList); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
var models []string
for _, model := range modelList.Output.Models {
modelName := model.ModelName
models = append(models, modelName)
}
return models, nil
}
func (z *AliyunModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) {
return nil, fmt.Errorf("%s, no such method", z.Name())
}
func (z *AliyunModel) CheckConnection(apiConfig *APIConfig) error {
_, err := z.ListModels(apiConfig)
if err != nil {
return err
}
return nil
}

View File

@@ -18,8 +18,8 @@ package models
import "strings"
func GetThinkingAndAnswer(modelSeries *string, content *string) (*string, *string) {
switch *modelSeries {
func GetThinkingAndAnswer(modelType *string, content *string) (*string, *string) {
switch *modelType {
case "qwen3":
return extractThinkContent(content)
}

View File

@@ -45,6 +45,8 @@ func (f *ModelFactory) CreateModelDriver(providerName string, baseURL map[string
return NewGiteeModel(baseURL, urlSuffix), nil
case "siliconflow":
return NewSiliconflowModel(baseURL, urlSuffix), nil
case "aliyun":
return NewAliyunModel(baseURL, urlSuffix), nil
default:
return NewDummyModel(baseURL, urlSuffix), nil
}

View File

@@ -69,10 +69,10 @@ func (z *GiteeModel) Chat(modelName, message *string, apiConfig *APIConfig, chat
url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat)
// I need to get the model series, such as qwen3 is the prefix, the model series will be qwen. glm is the prefix, the model series will be glm. such as the model name: qwen3-0.6b, the model series will be qwen3
// the model name is glm-4.7, the model series will be glm
modelSeries := strings.Split(*modelName, "-")[0]
if modelSeries == "qwen" || modelSeries == "glm" {
// I need to get the model type, such as qwen3 is the prefix, the model type will be qwen. glm is the prefix, the model type will be glm. such as the model name: qwen3-0.6b, the model type will be qwen3
// the model name is glm-4.7, the model type will be glm
modelType := strings.Split(*modelName, "-")[0]
if modelType == "qwen" || modelType == "glm" {
url = fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.AsyncChat)
}
@@ -172,7 +172,7 @@ func (z *GiteeModel) Chat(modelName, message *string, apiConfig *APIConfig, chat
return nil, fmt.Errorf("invalid content format")
}
thinking, answer := GetThinkingAndAnswer(chatModelConfig.ModelSeries, &content)
thinking, answer := GetThinkingAndAnswer(chatModelConfig.ModelType, &content)
chatResponse := &ChatResponse{
Answer: answer,

View File

@@ -69,10 +69,10 @@ func (z *SiliconflowModel) Chat(modelName, message *string, apiConfig *APIConfig
url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat)
// I need to get the model series, such as qwen3 is the prefix, the model series will be qwen. glm is the prefix, the model series will be glm. such as the model name: qwen3-0.6b, the model series will be qwen3
// the model name is glm-4.7, the model series will be glm
modelSeries := strings.Split(*modelName, "-")[0]
if modelSeries == "qwen" || modelSeries == "glm" {
// I need to get the model type, such as qwen3 is the prefix, the model type will be qwen. glm is the prefix, the model type will be glm. such as the model name: qwen3-0.6b, the model type will be qwen3
// the model name is glm-4.7, the model type will be glm
modelType := strings.Split(*modelName, "-")[0]
if modelType == "qwen" || modelType == "glm" {
url = fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.AsyncChat)
}
@@ -172,7 +172,7 @@ func (z *SiliconflowModel) Chat(modelName, message *string, apiConfig *APIConfig
return nil, fmt.Errorf("invalid content format")
}
thinking, answer := GetThinkingAndAnswer(chatModelConfig.ModelSeries, &content)
thinking, answer := GetThinkingAndAnswer(chatModelConfig.ModelType, &content)
chatResponse := &ChatResponse{
Answer: answer,

View File

@@ -52,7 +52,7 @@ type ChatConfig struct {
TopP *float64
DoSample *bool
Stop *[]string
ModelSeries *string
ModelType *string
Effort *string
Verbosity *string
}

View File

@@ -776,7 +776,7 @@ func (m *ModelProviderService) ChatToModel(providerName, instanceName, modelName
return nil, common.CodeNotFound, errors.New(fmt.Sprintf("provider %s model %s not found", providerName, modelName))
}
modelConfig.ModelSeries = model.Series
modelConfig.ModelType = model.Type
var extra map[string]string
err = json.Unmarshal([]byte(instance.Extra), &extra)