From 47626bbe6369591b7a84755357098a046b9b5c4e Mon Sep 17 00:00:00 2001 From: Hz_ Date: Wed, 27 May 2026 13:19:39 +0800 Subject: [PATCH] go: add Qiniu model provider (#15280) ### What problem does this PR solve? This PR adds Qiniu provider integration for the Go model driver layer in RAGFlow. Supported capabilities: - [X] Chat - [X] Think Chat - [X] Stream Chat - [X] Stream Think Chat - [X] Model listing - [X] Provider configuration and factory registration Verified examples from the CLI: ``` login user '***' password '***'; ADD PROVIDER 'qiniu'; CREATE PROVIDER 'qiniu' INSTANCE 'test' KEY '***'; chat with 'deepseek/deepseek-v3.1-terminus-thinking@test@qiniu' message 'hello'; think chat with 'deepseek/deepseek-v3.1-terminus-thinking@test@qiniu' message 'hello'; stream chat with 'deepseek/deepseek-v3.1-terminus-thinking@test@qiniu' message 'hello, what are you'; stream think chat with 'deepseek/deepseek-v3.1-terminus-thinking@test@qiniu' message 'hello, what are you'; stream think chat with 'qwen3-max-2026-01-23@test@qiniu' message 'hello, what are you'; LIST MODELS FROM 'qiniu' 'test'; ``` ### Type of change - [X] New Feature - [X] Provider integration --- conf/models/qiniu.json | 419 +++++++++++++++++++++++++ internal/entity/models/factory.go | 2 + internal/entity/models/qiniu.go | 504 ++++++++++++++++++++++++++++++ 3 files changed, 925 insertions(+) create mode 100644 conf/models/qiniu.json create mode 100644 internal/entity/models/qiniu.go diff --git a/conf/models/qiniu.json b/conf/models/qiniu.json new file mode 100644 index 0000000000..51ab82f2b3 --- /dev/null +++ b/conf/models/qiniu.json @@ -0,0 +1,419 @@ +{ + "name": "Qiniu", + "url": { + "default": "https://api.qnaigc.com/v1" + }, + "url_suffix": { + "chat": "chat/completions", + "models": "models" + }, + "class": "qiniu", + "models": [ + { + "name": "deepseek/deepseek-v4-flash", + "max_tokens": 1048576, + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "deepseek/deepseek-v4-pro", + "max_tokens": 1048576, + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "moonshotai/kimi-k2.6", + "model_types": ["vision"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "moonshotai/kimi-k2.5", + "model_types": ["vision"] + }, + { + "name": "z-ai/glm-5.1", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "z-ai/glm-5", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "minimax/minimax-m2.7", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "minimax/minimax-m2.5", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "minimax/minimax-m2.5-highspeed", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "minimax/minimax-m2.1", + "model_types": ["chat"] + }, + { + "name": "kimi-k2-thinking", + "max_tokens": 262144, + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "meituan/longcat-flash-lite", + "model_types": ["chat"] + }, + { + "name": "qwen3-max", + "model_types": ["chat"] + }, + { + "name": "z-ai/glm-4.6", + "max_tokens": 204800, + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "z-ai/glm-4.7", + "model_types": ["chat"] + }, + { + "name": "deepseek/deepseek-v3.2-251201", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "deepseek/deepseek-v3.2-exp-thinking", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "deepseek/deepseek-v3.1-terminus", + "model_types": ["chat"] + }, + { + "name": "deepseek/deepseek-v3.1-terminus-thinking", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "deepseek-v3.1", + "model_types": ["chat"] + }, + { + "name": "deepseek-v3-0324", + "model_types": ["chat"] + }, + { + "name": "deepseek-r1-0528", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "deepseek-r1", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "doubao-seed-1.6-flash", + "max_tokens": 262144, + "model_types": ["vision"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "doubao-1.5-pro-32k", + "max_tokens": 131072, + "model_types": ["vision"] + }, + { + "name": "doubao-seed-1.6", + "max_tokens": 262144, + "model_types": ["vision"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "doubao-seed-2.0-pro", + "model_types": ["vision"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "doubao-seed-2.0-lite", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "doubao-seed-2.0-mini", + "max_tokens": 262144, + "model_types": ["vision"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "doubao-seed-2.0-code", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3-next-80b-a3b-thinking", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3-235b-a22b-thinking-2507", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3-max-2026-01-23", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3-next-80b-a3b-instruct", + "model_types": ["chat"] + }, + { + "name": "qwen3-max-preview", + "model_types": ["chat"] + }, + { + "name": "qwen-2.5-vl-72b-instruct", + "model_types": ["vision"] + }, + { + "name": "qwen3-coder-480b-a35b-instruct", + "model_types": ["chat"] + }, + { + "name": "qwen-turbo", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3-235b-a22b-instruct-2507", + "model_types": ["chat"] + }, + { + "name": "qwen3-32b", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3-30b-a3b", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3-235b-a22b", + "model_types": ["chat"] + }, + { + "name": "qwen-2.5-vl-7b-instruct", + "model_types": ["vision"] + }, + { + "name": "qwen-vl-max-2025-01-25", + "model_types": ["vision"] + }, + { + "name": "qwen2.5-max-2025-01-25", + "model_types": ["chat"] + }, + { + "name": "minimax-m1", + "max_tokens": 1048576, + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "glm-4.5", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3-vl-30b-a3b-instruct", + "model_types": ["vision"] + }, + { + "name": "deepseek-v3", + "model_types": ["chat"] + }, + { + "name": "qwen3-30b-a3b-thinking-2507", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "glm-4.5-air", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3.5-397b-a17b", + "model_types": ["vision"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen/qwen3.5-plus", + "model_types": ["vision"] + }, + { + "name": "qwen/qwen3.6-plus", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "deepseek/deepseek-v3.2-exp", + "model_types": ["chat"] + }, + { + "name": "qwen/qwen3.7-max", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen/qwen3.6-27b", + "max_tokens": 262144, + "model_types": ["vision"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "tencent/hy3-preview", + "model_types": ["chat"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3.5-35b-a3b", + "model_types": ["vision"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3-vl-30b-a3b-thinking", + "model_types": ["vision"], + "thinking": { + "default_value": true, + "clear_thinking": true + } + }, + { + "name": "qwen3-30b-a3b-instruct-2507", + "model_types": ["chat"] + } + ] +} \ No newline at end of file diff --git a/internal/entity/models/factory.go b/internal/entity/models/factory.go index 6b520cd926..985961f898 100644 --- a/internal/entity/models/factory.go +++ b/internal/entity/models/factory.go @@ -149,6 +149,8 @@ func (f *ModelFactory) CreateModelDriver(providerName string, baseURL map[string return NewOrcaRouterModel(baseURL, urlSuffix), nil case "huaweicloud": return NewHuaweiCloudModel(baseURL, urlSuffix), nil + case "qiniu": + return NewQiniuModel(baseURL, urlSuffix), nil default: return NewDummyModel(baseURL, urlSuffix), nil } diff --git a/internal/entity/models/qiniu.go b/internal/entity/models/qiniu.go new file mode 100644 index 0000000000..0fea9cd33b --- /dev/null +++ b/internal/entity/models/qiniu.go @@ -0,0 +1,504 @@ +package models + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net/http" + "ragflow/internal/common" + "strings" + "time" + + "github.com/goccy/go-json" +) + +type QiniuModel struct { + BaseURL map[string]string + URLSuffix URLSuffix + httpClient *http.Client +} + +func NewQiniuModel(baseURL map[string]string, urlSuffix URLSuffix) *QiniuModel { + return &QiniuModel{ + BaseURL: baseURL, + URLSuffix: urlSuffix, + httpClient: &http.Client{ + Timeout: 120 * time.Second, + Transport: &http.Transport{ + MaxConnsPerHost: 10, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + DisableCompression: false, + }, + }, + } +} + +func (q *QiniuModel) NewInstance(baseURL map[string]string) ModelDriver { + return NewQiniuModel(baseURL, q.URLSuffix) +} + +func (q *QiniuModel) Name() string { + return "qiniu" +} + +var qiniuQwenThinkingModels = map[string]struct{}{ + "qwen3-next-80b-a3b-thinking": {}, + "qwen3-235b-a22b-thinking-2507": {}, + "qwen3-max-2026-01-23": {}, + "qwen-turbo": {}, + "qwen3-32b": {}, + "qwen3-30b-a3b": {}, + "qwen3-30b-a3b-thinking-2507": {}, + "qwen3.5-397b-a17b": {}, + "qwen/qwen3.6-plus": {}, + "qwen/qwen3.7-max": {}, + "qwen/qwen3.6-27b": {}, + "qwen3.5-35b-a3b": {}, + "qwen3-vl-30b-a3b-thinking": {}, +} + +var qiniuThinkingModels = map[string]struct{}{ + "deepseek/deepseek-v4-flash": {}, + "deepseek/deepseek-v4-pro": {}, + "moonshotai/kimi-k2.6": {}, + "z-ai/glm-5.1": {}, + "z-ai/glm-5": {}, + "minimax/minimax-m2.7": {}, + "minimax/minimax-m2.5": {}, + "minimax/minimax-m2.5-highspeed": {}, + "kimi-k2-thinking": {}, + "z-ai/glm-4.6": {}, + "deepseek/deepseek-v3.2-251201": {}, + "deepseek/deepseek-v3.2-exp-thinking": {}, + "deepseek/deepseek-v3.1-terminus-thinking": {}, + "deepseek-r1-0528": {}, + "deepseek-r1": {}, + "doubao-seed-1.6-flash": {}, + "doubao-seed-1.6": {}, + "doubao-seed-2.0-pro": {}, + "doubao-seed-2.0-lite": {}, + "doubao-seed-2.0-mini": {}, + "doubao-seed-2.0-code": {}, + "minimax-m1": {}, + "glm-4.5": {}, + "glm-4.5-air": {}, + "tencent/hy3-preview": {}, +} + +func applyQiniuThinkingConfig(reqBody map[string]interface{}, modelName string, chatModelConfig *ChatConfig) { + if chatModelConfig == nil || chatModelConfig.Thinking == nil { + return + } + + lowerModelName := strings.ToLower(modelName) + if _, ok := qiniuQwenThinkingModels[lowerModelName]; ok { + enableThinking := *chatModelConfig.Thinking + if chatModelConfig.Effort != nil && strings.ToLower(*chatModelConfig.Effort) == "none" { + enableThinking = false + } + reqBody["enable_thinking"] = enableThinking + return + } + + if _, ok := qiniuThinkingModels[lowerModelName]; !ok { + return + } + + thinkingType := "disabled" + if *chatModelConfig.Thinking { + thinkingType = "enabled" + effort := "high" + if chatModelConfig.Effort != nil && *chatModelConfig.Effort != "" { + effort = strings.ToLower(*chatModelConfig.Effort) + } + + switch effort { + case "none": + thinkingType = "disabled" + case "default": + reqBody["reasoning_effort"] = "high" + case "low", "medium", "high": + reqBody["reasoning_effort"] = effort + case "max": + reqBody["reasoning_effort"] = "high" + default: + reqBody["reasoning_effort"] = effort + } + } + + reqBody["thinking"] = map[string]interface{}{ + "type": thinkingType, + } +} + +func (q *QiniuModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { + if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" { + return nil, fmt.Errorf("api key is nil or empty") + } + if len(messages) == 0 { + return nil, fmt.Errorf("no messages") + } + + var region = "default" + if apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + url := fmt.Sprintf("%s/%s", q.BaseURL[region], q.URLSuffix.Chat) + + apiMessages := make([]map[string]interface{}, len(messages)) + for i, msg := range messages { + apiMessages[i] = map[string]interface{}{ + "role": msg.Role, + "content": msg.Content, + } + } + reqBody := map[string]interface{}{ + "model": modelName, + "messages": apiMessages, + "stream": false, + "temperature": 1, + } + + if chatModelConfig != nil { + 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 + } + + applyQiniuThinkingConfig(reqBody, modelName, chatModelConfig) + } + + 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 := q.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 body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + } + + 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") + } + + content, ok := messageMap["content"].(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 reasoning content format") + } + + if reasonContent != "" && reasonContent[0] == '\n' { + reasonContent = reasonContent[1:] + } + } + + chatResponse := &ChatResponse{ + Answer: &content, + ReasonContent: &reasonContent, + } + + return chatResponse, nil +} + +func (q *QiniuModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { + if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" { + return fmt.Errorf("api key is nil or empty") + } + if len(messages) == 0 { + return fmt.Errorf("messages is empty") + } + + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + baseURL := strings.TrimSuffix(q.BaseURL[region], "/") + if baseURL == "" { + return fmt.Errorf("qiniu: no base URL configured for region %q", region) + } + url := fmt.Sprintf("%s/%s", baseURL, q.URLSuffix.Chat) + + apiMessages := make([]map[string]interface{}, len(messages)) + for i, msg := range messages { + apiMessages[i] = map[string]interface{}{ + "role": msg.Role, + "content": msg.Content, + } + } + reqBody := map[string]interface{}{ + "model": modelName, + "messages": apiMessages, + "stream": true, + "temperature": 1, + } + + if chatModelConfig != nil { + 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 + } + + applyQiniuThinkingConfig(reqBody, modelName, chatModelConfig) + } + 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 := q.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)) + } + + scanner := bufio.NewScanner(resp.Body) + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + for scanner.Scan() { + line := scanner.Text() + common.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 + } + + reasoningContent, ok := delta["reasoning_content"].(string) + if ok && reasoningContent != "" { + if err = sender(nil, &reasoningContent); 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 != "" { + break + } + } + // Send [DONE] marker for OpenAI compatibility + endOfStream := "[DONE]" + if err = sender(&endOfStream, nil); err != nil { + return err + } + + return scanner.Err() +} + +func (q *QiniuModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { + return nil, fmt.Errorf("%s, no such method", q.Name()) +} + +func (q *QiniuModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { + return nil, fmt.Errorf("%s, no such method", q.Name()) +} + +func (q *QiniuModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { + return nil, fmt.Errorf("%s, no such method", q.Name()) +} + +func (q *QiniuModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { + return fmt.Errorf("%s, no such method", q.Name()) +} + +func (q *QiniuModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { + return nil, fmt.Errorf("%s, no such method", q.Name()) +} + +func (q *QiniuModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { + return fmt.Errorf("%s, no such method", q.Name()) +} + +func (q *QiniuModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { + return nil, fmt.Errorf("%s, no such method", q.Name()) +} + +func (q *QiniuModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { + return nil, fmt.Errorf("%s, no such method", q.Name()) +} + +func (q *QiniuModel) ListModels(apiConfig *APIConfig) ([]string, error) { + if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" { + return nil, fmt.Errorf("api key is nil or empty") + } + + var region = "default" + if apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + baseURL := strings.TrimSuffix(q.BaseURL[region], "/") + if baseURL == "" { + return nil, fmt.Errorf("qiniu: no base URL configured for region %q", region) + } + url := fmt.Sprintf("%s/%s", baseURL, q.URLSuffix.Models) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + + resp, err := q.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)) + } + + var result map[string]interface{} + if err = json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + data, ok := result["data"].([]interface{}) + if !ok { + return nil, fmt.Errorf("invalid models list format") + } + + models := make([]string, 0, len(data)) + for _, model := range data { + modelMap, ok := model.(map[string]interface{}) + if !ok { + continue + } + modelID, ok := modelMap["id"].(string) + if !ok || modelID == "" { + continue + } + models = append(models, modelID) + } + + return models, nil +} + +func (q *QiniuModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { + return nil, fmt.Errorf("%s, no such method", q.Name()) +} + +func (q *QiniuModel) CheckConnection(apiConfig *APIConfig) error { + return fmt.Errorf("%s, no such method", q.Name()) +} + +func (q *QiniuModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { + return nil, fmt.Errorf("%s, no such method", q.Name()) +} + +func (q *QiniuModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { + return nil, fmt.Errorf("%s, no such method", q.Name()) +}