diff --git a/conf/models/cohere.json b/conf/models/cohere.json new file mode 100644 index 0000000000..8b5ef93ff7 --- /dev/null +++ b/conf/models/cohere.json @@ -0,0 +1,43 @@ +{ + "name": "CoHere", + "url": { + "default": "https://api.cohere.com" + }, + "url_suffix": { + "chat": "v2/chat", + "models": "v1/models", + "embeddings": "v2/embed", + "rerank": "v2/rerank" + }, + "class": "cohere", + "models": [ + { + "name": "command-a-03-2025", + "max_tokens": 256000, + "model_types": [ + "chat" + ] + }, + { + "name": "command-a-reasoning-08-2025", + "max_tokens": 256000, + "model_types": [ + "chat" + ] + }, + { + "name": "rerank-v4.0-pro", + "max_tokens": 128000, + "model_types": [ + "rerank" + ] + }, + { + "name": "embed-v4.0", + "max_tokens": 8192, + "model_types": [ + "embedding" + ] + } + ] +} \ No newline at end of file diff --git a/conf/models/fishaudio.json b/conf/models/fishaudio.json new file mode 100644 index 0000000000..585aab3369 --- /dev/null +++ b/conf/models/fishaudio.json @@ -0,0 +1,14 @@ +{ + "name": "FishAudio", + "url": { + "default": "https://api.fish.audio" + }, + "url_suffix": { + "models": "model", + "balance": "self/package" + }, + "class": "fishaudio", + "models": [ + + ] +} \ No newline at end of file diff --git a/conf/models/nvidia.json b/conf/models/nvidia.json index 9f2f9a415d..b711b76145 100644 --- a/conf/models/nvidia.json +++ b/conf/models/nvidia.json @@ -18,13 +18,6 @@ "chat" ] }, - { - "name": "baai/bge-m3", - "max_tokens": 8192, - "model_types": [ - "embedding" - ] - }, { "name": "bytedance/seed-oss-36b-instruct", "max_tokens": 32768, @@ -47,26 +40,11 @@ ] }, { - "name": "deepseek-ai/deepseek-v3.2", - "max_tokens": 131072, + "name": "nvidia/nv-embed-v1", + "max_tokens": 8192, "model_types": [ - "chat" - ], - "thinking": { - "default_value": true, - "clear_thinking": true - } - }, - { - "name": "deepseek-ai/deepseek-v3.1", - "max_tokens": 131072, - "model_types": [ - "chat" - ], - "thinking": { - "default_value": true, - "clear_thinking": true - } + "embedding" + ] }, { "name": "google/codegemma-7b", @@ -89,27 +67,6 @@ "chat" ] }, - { - "name": "google/gemma-7b", - "max_tokens": 8192, - "model_types": [ - "chat" - ] - }, - { - "name": "ibm/granite-3.3-8b-instruct", - "max_tokens": 131072, - "model_types": [ - "chat" - ] - }, - { - "name": "meta/llama-3.1-405b-instruct", - "max_tokens": 131072, - "model_types": [ - "chat" - ] - }, { "name": "meta/llama-3.2-90b-vision-instruct", "max_tokens": 131072, @@ -125,24 +82,6 @@ "chat" ] }, - { - "name": "microsoft/phi-4-mini-flash-reasoning", - "max_tokens": 131072, - "model_types": [ - "chat" - ], - "thinking": { - "default_value": true, - "clear_thinking": true - } - }, - { - "name": "minimaxai/minimax-m2.1", - "max_tokens": 204800, - "model_types": [ - "chat" - ] - }, { "name": "minimaxai/minimax-m2.5", "max_tokens": 204800, @@ -157,20 +96,6 @@ "chat" ] }, - { - "name": "mistralai/devstral-2-123b-instruct-2512", - "max_tokens": 131072, - "model_types": [ - "chat" - ] - }, - { - "name": "mistralai/magistral-small-2506", - "max_tokens": 131072, - "model_types": [ - "chat" - ] - }, { "name": "mistralai/mistral-7b-instruct-v0.3", "max_tokens": 32768, @@ -186,7 +111,7 @@ ] }, { - "name": "mistralai/mistral-medium-3-5-128b", + "name": "mistralai/mistral-medium-3.5-128b", "max_tokens": 131072, "model_types": [ "chat", @@ -200,24 +125,6 @@ "chat" ] }, - { - "name": "mistralai/mixtral-8x22b-instruct", - "max_tokens": 65536, - "model_types": [ - "chat" - ] - }, - { - "name": "moonshotai/kimi-k2.5", - "max_tokens": 262144, - "model_types": [ - "chat" - ], - "thinking": { - "default_value": true, - "clear_thinking": true - } - }, { "name": "moonshotai/kimi-k2.6", "max_tokens": 262144, @@ -233,13 +140,6 @@ "chat" ] }, - { - "name": "moonshotai/kimi-k2-instruct-0905", - "max_tokens": 131072, - "model_types": [ - "chat" - ] - }, { "name": "moonshotai/kimi-k2-thinking", "max_tokens": 131072, @@ -304,13 +204,6 @@ "embedding" ] }, - { - "name": "nvidia/llama-3.2-nv-embedqa-1b-v2", - "max_tokens": 8192, - "model_types": [ - "embedding" - ] - }, { "name": "nvidia/llama-3.3-nemotron-super-49b-v1", "max_tokens": 131072, @@ -329,13 +222,6 @@ "clear_thinking": true } }, - { - "name": "nvidia/nemoguard-jailbreak-detect", - "max_tokens": 4096, - "model_types": [ - "chat" - ] - }, { "name": "nvidia/nemotron-3-nano-30b-a3b", "max_tokens": 131072, @@ -419,19 +305,12 @@ ] }, { - "name": "nvidia/riva-translate-4b-instruct-v1_1", + "name": "nvidia/riva-translate-4b-instruct-v1.1", "max_tokens": 4096, "model_types": [ "chat" ] }, - { - "name": "nvidia/usdcode", - "max_tokens": 8192, - "model_types": [ - "chat" - ] - }, { "name": "openai/gpt-oss-120b", "max_tokens": 131072, @@ -440,30 +319,12 @@ ] }, { - "name": "qwen/qwen2.5-coder-7b-instruct", - "max_tokens": 32768, - "model_types": [ - "chat" - ] - }, - { - "name": "qwen/qwen3-5-122b-a10b", + "name": "qwen/qwen3.5-122b-a10b", "max_tokens": 131072, "model_types": [ "chat" ] }, - { - "name": "qwen/qwen3-235b-a22b", - "max_tokens": 131072, - "model_types": [ - "chat" - ], - "thinking": { - "default_value": true, - "clear_thinking": true - } - }, { "name": "qwen/qwen3-coder-480b-a35b-instruct", "max_tokens": 262144, @@ -476,14 +337,7 @@ } }, { - "name": "snowflake/arctic-embed-l", - "max_tokens": 512, - "model_types": [ - "embedding" - ] - }, - { - "name": "z-ai/glm-5", + "name": "z-ai/glm5", "max_tokens": 131072, "model_types": [ "chat" @@ -505,7 +359,7 @@ } }, { - "name": "z-ai/glm-4.7", + "name": "z-ai/glm4.7", "max_tokens": 131072, "model_types": [ "chat" diff --git a/conf/models/volcengine.json b/conf/models/volcengine.json index 326b407d0c..8253549370 100644 --- a/conf/models/volcengine.json +++ b/conf/models/volcengine.json @@ -6,8 +6,7 @@ "url_suffix": { "chat": "chat/completions", "files": "files", - "embedding": "embeddings/multimodal", - "models": "models" + "embedding": "embeddings/multimodal" }, "class": "volcengine", "models": [ @@ -23,7 +22,7 @@ } }, { - "name": "doubao-embedding-vision-250615", + "name": "doubao-embedding-vision-251215", "max_tokens": 131072, "model_types": [ "embedding" diff --git a/internal/entity/models/cohere.go b/internal/entity/models/cohere.go new file mode 100644 index 0000000000..6a653ec7cc --- /dev/null +++ b/internal/entity/models/cohere.go @@ -0,0 +1,561 @@ +package models + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +type CoHereModel struct { + BaseURL map[string]string + URLSuffix URLSuffix + httpClient *http.Client +} + +func (c *CoHereModel) NewInstance(baseURL map[string]string) ModelDriver { + return &CoHereModel{ + BaseURL: baseURL, + URLSuffix: c.URLSuffix, + httpClient: &http.Client{ + Timeout: 120 * time.Second, + }, + } +} + +func NewCoHereModel(baseURL map[string]string, urlSuffix URLSuffix) *CoHereModel { + return &CoHereModel{ + BaseURL: baseURL, + URLSuffix: urlSuffix, + httpClient: &http.Client{ + Timeout: 120 * time.Second, + }, + } +} + +func (c *CoHereModel) Name() string { + return "cohere" +} + +func (c *CoHereModel) 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("messages is empty") + } + + var region = "default" + if apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", c.BaseURL[region], c.URLSuffix.Chat) + + // 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, + } + } + + // Build request body + reqBody := map[string]interface{}{ + "model": modelName, + "messages": apiMessages, + "stream": false, + "temperature": 0.3, + } + + if chatModelConfig != nil { + if chatModelConfig.Stream != nil { + reqBody["stream"] = *chatModelConfig.Stream + } + + if chatModelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *chatModelConfig.MaxTokens + } + + if chatModelConfig.Temperature != nil { + reqBody["temperature"] = *chatModelConfig.Temperature + } + + if chatModelConfig.TopP != nil { + reqBody["top_p"] = *chatModelConfig.TopP + } + + if chatModelConfig.Thinking != nil { + if *chatModelConfig.Thinking { + reqBody["thinking"] = map[string]interface{}{ + "type": "enabled", + } + } else { + reqBody["thinking"] = map[string]interface{}{ + "type": "disabled", + } + } + } + } + + 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("accept", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("bearer %s", strings.TrimSpace(*apiConfig.ApiKey))) + + resp, err := c.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("Cohere chat API error: %d %s", resp.StatusCode, string(body)) + } + + // Parse response + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + messageMap, ok := result["message"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("no message found in Cohere response: %s", string(body)) + } + + contentArray, ok := messageMap["content"].([]interface{}) + if !ok { + return nil, fmt.Errorf("content is not an array in Cohere response") + } + + var fullContent string + var reasonContent string + for _, cBlock := range contentArray { + cmap, ok := cBlock.(map[string]interface{}) + if !ok { + continue + } + if blockType, ok := cmap["type"].(string); ok && blockType == "thinking" { + if thinkingText, ok := cmap["thinking"].(string); ok { + reasonContent += thinkingText + } + } else if text, ok := cmap["text"].(string); ok { + fullContent += text + } + } + + chatResponse := &ChatResponse{ + Answer: &fullContent, + ReasonContent: &reasonContent, + } + + return chatResponse, nil +} + +func (c *CoHereModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { + if len(messages) == 0 { + return fmt.Errorf("messages is empty") + } + + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", c.BaseURL[region], c.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 modelConfig != nil { + if modelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *modelConfig.MaxTokens + } + if modelConfig.Temperature != nil { + reqBody["temperature"] = *modelConfig.Temperature + } + if modelConfig.TopP != nil { + reqBody["p"] = *modelConfig.TopP + } + } + + if modelConfig != nil { + if modelConfig.Stream != nil { + reqBody["stream"] = *modelConfig.Stream + } + + if modelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *modelConfig.MaxTokens + } + + if modelConfig.Temperature != nil { + reqBody["temperature"] = *modelConfig.Temperature + } + + if modelConfig.TopP != nil { + reqBody["top_p"] = *modelConfig.TopP + } + + if modelConfig.Thinking != nil { + if *modelConfig.Thinking { + reqBody["thinking"] = map[string]interface{}{ + "type": "enabled", + } + } else { + reqBody["thinking"] = map[string]interface{}{ + "type": "disabled", + } + } + } + } + + 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("accept", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(*apiConfig.ApiKey))) + + resp, err := c.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("Cohere stream API error %d: %s", resp.StatusCode, string(body)) + } + + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Text() + data := strings.TrimSpace(line) + + if strings.HasPrefix(data, "data:") { + data = strings.TrimSpace(data[5:]) + } + + if data == "" || data == "[DONE]" { + continue + } + + var event map[string]interface{} + if err = json.Unmarshal([]byte(data), &event); err != nil { + continue + } + eventType, ok := event["type"].(string) + if !ok { + continue + } + + if eventType == "message-end" { + break + } + + if eventType == "content-delta" { + delta, ok := event["delta"].(map[string]interface{}) + if !ok { + continue + } + msg, ok := delta["message"].(map[string]interface{}) + if !ok { + continue + } + content, ok := msg["content"].(map[string]interface{}) + if !ok { + continue + } + + if thinking, ok := content["thinking"].(string); ok && thinking != "" { + if err := sender(nil, &thinking); err != nil { + return err + } + } + + if text, ok := content["text"].(string); ok && text != "" { + if err := sender(&text, nil); err != nil { + return err + } + } + } + } + + endOfStream := "[DONE]" + if err = sender(&endOfStream, nil); err != nil { + return err + } + + return scanner.Err() +} + +func (c *CoHereModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { + if len(texts) == 0 { + return []EmbeddingData{}, nil + } + + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + baseURL := strings.TrimSuffix(c.BaseURL[region], "/") + suffix := strings.TrimPrefix(c.URLSuffix.Embedding, "/") + if suffix == "" { + suffix = "v2/embed" + } + url := fmt.Sprintf("%s/%s", baseURL, suffix) + + reqBody := map[string]interface{}{ + "model": *modelName, + "texts": texts, + "input_type": "search_document", + "embedding_types": []string{"float"}, + } + + 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("Accept", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(*apiConfig.ApiKey))) + + resp, err := c.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("Cohere embedding API error: status %d, body: %s", resp.StatusCode, string(body)) + } + + var result struct { + Embeddings struct { + Float [][]float64 `json:"float"` + } `json:"embeddings"` + } + if err = json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + if len(result.Embeddings.Float) == 0 { + return nil, fmt.Errorf("Cohere embedding response contains no float data: %s", string(body)) + } + + var embeddings []EmbeddingData + for i, floatArr := range result.Embeddings.Float { + embeddings = append(embeddings, EmbeddingData{ + Embedding: floatArr, + Index: i, + }) + } + + return embeddings, nil +} + +func (c *CoHereModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { + if len(documents) == 0 { + return &RerankResponse{}, nil + } + + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + baseURL := strings.TrimSuffix(c.BaseURL[region], "/") + suffix := strings.TrimPrefix(c.URLSuffix.Rerank, "/") + if suffix == "" { + suffix = "v2/rerank" + } + url := fmt.Sprintf("%s/%s", baseURL, suffix) + + var topN = rerankConfig.TopN + if rerankConfig.TopN == 0 { + topN = len(documents) + } + + reqBody := map[string]interface{}{ + "model": *modelName, + "query": query, + "documents": documents, + "top_n": topN, + } + + 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("Accept", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(*apiConfig.ApiKey))) + + resp, err := c.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("Cohere rerank API error: status %d, body: %s", resp.StatusCode, string(body)) + } + + var rerankResp struct { + Results []struct { + Index int `json:"index"` + RelevanceScore float64 `json:"relevance_score"` + } `json:"results"` + } + + if err := json.Unmarshal(body, &rerankResp); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + var rerankResponse RerankResponse + for _, result := range rerankResp.Results { + rerankResult := RerankResult{ + Index: result.Index, + RelevanceScore: result.RelevanceScore, + } + rerankResponse.Data = append(rerankResponse.Data, rerankResult) + } + + return &rerankResponse, nil +} + +func (c *CoHereModel) ListModels(apiConfig *APIConfig) ([]string, error) { + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + baseURL := c.BaseURL[region] + if baseURL == "" { + baseURL = c.BaseURL["default"] + } + if baseURL == "" { + baseURL = "https://api.cohere.com" + } + suffix := c.URLSuffix.Models + if suffix == "" { + suffix = "v1/models" + } + url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), strings.TrimPrefix(suffix, "/")) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("accept", "application/json") + if apiConfig != nil && apiConfig.ApiKey != nil { + req.Header.Set("Authorization", fmt.Sprintf("bearer %s", strings.TrimSpace(*apiConfig.ApiKey))) + } + + resp, err := c.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("Cohere 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) + } + + models := make([]string, 0) + if modelsRaw, ok := result["models"].([]interface{}); ok { + for _, model := range modelsRaw { + if modelMap, ok := model.(map[string]interface{}); ok { + if modelName, ok := modelMap["name"].(string); ok { + models = append(models, modelName) + } + } + } + } else { + return nil, fmt.Errorf("failed to find 'models' array in response") + } + + return models, nil +} + +func (c *CoHereModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { + return nil, fmt.Errorf(c.Name() + " no such method") +} + +func (c *CoHereModel) CheckConnection(apiConfig *APIConfig) error { + _, err := c.ListModels(apiConfig) + return err +} diff --git a/internal/entity/models/factory.go b/internal/entity/models/factory.go index 1c0de11c65..d68b7a85f3 100644 --- a/internal/entity/models/factory.go +++ b/internal/entity/models/factory.go @@ -69,6 +69,10 @@ func (f *ModelFactory) CreateModelDriver(providerName string, baseURL map[string return NewHuggingFaceModel(baseURL, urlSuffix), nil case "baidu": return NewBaiduModel(baseURL, urlSuffix), nil + case "cohere": + return NewCoHereModel(baseURL, urlSuffix), nil + case "fishaudio": + return NewFishAudioModel(baseURL, urlSuffix), nil default: return NewDummyModel(baseURL, urlSuffix), nil } diff --git a/internal/entity/models/fishaudio.go b/internal/entity/models/fishaudio.go new file mode 100644 index 0000000000..c618ef7790 --- /dev/null +++ b/internal/entity/models/fishaudio.go @@ -0,0 +1,157 @@ +package models + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// 208cc2d0e4594ca896a600c43c9497aa + +type FishAudioModel struct { + BaseURL map[string]string + URLSuffix URLSuffix + httpClient *http.Client +} + +func NewFishAudioModel(baseURL map[string]string, urlSuffix URLSuffix) *FishAudioModel { + return &FishAudioModel{ + BaseURL: baseURL, + URLSuffix: urlSuffix, + httpClient: &http.Client{ + Timeout: 120 * time.Second, + }, + } +} +func (f *FishAudioModel) NewInstance(baseURL map[string]string) ModelDriver { + return &FishAudioModel{ + BaseURL: baseURL, + URLSuffix: f.URLSuffix, + httpClient: &http.Client{ + Timeout: 120 * time.Second, + }, + } +} + +func (f *FishAudioModel) Name() string { + return "fishaudio" +} + +func (f *FishAudioModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { + return nil, fmt.Errorf(f.Name() + " no such method") +} + +func (f *FishAudioModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { + return fmt.Errorf(f.Name() + " no such method") +} + +func (f *FishAudioModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { + return nil, fmt.Errorf("no such method") +} + +func (f *FishAudioModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { + return nil, fmt.Errorf("no such method") +} +func (f *FishAudioModel) ListModels(apiConfig *APIConfig) ([]string, error) { + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", f.BaseURL[region], f.URLSuffix.Models) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + if apiConfig != nil && apiConfig.ApiKey != nil && *apiConfig.ApiKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + } else { + return nil, fmt.Errorf("Fish Audio API key is missing") + } + + resp, err := f.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("Fish Audio API request failed with status %d: %s", resp.StatusCode, string(body)) + } + + var result struct { + Items []struct { + ID string `json:"_id"` + Title string `json:"title"` + } `json:"items"` + } + + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + models := make([]string, 0, len(result.Items)) + for _, item := range result.Items { + models = append(models, item.Title) + } + + return models, nil +} + +func (f *FishAudioModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + baseURL := f.BaseURL[region] + if baseURL == "" { + baseURL = f.BaseURL["default"] + } + + url := fmt.Sprintf("%s/wallet/self/api-credit", strings.TrimSuffix(baseURL, "/")) + + 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 := f.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("Fish Audio balance API error: status %d, body: %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) + } + + return result, nil +} + +func (f *FishAudioModel) CheckConnection(apiConfig *APIConfig) error { + _, err := f.ListModels(apiConfig) + return err +}