diff --git a/conf/models/deepseek.json b/conf/models/deepseek.json index b0504223af..61c6a0f9e6 100644 --- a/conf/models/deepseek.json +++ b/conf/models/deepseek.json @@ -13,16 +13,14 @@ "max_tokens": 128000, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "deepseek-reasoner", "max_tokens": 128000, "model_types": [ "chat" - ], - "features": {} + ] } ], "features": { diff --git a/conf/models/minimax.json b/conf/models/minimax.json new file mode 100644 index 0000000000..b2bf985600 --- /dev/null +++ b/conf/models/minimax.json @@ -0,0 +1,78 @@ +{ + "name": "MiniMax", + "url": { + "default": "https://api.minimaxi.com/", + "global": "https://api.minimax.io/" + }, + "url_suffix": { + "chat": "v1/text/chatcompletion_v2", + "tts": "v1/t2a_v2", + "files": "v1/files/list" + }, + "models": [ + { + "name": "minimax-m2.7", + "max_tokens": 204800, + "model_types": [ + "chat" + ] + }, + { + "name": "minimax-m2.7-highspeed", + "max_tokens": 204800, + "model_types": [ + "chat" + ] + }, + { + "name": "minimax-m2.5", + "max_tokens": 204800, + "model_types": [ + "chat" + ] + }, + { + "name": "minimax-m2.5-highspeed", + "max_tokens": 204800, + "model_types": [ + "chat" + ] + }, + { + "name": "minimax-m2.1", + "max_tokens": 204800, + "model_types": [ + "chat" + ] + }, + { + "name": "minimax-m2.1-highspeed", + "max_tokens": 204800, + "model_types": [ + "chat" + ] + }, + { + "name": "minimax-m2", + "max_tokens": 204800, + "model_types": [ + "chat" + ] + }, + { + "name": "minimax-m2-her", + "max_tokens": 65536, + "model_types": [ + "chat" + ] + } + ], + "features": { + "thinking": { + "default_value": true, + "supported_models": [ + "deepseek-chat" + ] + } + } +} \ No newline at end of file diff --git a/conf/models/moonshot.json b/conf/models/moonshot.json index 94c935a786..e54fdb33d3 100644 --- a/conf/models/moonshot.json +++ b/conf/models/moonshot.json @@ -15,8 +15,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "kimi-k2.5", @@ -24,8 +23,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "moonshot-v1-8k", @@ -33,24 +31,21 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "moonshot-v1-32k", "max_tokens": 32000, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "moonshot-v1-128k", "max_tokens": 128000, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "moonshot-v1-8k-vision-preview", @@ -58,8 +53,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "moonshot-v1-32k-vision-preview", @@ -67,8 +61,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "moonshot-v1-128k-vision-preview", @@ -76,8 +69,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] } ], "features": { diff --git a/conf/models/openai.json b/conf/models/openai.json index f89c6c0d1d..d21d41650c 100644 --- a/conf/models/openai.json +++ b/conf/models/openai.json @@ -13,8 +13,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-5.2", @@ -22,8 +21,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-5.1", @@ -31,8 +29,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-5.1-chat-latest", @@ -40,8 +37,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-5", @@ -49,8 +45,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-5-mini", @@ -58,8 +53,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-5-nano", @@ -67,8 +61,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-5-chat-latest", @@ -76,8 +69,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-4.1", @@ -85,8 +77,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-4.1-mini", @@ -94,8 +85,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-4.1-nano", @@ -103,16 +93,14 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-4.5-preview", "max_tokens": 128000, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "o3", @@ -120,8 +108,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "o4-mini", @@ -129,8 +116,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "o4-mini-high", @@ -138,8 +124,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-4o-mini", @@ -147,8 +132,7 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-4o", @@ -156,88 +140,77 @@ "model_types": [ "chat", "vision" - ], - "features": {} + ] }, { "name": "gpt-3.5-turbo", "max_tokens": 4096, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "gpt-3.5-turbo-16k-0613", "max_tokens": 16385, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "text-embedding-ada-002", "max_tokens": 8191, "model_types": [ "embedding" - ], - "features": {} + ] }, { "name": "text-embedding-3-small", "max_tokens": 8191, "model_types": [ "embedding" - ], - "features": {} + ] }, { "name": "text-embedding-3-large", "max_tokens": 8191, "model_types": [ "embedding" - ], - "features": {} + ] }, { "name": "whisper-1", "max_tokens": 26214400, "model_types": [ "asr" - ], - "features": {} + ] }, { "name": "gpt-4", "max_tokens": 8191, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "gpt-4-turbo", "max_tokens": 8191, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "gpt-4-32k", "max_tokens": 32768, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "tts-1", "max_tokens": 2048, "model_types": [ "tts" - ], - "features": {} + ] } ] } \ No newline at end of file diff --git a/conf/models/xai.json b/conf/models/xai.json index 5e12776c92..1de51cd6b2 100644 --- a/conf/models/xai.json +++ b/conf/models/xai.json @@ -10,38 +10,32 @@ { "name": "grok-4", "max_tokens": 256000, - "model_types": ["chat"], - "features": {} + "model_types": ["chat"] }, { "name": "grok-3", "max_tokens": 131072, - "model_types": ["chat"], - "features": {} + "model_types": ["chat"] }, { "name": "grok-3-fast", "max_tokens": 131072, - "model_types": ["chat"], - "features": {} + "model_types": ["chat"] }, { "name": "grok-3-mini", "max_tokens": 131072, - "model_types": ["chat"], - "features": {} + "model_types": ["chat"] }, { "name": "grok-3-mini-mini-fast", "max_tokens": 131072, - "model_types": ["chat"], - "features": {} + "model_types": ["chat"] }, { "name": "grok-2-vision", "max_tokens": 32768, - "model_types": ["vision"], - "features": {} + "model_types": ["vision"] } ] } \ No newline at end of file diff --git a/conf/models/zhipu-ai.json b/conf/models/zhipu-ai.json index b38624bffe..3ed3b3cf74 100644 --- a/conf/models/zhipu-ai.json +++ b/conf/models/zhipu-ai.json @@ -8,206 +8,217 @@ "async_chat": "async/chat/completions", "async_result": "async-result", "embedding": "embedding", - "rerank": "rerank" + "rerank": "rerank", + "files": "files" }, "models": [ { - "name": "glm-4.7", - "max_tokens": 128000, + "name": "glm-5.1", + "max_tokens": 204800, "model_types": [ "chat" - ], - "features": {} + ] }, { - "name": "glm-4.5", - "max_tokens": 128000, + "name": "glm-5", + "max_tokens": 204800, "model_types": [ "chat" - ], - "features": {} + ] + }, + { + "name": "glm-5-turbo", + "max_tokens": 204800, + "model_types": [ + "chat" + ] + }, + { + "name": "glm-5v-turbo", + "max_tokens": 204800, + "model_types": [ + "chat" + ] + }, + { + "name": "glm-4.7", + "max_tokens": 204800, + "model_types": [ + "chat" + ] + }, + { + "name": "glm-4.7-flashx", + "max_tokens": 204800, + "model_types": [ + "chat" + ] + }, + { + "name": "glm-4.6", + "max_tokens": 204800, + "model_types": [ + "chat" + ] }, { "name": "glm-4.6v-Flash", - "max_tokens": 128000, + "max_tokens": 131072, "model_types": [ "chat", "vision" - ], - "features": {} + ] + }, + { + "name": "glm-4.5", + "max_tokens": 131072, + "model_types": [ + "chat" + ] }, { "name": "glm-4.5-x", - "max_tokens": 128000, + "max_tokens": 131072, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "glm-4.5-air", - "max_tokens": 128000, + "max_tokens": 131072, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "glm-4.5-airx", - "max_tokens": 128000, + "max_tokens": 131072, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "glm-4.5-flash", - "max_tokens": 128000, + "max_tokens": 131072, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "glm-4.5v", "max_tokens": 64000, "model_types": [ "vision" - ], - "features": {} + ] }, { "name": "glm-4-plus", - "max_tokens": 128000, + "max_tokens": 131072, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "glm-4-0520", - "max_tokens": 128000, + "max_tokens": 131072, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "glm-4", - "max_tokens": 128000, + "max_tokens": 131072, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "glm-4-airx", "max_tokens": 8000, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "glm-4-air", - "max_tokens": 128000, + "max_tokens": 131072, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "glm-4-flash", - "max_tokens": 128000, + "max_tokens": 131072, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "glm-4-flashx", - "max_tokens": 128000, + "max_tokens": 131072, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "glm-4-long", "max_tokens": 1000000, "model_types": [ "chat" - ], - "features": {} - }, - { - "name": "glm-3-turbo", - "max_tokens": 128000, - "model_types": [ - "chat" - ], - "features": {} + ] }, { "name": "glm-4v", "max_tokens": 2000, "model_types": [ "vision" - ], - "features": {} + ] }, { "name": "glm-4-9b", "max_tokens": 8192, "model_types": [ "chat" - ], - "features": {} + ] }, { "name": "embedding-2", "max_tokens": 512, "model_types": [ "embedding" - ], - "features": {} + ] }, { "name": "embedding-3", "max_tokens": 512, "model_types": [ "embedding" - ], - "features": {} + ] }, { "name": "glm-asr", "max_tokens": 4096, "model_types": [ "asr" - ], - "features": {} + ] }, { "name": "glm-tts", "model_types": [ "tts" - ], - "features": {} + ] }, { "name": "glm-ocr", "model_types": [ "ocr" - ], - "features": {} + ] }, { "name": "glm-rerank", "model_types": [ "rerank" - ], - "features": {} + ] } ], "features": { diff --git a/internal/cli/client.go b/internal/cli/client.go index 984e1e8ff8..18a0be69ac 100644 --- a/internal/cli/client.go +++ b/internal/cli/client.go @@ -250,6 +250,8 @@ func (c *RAGFlowClient) ExecuteUserCommand(cmd *Command) (ResponseIf, error) { return c.ChatToModel(cmd) case "think_chat_to_model": return c.ChatToModel(cmd) + case "check_provider_connection": + return c.CheckProviderConnection(cmd) case "use_model": return c.UseModel(cmd) case "show_current_model": diff --git a/internal/cli/lexer.go b/internal/cli/lexer.go index 631441626b..8dc12bc3cf 100644 --- a/internal/cli/lexer.go +++ b/internal/cli/lexer.go @@ -385,6 +385,8 @@ func (l *Lexer) lookupIdent(ident string) Token { return Token{Type: TokenFile, Value: ident} case "USE": return Token{Type: TokenUse, Value: ident} + case "CHECK": + return Token{Type: TokenCheck, Value: ident} case "UPDATE": return Token{Type: TokenUpdate, Value: ident} case "REMOVE": diff --git a/internal/cli/parser.go b/internal/cli/parser.go index 85271b2725..254893ef75 100644 --- a/internal/cli/parser.go +++ b/internal/cli/parser.go @@ -196,6 +196,8 @@ func (p *Parser) parseUserCommand() (*Command, error) { return p.parseChatCommand() case TokenThink: return p.parseThinkCommand() + case TokenCheck: + return p.parseCheckCommand() case TokenLS: return p.parseContextListCommand() case TokenCat: diff --git a/internal/cli/types.go b/internal/cli/types.go index 59130f3107..7969a26bf4 100644 --- a/internal/cli/types.go +++ b/internal/cli/types.go @@ -115,6 +115,7 @@ const ( TokenDisable TokenEnable TokenUse + TokenCheck TokenThink TokenLS TokenCat diff --git a/internal/cli/user_command.go b/internal/cli/user_command.go index 875ab14ac2..1066af57cd 100644 --- a/internal/cli/user_command.go +++ b/internal/cli/user_command.go @@ -1579,6 +1579,46 @@ func (c *RAGFlowClient) ChatToModel(cmd *Command) (ResponseIf, error) { return &result, nil } +func (c *RAGFlowClient) CheckProviderConnection(cmd *Command) (ResponseIf, error) { + if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" { + return nil, fmt.Errorf("API token not set. Please login first") + } + + if c.ServerType != "user" { + return nil, fmt.Errorf("this command is only allowed in USER mode") + } + + instanceName, ok := cmd.Params["instance_name"].(string) + if !ok { + return nil, fmt.Errorf("instance name not provided") + } + + providerName, ok := cmd.Params["provider_name"].(string) + if !ok { + return nil, fmt.Errorf("provider name not provided") + } + + url := fmt.Sprintf("/providers/%s/instances/%s/connection", providerName, instanceName) + + resp, err := c.HTTPClient.Request("GET", url, true, "web", nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to check provider connection: %w", err) + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("failed to check provider connection: HTTP %d, body: %s", resp.StatusCode, string(resp.Body)) + } + var result SimpleResponse + if err = json.Unmarshal(resp.Body, &result); err != nil { + return nil, fmt.Errorf("check provider connection failed: invalid JSON (%w)", err) + } + if result.Code != 0 { + return nil, fmt.Errorf("%s", result.Message) + } + result.Duration = resp.Duration + return &result, nil + +} + // UseModel sets the current model for chat func (c *RAGFlowClient) UseModel(cmd *Command) (ResponseIf, error) { if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" { diff --git a/internal/cli/user_parser.go b/internal/cli/user_parser.go index d9e48ab974..a597ac64cf 100644 --- a/internal/cli/user_parser.go +++ b/internal/cli/user_parser.go @@ -2325,6 +2325,42 @@ func (p *Parser) parseStreamCommand() (*Command, error) { return command, nil } +func (p *Parser) parseCheckCommand() (*Command, error) { + p.nextToken() // consume CHECK + + if p.curToken.Type != TokenInstance { + return nil, fmt.Errorf("expected INSTANCE after CHECK") + } + p.nextToken() + + if p.curToken.Type != TokenQuotedString { + return nil, fmt.Errorf("expected instance name after INSTANCE") + } + instanceName := p.curToken.Value + p.nextToken() + + if p.curToken.Type != TokenFrom { + return nil, fmt.Errorf("expected FROM after instance name") + } + p.nextToken() + + if p.curToken.Type != TokenQuotedString { + return nil, fmt.Errorf("expected provider name after FROM") + } + providerName := p.curToken.Value + p.nextToken() + + // Semicolon is optional + if p.curToken.Type == TokenSemicolon { + p.nextToken() + } + + cmd := NewCommand("check_provider_connection") + cmd.Params["provider_name"] = providerName + cmd.Params["instance_name"] = instanceName + return cmd, nil +} + func (p *Parser) parseUseCommand() (*Command, error) { p.nextToken() // consume USE diff --git a/internal/entity/models/deepseek.go b/internal/entity/models/deepseek.go index 6d2945190a..f215df7b1c 100644 --- a/internal/entity/models/deepseek.go +++ b/internal/entity/models/deepseek.go @@ -55,37 +55,19 @@ func (z *DeepSeekModel) Name() string { // Chat sends a message and returns response func (z *DeepSeekModel) Chat(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { - return nil, fmt.Errorf("not implemented") + return nil, fmt.Errorf("%s, no such method", z.Name()) } // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) func (z *DeepSeekModel) ChatStreamlyWithSender(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { - return fmt.Errorf("not implemented") + return nil } // EncodeToEmbedding encodes a list of texts into embeddings func (z *DeepSeekModel) EncodeToEmbedding(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([][]float64, error) { - return nil, fmt.Errorf("not implemented") + return nil, fmt.Errorf("%s, no such method", z.Name()) } -/* -{ - "object": "list", - "data": [ - { - "id": "deepseek-chat", - "object": "model", - "owned_by": "deepseek" - }, - { - "id": "deepseek-reasoner", - "object": "model", - "owned_by": "deepseek" - } - ] -} -*/ - type Model struct { ID string `json:"id"` Object string `json:"object"` @@ -153,3 +135,11 @@ func (z *DeepSeekModel) ListModels(apiConfig *APIConfig) ([]string, error) { func (z *DeepSeekModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", z.Name()) } + +func (z *DeepSeekModel) CheckConnection(apiConfig *APIConfig) error { + _, err := z.ListModels(apiConfig) + if err != nil { + return err + } + return nil +} diff --git a/internal/entity/models/dummy.go b/internal/entity/models/dummy.go index 4846a45776..4d81c62bdc 100644 --- a/internal/entity/models/dummy.go +++ b/internal/entity/models/dummy.go @@ -20,13 +20,13 @@ import ( "fmt" ) -// DummyModel implements ModelDriver for Zhipu AI +// DummyModel implements ModelDriver for Dummy AI type DummyModel struct { BaseURL map[string]string URLSuffix URLSuffix } -// NewDummyModel creates a new Zhipu AI model instance +// NewDummyModel creates a new Dummy AI model instance func NewDummyModel(baseURL map[string]string, urlSuffix URLSuffix) *DummyModel { return &DummyModel{ BaseURL: baseURL, @@ -60,3 +60,7 @@ func (z *DummyModel) ListModels(apiConfig *APIConfig) ([]string, error) { func (z *DummyModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } + +func (z *DummyModel) CheckConnection(apiConfig *APIConfig) error { + return fmt.Errorf("no such method") +} diff --git a/internal/entity/models/factory.go b/internal/entity/models/factory.go index dd9efc1667..facfce3707 100644 --- a/internal/entity/models/factory.go +++ b/internal/entity/models/factory.go @@ -39,6 +39,8 @@ func (f *ModelFactory) CreateModelDriver(providerName string, baseURL map[string return NewDeepSeekModel(baseURL, urlSuffix), nil case "moonshot": return NewMoonshotModel(baseURL, urlSuffix), nil + case "minimax": + return NewMinimaxModel(baseURL, urlSuffix), nil default: return NewDummyModel(baseURL, urlSuffix), nil } diff --git a/internal/entity/models/minimax.go b/internal/entity/models/minimax.go new file mode 100644 index 0000000000..f090a2b58b --- /dev/null +++ b/internal/entity/models/minimax.go @@ -0,0 +1,109 @@ +// +// 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 ( + "fmt" + "io" + "net/http" + "time" +) + +// MinimaxModel implements ModelDriver for Zhipu AI +type MinimaxModel struct { + BaseURL map[string]string + URLSuffix URLSuffix + httpClient *http.Client // Reusable HTTP client with connection pool +} + +// NewMinimaxModel creates a new Zhipu AI model instance +func NewMinimaxModel(baseURL map[string]string, urlSuffix URLSuffix) *MinimaxModel { + return &MinimaxModel{ + 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 *MinimaxModel) Name() string { + return "minimax" +} + +// Chat sends a message and returns response +func (z *MinimaxModel) Chat(modelName, message *string, apiConfig *APIConfig, modelConfig *ChatConfig) (*ChatResponse, error) { + return nil, fmt.Errorf("%s, no such method", z.Name()) +} + +// ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) +func (z *MinimaxModel) ChatStreamlyWithSender(modelName, message *string, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { + return fmt.Errorf("%s, no such method", z.Name()) +} + +// EncodeToEmbedding encodes a list of texts into embeddings +func (z *MinimaxModel) EncodeToEmbedding(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([][]float64, error) { + return nil, fmt.Errorf("not implemented") +} + +func (z *MinimaxModel) ListModels(apiConfig *APIConfig) ([]string, error) { + return nil, fmt.Errorf("%s, no such method", z.Name()) +} + +func (z *MinimaxModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { + return nil, fmt.Errorf("%s, no such method", z.Name()) +} + +func (z *MinimaxModel) CheckConnection(apiConfig *APIConfig) error { + var region = "default" + if apiConfig.Region != nil { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Files) + + req, err := http.NewRequest("GET", url, nil) + 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() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + } + + return nil +} diff --git a/internal/entity/models/moonshot.go b/internal/entity/models/moonshot.go index 55058cb41a..7117874e52 100644 --- a/internal/entity/models/moonshot.go +++ b/internal/entity/models/moonshot.go @@ -180,3 +180,11 @@ func (z *MoonshotModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e return response, nil } + +func (z *MoonshotModel) CheckConnection(apiConfig *APIConfig) error { + _, err := z.ListModels(apiConfig) + if err != nil { + return err + } + return nil +} diff --git a/internal/entity/models/types.go b/internal/entity/models/types.go index c316fd60eb..705dc92595 100644 --- a/internal/entity/models/types.go +++ b/internal/entity/models/types.go @@ -14,6 +14,8 @@ type ModelDriver interface { ListModels(apiConfig *APIConfig) ([]string, error) Balance(apiConfig *APIConfig) (map[string]interface{}, error) + + CheckConnection(apiConfig *APIConfig) error } type ChatResponse struct { @@ -30,6 +32,7 @@ type URLSuffix struct { Rerank string `json:"rerank"` Models string `json:"models"` Balance string `json:"balance"` + Files string `json:"files"` } type ChatConfig struct { diff --git a/internal/entity/models/zhipu-ai.go b/internal/entity/models/zhipu-ai.go index b7c6deb8cd..e30a4aeac5 100644 --- a/internal/entity/models/zhipu-ai.go +++ b/internal/entity/models/zhipu-ai.go @@ -425,3 +425,37 @@ func (z *ZhipuAIModel) ListModels(apiConfig *APIConfig) ([]string, error) { func (z *ZhipuAIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", z.Name()) } + +func (z *ZhipuAIModel) CheckConnection(apiConfig *APIConfig) error { + var region = "default" + if apiConfig.Region != nil { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Files) + + req, err := http.NewRequest("GET", url, nil) + 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() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + } + + return nil +} diff --git a/internal/handler/providers.go b/internal/handler/providers.go index 8a493680e4..a3bdddb6c6 100644 --- a/internal/handler/providers.go +++ b/internal/handler/providers.go @@ -393,6 +393,43 @@ func (h *ProviderHandler) ShowInstanceBalance(c *gin.Context) { }) } +func (h *ProviderHandler) CheckProviderConnection(c *gin.Context) { + providerName := c.Param("provider_name") + if providerName == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "Provider name is required", + }) + return + } + + instanceName := c.Param("instance_name") + if instanceName == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "Instance name is required", + }) + return + } + + userID := c.GetString("user_id") + + // Get tenant ID from user + errorCode, err := h.modelProviderService.CheckProviderConnection(providerName, instanceName, userID) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "code": errorCode, + "message": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + }) +} + type AlterProviderInstanceRequest struct { LLMName string `json:"llm_name" binding:"required"` } diff --git a/internal/router/router.go b/internal/router/router.go index b2543d1b0a..18e1ccaaa1 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -213,6 +213,7 @@ func (r *Router) Setup(engine *gin.Engine) { provider.GET("/:provider_name/instances", r.providerHandler.ListProviderInstances) provider.GET("/:provider_name/instances/:instance_name", r.providerHandler.ShowProviderInstance) provider.GET("/:provider_name/instances/:instance_name/balance", r.providerHandler.ShowInstanceBalance) + provider.GET("/:provider_name/instances/:instance_name/connection", r.providerHandler.CheckProviderConnection) provider.PUT("/:provider_name/instances/:instance_name", r.providerHandler.AlterProviderInstance) provider.DELETE("/:provider_name/instances", r.providerHandler.DropProviderInstance) provider.GET("/:provider_name/instances/:instance_name/models", r.providerHandler.ListInstanceModels) diff --git a/internal/service/model_service.go b/internal/service/model_service.go index bb98a9e744..1eb71a1432 100644 --- a/internal/service/model_service.go +++ b/internal/service/model_service.go @@ -474,23 +474,58 @@ func (m *ModelProviderService) ShowInstanceBalance(providerName, instanceName, u return nil, common.CodeServerError, err } return result, common.CodeSuccess, nil +} - // convert instance.Extra (json string) to map - //var extra map[string]string - //err = json.Unmarshal([]byte(instance.Extra), &extra) - //if err != nil { - // return nil, common.CodeServerError, err - //} - // - //result := map[string]interface{}{ - // "id": instance.ID, - // "instanceName": instance.InstanceName, - // "providerID": instance.ProviderID, - // "status": instance.Status, - // "region": extra["region"], - //} - // - //return result, common.CodeSuccess, nil +func (m *ModelProviderService) CheckProviderConnection(providerName, instanceName, userID string) (common.ErrorCode, error) { + + // Get tenant ID from user + tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner") + if err != nil { + return common.CodeServerError, err + } + + if len(tenants) == 0 { + return common.CodeNotFound, errors.New("user has no tenants") + } + + tenantID := tenants[0].TenantID + + // Check if provider exists + provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName) + if err != nil { + return common.CodeServerError, err + } + + instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, instanceName) + if err != nil { + return common.CodeServerError, err + } + + providerInfo := dao.GetModelProviderManager().FindProvider(providerName) + if providerInfo == nil { + return common.CodeServerError, fmt.Errorf("provider %s not found", providerName) + } + + var extra map[string]string + err = json.Unmarshal([]byte(instance.Extra), &extra) + if err != nil { + return common.CodeServerError, err + } + + apiConfig := &modelModule.APIConfig{ + ApiKey: nil, + Region: nil, + } + + region := extra["region"] + apiConfig.Region = ®ion + apiConfig.ApiKey = &instance.APIKey + + err = providerInfo.ModelDriver.CheckConnection(apiConfig) + if err != nil { + return common.CodeServerError, err + } + return common.CodeSuccess, nil } func (m *ModelProviderService) AlterProviderInstance(providerName, instanceName, newInstanceName, apiKey, userID string) (common.ErrorCode, error) {