2026-04-24 20:59:30 +08:00
|
|
|
//
|
|
|
|
|
// 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"
|
2026-06-02 03:27:26 -04:00
|
|
|
"context"
|
2026-04-24 20:59:30 +08:00
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2026-05-15 14:03:33 +08:00
|
|
|
"mime/multipart"
|
2026-04-24 20:59:30 +08:00
|
|
|
"net/http"
|
2026-05-15 14:03:33 +08:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2026-05-06 10:41:58 +08:00
|
|
|
"ragflow/internal/common"
|
2026-05-08 06:01:10 +02:00
|
|
|
"strconv"
|
2026-04-24 20:59:30 +08:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// SiliconflowModel implements ModelDriver for Siliconflow
|
|
|
|
|
type SiliconflowModel struct {
|
2026-06-04 17:50:22 +08:00
|
|
|
baseModel BaseModel
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewSiliconflowModel creates a new Siliconflow model instance
|
|
|
|
|
func NewSiliconflowModel(baseURL map[string]string, urlSuffix URLSuffix) *SiliconflowModel {
|
|
|
|
|
return &SiliconflowModel{
|
2026-06-04 17:50:22 +08:00
|
|
|
baseModel: BaseModel{
|
|
|
|
|
BaseURL: baseURL,
|
|
|
|
|
URLSuffix: urlSuffix,
|
|
|
|
|
httpClient: &http.Client{
|
|
|
|
|
Transport: &http.Transport{
|
|
|
|
|
MaxIdleConns: 100,
|
|
|
|
|
MaxIdleConnsPerHost: 10,
|
|
|
|
|
IdleConnTimeout: 90 * time.Second,
|
|
|
|
|
DisableCompression: false,
|
|
|
|
|
},
|
2026-04-24 20:59:30 +08:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) NewInstance(baseURL map[string]string) ModelDriver {
|
2026-06-04 17:50:22 +08:00
|
|
|
return NewSiliconflowModel(baseURL, s.baseModel.URLSuffix)
|
2026-04-29 17:05:08 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) Name() string {
|
2026-04-24 20:59:30 +08:00
|
|
|
return "siliconflow"
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 12:59:01 +08:00
|
|
|
// SiliconflowRerankRequest represents SILICONFLOW rerank request
|
|
|
|
|
type SiliconflowRerankRequest struct {
|
|
|
|
|
Model string `json:"model"`
|
|
|
|
|
Query string `json:"query"`
|
|
|
|
|
Documents []string `json:"documents"`
|
|
|
|
|
TopN int `json:"top_n"`
|
|
|
|
|
ReturnDocuments bool `json:"return_documents"`
|
|
|
|
|
MaxChunksPerDoc int `json:"max_chunks_per_doc"`
|
|
|
|
|
OverlapTokens int `json:"overlap_tokens"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 15:25:01 +08:00
|
|
|
// ChatWithMessages sends multiple messages with roles and returns response
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) {
|
2026-06-04 17:50:22 +08:00
|
|
|
if err := s.baseModel.APIConfigCheck(apiConfig); err != nil {
|
|
|
|
|
return nil, err
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-30 15:25:01 +08:00
|
|
|
if len(messages) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("messages is empty")
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resolvedBaseURL, err := s.baseModel.GetBaseURL(apiConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
2026-06-04 17:50:22 +08:00
|
|
|
url := fmt.Sprintf("%s/%s", resolvedBaseURL, s.baseModel.URLSuffix.Chat)
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 15:25:01 +08:00
|
|
|
// Convert messages to the format expected by API
|
|
|
|
|
apiMessages := make([]map[string]interface{}, len(messages))
|
|
|
|
|
for i, msg := range messages {
|
|
|
|
|
apiMessages[i] = map[string]interface{}{
|
|
|
|
|
"role": msg.Role,
|
|
|
|
|
"content": msg.Content,
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build request body
|
|
|
|
|
reqBody := map[string]interface{}{
|
2026-04-30 15:25:01 +08:00
|
|
|
"model": modelName,
|
|
|
|
|
"messages": apiMessages,
|
2026-04-24 20:59:30 +08:00
|
|
|
"stream": false,
|
|
|
|
|
"temperature": 1,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 15:25:01 +08:00
|
|
|
if chatModelConfig != nil {
|
|
|
|
|
if chatModelConfig.Stream != nil {
|
|
|
|
|
reqBody["stream"] = *chatModelConfig.Stream
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 15:25:01 +08:00
|
|
|
if chatModelConfig.MaxTokens != nil {
|
|
|
|
|
reqBody["max_tokens"] = *chatModelConfig.MaxTokens
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 15:25:01 +08:00
|
|
|
if chatModelConfig.Temperature != nil {
|
|
|
|
|
reqBody["temperature"] = *chatModelConfig.Temperature
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 15:25:01 +08:00
|
|
|
if chatModelConfig.TopP != nil {
|
|
|
|
|
reqBody["top_p"] = *chatModelConfig.TopP
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 15:25:01 +08:00
|
|
|
if chatModelConfig.Stop != nil {
|
|
|
|
|
reqBody["stop"] = *chatModelConfig.Stop
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
2026-06-08 02:32:52 -03:00
|
|
|
|
|
|
|
|
if chatModelConfig.Thinking != nil {
|
|
|
|
|
if *chatModelConfig.Thinking {
|
|
|
|
|
reqBody["thinking"] = map[string]interface{}{
|
|
|
|
|
"type": "enabled",
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
reqBody["thinking"] = map[string]interface{}{
|
|
|
|
|
"type": "disabled",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonData, err := json.Marshal(reqBody)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 03:27:26 -04:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
2026-04-24 20:59:30 +08:00
|
|
|
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))
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resp, err := s.baseModel.httpClient.Do(req)
|
2026-04-24 20:59:30 +08:00
|
|
|
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{}
|
2026-04-30 15:25:01 +08:00
|
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
2026-04-24 20:59:30 +08:00
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
content, ok := messageMap["content"].(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("invalid content format")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 15:25:01 +08:00
|
|
|
var reasonContent string
|
|
|
|
|
if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking {
|
|
|
|
|
reasonContent, ok = messageMap["reasoning_content"].(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
// If reasoning_content not in response, try parsing from content tags
|
|
|
|
|
reasoning, answer := GetThinkingAndAnswer(chatModelConfig.ModelClass, &content)
|
|
|
|
|
if reasoning != nil {
|
|
|
|
|
reasonContent = *reasoning
|
|
|
|
|
content = *answer
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// if first char of reasonContent is \n remove the '\n'
|
|
|
|
|
if reasonContent != "" && reasonContent[0] == '\n' {
|
|
|
|
|
reasonContent = reasonContent[1:]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
|
|
|
|
chatResponse := &ChatResponse{
|
2026-04-30 15:25:01 +08:00
|
|
|
Answer: &content,
|
|
|
|
|
ReasonContent: &reasonContent,
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return chatResponse, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 19:33:57 +08:00
|
|
|
// ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel)
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error {
|
2026-06-04 17:50:22 +08:00
|
|
|
if err := s.baseModel.APIConfigCheck(apiConfig); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 19:33:57 +08:00
|
|
|
if len(messages) == 0 {
|
|
|
|
|
return fmt.Errorf("messages is empty")
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resolvedBaseURL, err := s.baseModel.GetBaseURL(apiConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
2026-06-04 17:50:22 +08:00
|
|
|
url := fmt.Sprintf("%s/%s", resolvedBaseURL, s.baseModel.URLSuffix.Chat)
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 19:33:57 +08:00
|
|
|
// Convert messages to API format
|
|
|
|
|
apiMessages := make([]map[string]interface{}, len(messages))
|
|
|
|
|
for i, msg := range messages {
|
|
|
|
|
apiMessages[i] = map[string]interface{}{
|
|
|
|
|
"role": msg.Role,
|
|
|
|
|
"content": msg.Content,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 20:59:30 +08:00
|
|
|
// Build request body with streaming enabled
|
|
|
|
|
reqBody := map[string]interface{}{
|
2026-04-30 19:33:57 +08:00
|
|
|
"model": modelName,
|
|
|
|
|
"messages": apiMessages,
|
|
|
|
|
"stream": true,
|
2026-04-24 20:59:30 +08:00
|
|
|
"temperature": 1,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 19:33:57 +08:00
|
|
|
if chatModelConfig != nil {
|
|
|
|
|
if chatModelConfig.Stream != nil {
|
|
|
|
|
reqBody["stream"] = *chatModelConfig.Stream
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 19:33:57 +08:00
|
|
|
if chatModelConfig.MaxTokens != nil {
|
|
|
|
|
reqBody["max_tokens"] = *chatModelConfig.MaxTokens
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 19:33:57 +08:00
|
|
|
if chatModelConfig.Temperature != nil {
|
|
|
|
|
reqBody["temperature"] = *chatModelConfig.Temperature
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 19:33:57 +08:00
|
|
|
if chatModelConfig.DoSample != nil {
|
|
|
|
|
reqBody["do_sample"] = *chatModelConfig.DoSample
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 19:33:57 +08:00
|
|
|
if chatModelConfig.TopP != nil {
|
|
|
|
|
reqBody["top_p"] = *chatModelConfig.TopP
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 19:33:57 +08:00
|
|
|
if chatModelConfig.Stop != nil {
|
|
|
|
|
reqBody["stop"] = *chatModelConfig.Stop
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 19:33:57 +08:00
|
|
|
if chatModelConfig.Thinking != nil {
|
|
|
|
|
if *chatModelConfig.Thinking {
|
|
|
|
|
reqBody["thinking"] = map[string]interface{}{
|
|
|
|
|
"type": "enabled",
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
reqBody["thinking"] = map[string]interface{}{
|
|
|
|
|
"type": "disabled",
|
|
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonData, err := json.Marshal(reqBody)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to marshal request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 03:27:26 -04:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
2026-04-24 20:59:30 +08:00
|
|
|
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))
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resp, err := s.baseModel.httpClient.Do(req)
|
2026-04-24 20:59:30 +08:00
|
|
|
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)
|
fix(go-models): raise SSE scanner buffer so large stream chunks are not dropped (#15382)
### Summary
Closes #15381
Every provider in `internal/entity/models/` reads its streaming response
with `bufio.NewScanner(resp.Body)` and iterates over `scanner.Scan()`.
The default `bufio.Scanner` maximum token size is 64KB, so when an
upstream sends a single SSE `data:` line larger than 64KB (long content
deltas, large tool or function call argument blobs, bundled
`reasoning_content`, or providers that emit a whole message in one
event) `scanner.Scan()` returns `false` and `scanner.Err()` returns
`bufio.ErrTooLong`. Streaming chat then ends with an error partway
through the response.
This change adds `scanner.Buffer(make([]byte, 64*1024), 1024*1024)`
immediately after every SSE scanner that was still bare, raising the cap
to 1MB. 1MB is the value already used for streaming chat in `openai.go`,
`modelscope.go`, `groq.go`, `mistral.go`, `xai.go` and the other already
patched providers (the 8MB cap in the repo is reserved for TTS and
embedding paths), so this simply converges the remaining providers onto
the established pattern. Nothing else changes: line parsing, `data:`
prefix handling, `[DONE]` detection, JSON unmarshalling, error handling,
and the existing `scanner.Err()` checks all stay the same.
Providers covered (23 scanners across 22 files): 302ai, aliyun,
baichuan, baidu, cohere, deepinfra, deepseek, gitee, huggingface,
lmstudio, minimax (the chat scanner, whose TTS scanner was already
bumped), moonshot, nvidia, ollama, openrouter, orcarouter, paddleocr,
siliconflow, tokenhub, vllm, volcengine, xunfei, zhipu-ai. `jiekouai.go`
is excluded because it is covered by the in flight #15337.
A table driven regression test (`sse_scanner_buffer_test.go`) streams a
single 128KB `data:` content delta followed by `data: [DONE]` through an
`httptest` server and asserts that `ChatStreamlyWithSender` delivers the
full content with no error across a representative subset of providers.
Without the buffer fix the test fails with `bufio.Scanner: token too
long`.
This PR also removes three duplicate declarations of the package level
`roundTripperFunc` test helper that several recently merged provider PRs
each added independently, which had left the `internal/entity/models`
test package unable to compile. The helper now lives in a single place
and is shared.
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
2026-05-29 07:34:00 -04:00
|
|
|
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
|
2026-04-24 20:59:30 +08:00
|
|
|
for scanner.Scan() {
|
|
|
|
|
line := scanner.Text()
|
2026-05-06 10:41:58 +08:00
|
|
|
common.Info(line)
|
2026-04-24 20:59:30 +08:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 15:25:01 +08:00
|
|
|
reasoningContent, ok := delta["reasoning_content"].(string)
|
|
|
|
|
if ok && reasoningContent != "" {
|
|
|
|
|
if err := sender(nil, &reasoningContent); err != nil {
|
|
|
|
|
return err
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
2026-04-30 15:25:01 +08:00
|
|
|
}
|
2026-04-24 20:59:30 +08:00
|
|
|
|
2026-04-30 15:25:01 +08:00
|
|
|
content, ok := delta["content"].(string)
|
|
|
|
|
if ok && content != "" {
|
|
|
|
|
if err := sender(&content, nil); err != nil {
|
|
|
|
|
return err
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:55:27 -04:00
|
|
|
type siliconflowEmbeddingResponse struct {
|
2026-05-18 15:07:07 +08:00
|
|
|
Object string `json:"object"`
|
2026-05-11 14:45:30 +08:00
|
|
|
Model string `json:"model"`
|
|
|
|
|
Data []siliconflowEmbeddingData `json:"data"`
|
|
|
|
|
Usage siliconflowUsage `json:"usage"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type siliconflowEmbeddingData struct {
|
|
|
|
|
Object string `json:"object"`
|
|
|
|
|
Embedding []float64 `json:"embedding"`
|
|
|
|
|
Index int `json:"index"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type siliconflowUsage struct {
|
|
|
|
|
PromptTokens int `json:"prompt_tokens"`
|
|
|
|
|
CompletionTokens int `json:"completion_tokens"`
|
|
|
|
|
TotalTokens int `json:"total_tokens"`
|
2026-05-11 00:55:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// siliconflowMaxBatchSize is the per-request input limit documented at
|
|
|
|
|
const siliconflowMaxBatchSize = 32
|
|
|
|
|
|
2026-05-11 14:45:30 +08:00
|
|
|
// Embed embeds a list of texts into embeddings
|
|
|
|
|
func (s *SiliconflowModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) {
|
2026-06-04 17:50:22 +08:00
|
|
|
if err := s.baseModel.APIConfigCheck(apiConfig); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 12:59:01 +08:00
|
|
|
if len(texts) == 0 {
|
2026-05-11 14:45:30 +08:00
|
|
|
return []EmbeddingData{}, nil
|
|
|
|
|
}
|
|
|
|
|
if len(texts) > siliconflowMaxBatchSize {
|
|
|
|
|
return nil, fmt.Errorf("siliconflow supports a maximum of %d inputs per request", siliconflowMaxBatchSize)
|
2026-04-28 12:59:01 +08:00
|
|
|
}
|
2026-05-11 14:45:30 +08:00
|
|
|
|
2026-05-11 00:55:27 -04:00
|
|
|
if modelName == nil || *modelName == "" {
|
|
|
|
|
return nil, fmt.Errorf("model name is required")
|
|
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resolvedBaseURL, err := s.baseModel.GetBaseURL(apiConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-04-28 12:59:01 +08:00
|
|
|
}
|
2026-06-04 17:50:22 +08:00
|
|
|
url := fmt.Sprintf("%s/%s", strings.TrimSuffix(resolvedBaseURL, "/"), s.baseModel.URLSuffix.Embedding)
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
apiKey := *apiConfig.ApiKey
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-05-11 00:55:27 -04:00
|
|
|
reqBody := map[string]interface{}{
|
2026-05-11 14:45:30 +08:00
|
|
|
"model": modelName,
|
|
|
|
|
"input": texts,
|
2026-05-11 00:55:27 -04:00
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-05-11 00:55:27 -04:00
|
|
|
jsonData, err := json.Marshal(reqBody)
|
|
|
|
|
if err != nil {
|
2026-05-11 14:45:30 +08:00
|
|
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
2026-05-11 00:55:27 -04:00
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-06-02 03:27:26 -04:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
2026-05-11 00:55:27 -04:00
|
|
|
if err != nil {
|
2026-05-11 14:45:30 +08:00
|
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
2026-05-11 00:55:27 -04:00
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-05-11 00:55:27 -04:00
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
if apiKey != "" {
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
|
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resp, err := s.baseModel.httpClient.Do(req)
|
2026-05-11 00:55:27 -04:00
|
|
|
if err != nil {
|
2026-05-11 14:45:30 +08:00
|
|
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
2026-05-11 00:55:27 -04:00
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-05-11 00:55:27 -04:00
|
|
|
body, err := io.ReadAll(resp.Body)
|
2026-05-11 14:45:30 +08:00
|
|
|
resp.Body.Close()
|
|
|
|
|
|
2026-05-11 00:55:27 -04:00
|
|
|
if err != nil {
|
2026-05-11 14:45:30 +08:00
|
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
2026-05-11 00:55:27 -04:00
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-05-11 00:55:27 -04:00
|
|
|
if resp.StatusCode != http.StatusOK {
|
2026-05-11 14:45:30 +08:00
|
|
|
return nil, fmt.Errorf("SILICONFLOW API error: %s, body: %s", resp.Status, string(body))
|
2026-05-11 00:55:27 -04:00
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-05-11 14:45:30 +08:00
|
|
|
var parsed siliconflowEmbeddingResponse
|
|
|
|
|
if err = json.Unmarshal(body, &parsed); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
2026-05-11 00:55:27 -04:00
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-05-11 14:45:30 +08:00
|
|
|
var embeddings []EmbeddingData
|
|
|
|
|
for _, dataElem := range parsed.Data {
|
|
|
|
|
var embeddingData EmbeddingData
|
|
|
|
|
embeddingData.Embedding = dataElem.Embedding
|
|
|
|
|
embeddingData.Index = dataElem.Index
|
|
|
|
|
embeddings = append(embeddings, embeddingData)
|
2026-04-28 12:59:01 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 14:45:30 +08:00
|
|
|
return embeddings, nil
|
2026-04-28 12:59:01 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) ListModels(apiConfig *APIConfig) ([]string, error) {
|
2026-06-04 17:50:22 +08:00
|
|
|
if err := s.baseModel.APIConfigCheck(apiConfig); err != nil {
|
|
|
|
|
return nil, err
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resolvedBaseURL, err := s.baseModel.GetBaseURL(apiConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
url := fmt.Sprintf("%s/%s", resolvedBaseURL, s.baseModel.URLSuffix.Models)
|
2026-04-24 20:59:30 +08:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 03:27:26 -04:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData))
|
2026-04-24 20:59:30 +08:00
|
|
|
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))
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resp, err := s.baseModel.httpClient.Do(req)
|
2026-04-24 20:59:30 +08:00
|
|
|
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 DSModelList
|
|
|
|
|
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.Models {
|
|
|
|
|
modelName := model.ID
|
|
|
|
|
if model.OwnedBy != "" {
|
|
|
|
|
modelName = model.ID + "@" + model.OwnedBy
|
|
|
|
|
}
|
|
|
|
|
models = append(models, modelName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return models, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 06:01:10 +02:00
|
|
|
type siliconflowBalanceResponse struct {
|
|
|
|
|
Code int `json:"code"`
|
|
|
|
|
Status bool `json:"status"`
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
Data struct {
|
|
|
|
|
Balance string `json:"balance"`
|
|
|
|
|
TotalBalance string `json:"totalBalance"`
|
|
|
|
|
} `json:"data"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) {
|
2026-06-04 17:50:22 +08:00
|
|
|
if err := s.baseModel.APIConfigCheck(apiConfig); err != nil {
|
|
|
|
|
return nil, err
|
2026-05-08 06:01:10 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
baseURL, err := s.baseModel.GetBaseURL(apiConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-05-08 06:01:10 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), s.baseModel.URLSuffix.Balance)
|
2026-05-08 06:01:10 +02:00
|
|
|
|
2026-06-02 03:27:26 -04:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
2026-05-08 06:01:10 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resp, err := s.baseModel.httpClient.Do(req)
|
2026-05-08 06:01:10 +02:00
|
|
|
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("SiliconFlow balance API error: %s, body: %s", resp.Status, string(body))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var parsed siliconflowBalanceResponse
|
|
|
|
|
if err = json.Unmarshal(body, &parsed); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !parsed.Status {
|
|
|
|
|
msg := parsed.Message
|
|
|
|
|
if msg == "" {
|
|
|
|
|
msg = "unknown API error"
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("SiliconFlow API error (code %d): %s", parsed.Code, msg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
raw := parsed.Data.TotalBalance
|
|
|
|
|
if raw == "" {
|
|
|
|
|
raw = parsed.Data.Balance
|
|
|
|
|
}
|
|
|
|
|
if raw == "" {
|
|
|
|
|
return nil, fmt.Errorf("no balance info in response")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
total, err := strconv.ParseFloat(raw, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid balance %q: %w", raw, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return map[string]interface{}{
|
|
|
|
|
"balance": total,
|
|
|
|
|
"currency": "CNY",
|
|
|
|
|
}, nil
|
2026-04-24 20:59:30 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) CheckConnection(apiConfig *APIConfig) error {
|
|
|
|
|
_, err := s.ListModels(apiConfig)
|
2026-04-24 20:59:30 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-05-09 17:41:54 +08:00
|
|
|
// SiliconflowRerankResponse represents SILICONFLOW rerank response
|
|
|
|
|
type SiliconflowRerankResponse struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Results []struct {
|
|
|
|
|
Index int `json:"index"`
|
|
|
|
|
Document struct {
|
|
|
|
|
Text string `json:"text"`
|
|
|
|
|
} `json:"document"`
|
|
|
|
|
RelevanceScore float64 `json:"relevance_score"`
|
|
|
|
|
} `json:"results"`
|
|
|
|
|
Meta struct {
|
|
|
|
|
Tokens struct {
|
|
|
|
|
InputTokens int `json:"input_tokens"`
|
|
|
|
|
OutputTokens int `json:"output_tokens"`
|
|
|
|
|
ImageTokens int `json:"image_tokens"`
|
|
|
|
|
} `json:"tokens"`
|
|
|
|
|
BilledUnits struct {
|
|
|
|
|
InputTokens int `json:"input_tokens"`
|
|
|
|
|
OutputTokens int `json:"output_tokens"`
|
|
|
|
|
ImageTokens int `json:"image_tokens"`
|
|
|
|
|
SearchUnits int `json:"search_units"`
|
|
|
|
|
Classifications int `json:"classifications"`
|
|
|
|
|
} `json:"billed_units"`
|
|
|
|
|
} `json:"meta"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rerank calculates similarity scores between query and documents
|
|
|
|
|
func (s *SiliconflowModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) {
|
2026-06-04 17:50:22 +08:00
|
|
|
if err := s.baseModel.APIConfigCheck(apiConfig); err != nil {
|
|
|
|
|
return nil, err
|
2026-04-28 12:59:01 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
if len(documents) == 0 {
|
|
|
|
|
return &RerankResponse{}, nil
|
2026-04-28 12:59:01 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
apiKey := *apiConfig.ApiKey
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-05-09 20:45:53 +08:00
|
|
|
var topN = rerankConfig.TopN
|
|
|
|
|
if rerankConfig.TopN == 0 {
|
|
|
|
|
topN = len(documents)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 12:59:01 +08:00
|
|
|
reqBody := SiliconflowRerankRequest{
|
|
|
|
|
Model: *modelName,
|
|
|
|
|
Query: query,
|
2026-05-09 17:41:54 +08:00
|
|
|
Documents: documents,
|
2026-05-09 20:45:53 +08:00
|
|
|
TopN: topN,
|
2026-04-28 12:59:01 +08:00
|
|
|
ReturnDocuments: false,
|
|
|
|
|
MaxChunksPerDoc: 1024,
|
|
|
|
|
OverlapTokens: 80,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonData, err := json.Marshal(reqBody)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resolvedBaseURL, err := s.baseModel.GetBaseURL(apiConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
url := fmt.Sprintf("%s/%s", strings.TrimSuffix(resolvedBaseURL, "/"), s.baseModel.URLSuffix.Rerank)
|
2026-04-28 12:59:01 +08:00
|
|
|
|
2026-06-02 03:27:26 -04:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(jsonData)))
|
2026-04-28 12:59:01 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
if apiKey != "" {
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resp, err := s.baseModel.httpClient.Do(req)
|
2026-04-28 12:59:01 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
|
return nil, fmt.Errorf("SiliconFlow Rerank API error: %s, body: %s", resp.Status, string(body))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
|
|
2026-05-09 17:41:54 +08:00
|
|
|
var siliconflowRerankResp SiliconflowRerankResponse
|
|
|
|
|
if err = json.Unmarshal(body, &siliconflowRerankResp); err != nil {
|
2026-04-28 12:59:01 +08:00
|
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 17:41:54 +08:00
|
|
|
var rerankResponse RerankResponse
|
|
|
|
|
for _, result := range siliconflowRerankResp.Results {
|
|
|
|
|
rerankResponse.Data = append(rerankResponse.Data, RerankResult{
|
|
|
|
|
Index: result.Index,
|
|
|
|
|
RelevanceScore: result.RelevanceScore,
|
|
|
|
|
})
|
2026-04-28 12:59:01 +08:00
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
return &rerankResponse, nil
|
2026-04-28 12:59:01 +08:00
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
|
|
|
// TranscribeAudio transcribe audio
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) {
|
2026-06-04 17:50:22 +08:00
|
|
|
if err := s.baseModel.APIConfigCheck(apiConfig); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 14:03:33 +08:00
|
|
|
if file == nil || *file == "" {
|
|
|
|
|
return nil, fmt.Errorf("file is missing")
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resolvedBaseURL, err := s.baseModel.GetBaseURL(apiConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-05-15 14:03:33 +08:00
|
|
|
}
|
2026-06-04 17:50:22 +08:00
|
|
|
url := fmt.Sprintf("%s/%s", resolvedBaseURL, s.baseModel.URLSuffix.ASR)
|
2026-05-15 14:03:33 +08:00
|
|
|
|
|
|
|
|
// multipart body
|
|
|
|
|
var body bytes.Buffer
|
|
|
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
|
|
|
|
|
|
// open audio file
|
|
|
|
|
audioFile, err := os.Open(*file)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to open audio file: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer audioFile.Close()
|
|
|
|
|
|
|
|
|
|
// create multipart file field
|
|
|
|
|
part, err := writer.CreateFormFile(
|
|
|
|
|
"file",
|
|
|
|
|
filepath.Base(*file),
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create multipart file: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// copy file content
|
|
|
|
|
if _, err = io.Copy(part, audioFile); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to copy audio data: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// model field
|
|
|
|
|
if err := writer.WriteField("model", *modelName); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to write model field: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// extra params
|
|
|
|
|
if asrConfig != nil && asrConfig.Params != nil {
|
|
|
|
|
for key, value := range asrConfig.Params {
|
|
|
|
|
|
|
|
|
|
var val string
|
|
|
|
|
|
|
|
|
|
switch v := value.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
val = v
|
|
|
|
|
case bool:
|
|
|
|
|
val = strconv.FormatBool(v)
|
|
|
|
|
case int:
|
|
|
|
|
val = strconv.Itoa(v)
|
|
|
|
|
case int64:
|
|
|
|
|
val = strconv.FormatInt(v, 10)
|
|
|
|
|
case float32:
|
|
|
|
|
val = strconv.FormatFloat(float64(v), 'f', -1, 32)
|
|
|
|
|
case float64:
|
|
|
|
|
val = strconv.FormatFloat(v, 'f', -1, 64)
|
|
|
|
|
default:
|
|
|
|
|
val = fmt.Sprintf("%v", v)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err = writer.WriteField(key, val); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to write field %s: %w", key, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err = writer.Close(); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// build request
|
2026-06-02 03:27:26 -04:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, &body)
|
2026-05-15 14:03:33 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
|
|
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
|
|
|
|
|
|
// send request
|
2026-06-04 17:50:22 +08:00
|
|
|
resp, err := s.baseModel.httpClient.Do(req)
|
2026-05-15 14:03:33 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
return nil, fmt.Errorf("SiliconFlow ASR error: %s - %s", resp.Status, string(respBody))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SiliconFlow response
|
|
|
|
|
var result struct {
|
|
|
|
|
Text string `json:"text"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err = json.Unmarshal(respBody, &result); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to unmarshal response: %w, body=%s", err, string(respBody))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &ASRResponse{Text: result.Text}, nil
|
2026-05-12 17:17:44 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error {
|
|
|
|
|
return fmt.Errorf("%s, no such method", s.Name())
|
2026-05-12 17:17:44 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 18:41:43 +08:00
|
|
|
// AudioSpeech convert text to audio
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) {
|
2026-06-04 17:50:22 +08:00
|
|
|
if err := s.baseModel.APIConfigCheck(apiConfig); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 14:03:33 +08:00
|
|
|
if audioContent == nil || *audioContent == "" {
|
|
|
|
|
return nil, fmt.Errorf("audio content is empty")
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resolvedBaseURL, err := s.baseModel.GetBaseURL(apiConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-05-15 14:03:33 +08:00
|
|
|
}
|
2026-06-04 17:50:22 +08:00
|
|
|
url := fmt.Sprintf("%s/%s", resolvedBaseURL, s.baseModel.URLSuffix.TTS)
|
2026-05-15 14:03:33 +08:00
|
|
|
|
|
|
|
|
reqBody := map[string]interface{}{
|
|
|
|
|
"model": *modelName,
|
|
|
|
|
"input": *audioContent,
|
|
|
|
|
"stream": false,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 18:41:43 +08:00
|
|
|
if ttsConfig != nil && ttsConfig.Params != nil {
|
|
|
|
|
for key, value := range ttsConfig.Params {
|
2026-05-15 14:03:33 +08:00
|
|
|
reqBody[key] = value
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-15 18:41:43 +08:00
|
|
|
if ttsConfig != nil && ttsConfig.Format != "" {
|
|
|
|
|
reqBody["response_format"] = ttsConfig.Format
|
2026-05-15 14:03:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonData, err := json.Marshal(reqBody)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 03:27:26 -04:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
2026-05-15 14:03:33 +08:00
|
|
|
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))
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resp, err := s.baseModel.httpClient.Do(req)
|
2026-05-15 14:03:33 +08:00
|
|
|
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 body: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
return nil, fmt.Errorf("%s - %s", resp.Status, string(body))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &TTSResponse{Audio: body}, nil
|
2026-05-12 17:17:44 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error {
|
2026-06-04 17:50:22 +08:00
|
|
|
if err := s.baseModel.APIConfigCheck(apiConfig); err != nil {
|
|
|
|
|
return err
|
2026-05-15 14:03:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if audioContent == nil || *audioContent == "" {
|
|
|
|
|
return fmt.Errorf("audio content is empty")
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resolvedBaseURL, err := s.baseModel.GetBaseURL(apiConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2026-05-15 14:03:33 +08:00
|
|
|
}
|
2026-06-04 17:50:22 +08:00
|
|
|
url := fmt.Sprintf("%s/%s", resolvedBaseURL, s.baseModel.URLSuffix.TTS)
|
2026-05-15 14:03:33 +08:00
|
|
|
|
|
|
|
|
reqBody := map[string]interface{}{
|
|
|
|
|
"model": *modelName,
|
|
|
|
|
"input": *audioContent,
|
|
|
|
|
"stream": true,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ttsConfig != nil && ttsConfig.Params != nil {
|
|
|
|
|
for key, value := range ttsConfig.Params {
|
|
|
|
|
reqBody[key] = value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ttsConfig != nil && ttsConfig.Format != "" {
|
|
|
|
|
reqBody["response_format"] = ttsConfig.Format
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonData, err := json.Marshal(reqBody)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to marshal request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 03:27:26 -04:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
2026-05-15 14:03:33 +08:00
|
|
|
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))
|
|
|
|
|
|
2026-06-04 17:50:22 +08:00
|
|
|
resp, err := s.baseModel.httpClient.Do(req)
|
2026-05-15 14:03:33 +08:00
|
|
|
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("SiliconFlow stream TTS API error: %d, body: %s", resp.StatusCode, string(body))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buf := make([]byte, 32*1024)
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
n, err := resp.Body.Read(buf)
|
|
|
|
|
if n > 0 {
|
|
|
|
|
chunk := string(buf[:n])
|
|
|
|
|
if errSend := sender(&chunk, nil); errSend != nil {
|
|
|
|
|
return errSend
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == io.EOF {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("error reading SiliconFlow binary audio stream: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
2026-05-12 17:17:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OCRFile OCR file
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) {
|
|
|
|
|
return nil, fmt.Errorf("%s, no such method", s.Name())
|
2026-05-12 17:17:44 +08:00
|
|
|
}
|
2026-05-15 12:29:52 +08:00
|
|
|
|
|
|
|
|
// ParseFile parse file
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) {
|
|
|
|
|
return nil, fmt.Errorf("%s, no such method", s.Name())
|
2026-05-15 12:29:52 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) {
|
|
|
|
|
return nil, fmt.Errorf("%s, no such method", s.Name())
|
2026-05-15 12:29:52 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:09:07 +08:00
|
|
|
func (s *SiliconflowModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) {
|
|
|
|
|
return nil, fmt.Errorf("%s, no such method", s.Name())
|
2026-05-15 12:29:52 +08:00
|
|
|
}
|