From 45ee5ca9cd0d4e7ac042a7c235eeeb238b61b13c Mon Sep 17 00:00:00 2001 From: Haruko386 Date: Tue, 12 May 2026 18:03:05 +0800 Subject: [PATCH] Go: implement provider: Jina (#14838) ### What problem does this PR solve? This PR completes the Jina provider **The following functionalities are now supported:** **Jina:** - [ ] Chat / Stream Chat (Not available for now: [(Jina chat API docs)](https://api.jina.ai/docs#/Search%20Foundation%20Models/chat_completions_v1_chat_completions_post)) - [x] Embedding - [x] Rerank - [x] Model listing - [x] Provider connection checking - [ ] ~~Balance~~ **Verified examples from the CLI:** ```plaintext RAGFlow(user)> embed text 'walkerwhat' 'jumperwho' with 'jina-embeddings-v2-base-en@test@jina' dimension 16 +-----------+-------+ | dimension | index | +-----------+-------+ | 768 | 0 | | 768 | 1 | +-----------+-------+ RAGFlow(user)> rerank query 'what is rag' document 'rag is retrieval augment generation' 'rag need llm' 'famous rag project includes ragflow' with 'jina-reranker-v2-base-multilingual@test@jina' top 3; +-------+-----------------+ | index | relevance_score | +-------+-----------------+ | 0 | 0.74316794 | | 2 | 0.18713269 | | 1 | 0.15817434 | +-------+-----------------+ RAGFlow(user)> list supported models from 'jina' 'test' +---------------------------------------------+ | model_name | +---------------------------------------------+ | Jina AI: Jina VLM | | Jina AI: Jina Reranker v3 | | Jina AI: Jina Code Embeddings 0.5b | | Jina AI: Jina Code Embeddings 1.5b | | Jina AI: Jina Embeddings v4 | | Jina AI: Jina Reranker M0 | | Jina AI: ReaderLM v2 | | Jina AI: Jina Clip v2 | | Jina AI: Jina Embeddings v3 | | Jina AI: Jina Colbert v2 | | Jina AI: Reader LM 0.5b | | Jina AI: Reader LM 1.5b | | Jina AI: Jina Reranker v2 Base Multilingual | | Jina AI: Jina Clip v1 | | Jina AI: Jina Reranker v1 Tiny EN | | Jina AI: Jina Reranker v1 Turbo EN | | Jina AI: Jina Reranker v1 Base EN | | Jina AI: Jina Colbert v1 EN | | Jina AI: Jina Embeddings v2 Base ES | | Jina AI: Jina Embeddings v2 Base Code | | Jina AI: Jina Embeddings v2 Base DE | | Jina AI: Jina Embeddings v2 Base ZH | | Jina AI: Jina Embeddings v2 Base EN | | Jina AI: Jina Embedding B EN v1 | | Jina AI: Jina Embeddings v5 Text Small | | Jina AI: Jina Embeddings v5 Omni Small | | Jina AI: Jina Embeddings v5 Omni Nano | | Jina AI: Jina Embeddings v5 Text Nano | +---------------------------------------------+ RAGFlow(user)> check instance 'test' from 'jina' SUCCESS ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- conf/models/jina.json | 100 ++++++++++++ internal/entity/models/factory.go | 2 + internal/entity/models/jina.go | 252 ++++++++++++++++++++++++++++++ 3 files changed, 354 insertions(+) create mode 100644 conf/models/jina.json create mode 100644 internal/entity/models/jina.go diff --git a/conf/models/jina.json b/conf/models/jina.json new file mode 100644 index 000000000..07463b6ed --- /dev/null +++ b/conf/models/jina.json @@ -0,0 +1,100 @@ +{ + "name": "Jina", + "url": { + "default": "https://api.jina.ai/v1", + "deepsearch": "https://deepsearch.jina.ai/v1" + }, + "url_suffix": { + "chat": "chat/completions", + "models": "models", + "embedding": "embeddings", + "rerank": "rerank" + }, + "class": "jina", + "models": [ + { + "name": "jina-reranker-v3", + "max_tokens": 134144, + "model_types": [ + "rerank" + ] + }, + { + "name": "jina-reranker-m0", + "max_tokens": 134144, + "model_types": [ + "rerank" + ] + }, + { + "name": "jina-colbert-v2", + "max_tokens": 134144, + "model_types": [ + "rerank" + ] + }, + { + "name": "jina-reranker-v2-base-multilingual", + "max_tokens": 134144, + "model_types": [ + "rerank" + ] + }, + { + "name": "jina-embeddings-v3", + "max_tokens": 8192, + "model_types": [ + "embedding" + ] + }, + { + "name": "jina-embeddings-v4", + "max_tokens": 32768, + "model_types": [ + "embedding" + ] + }, + { + "name": "jina-embeddings-v5-text-small", + "max_tokens": 32768, + "model_types": [ + "embedding" + ] + }, + { + "name": "jina-embeddings-v5-text-nano", + "max_tokens": 8192, + "model_types": [ + "embedding" + ] + }, + { + "name": "jina-embeddings-v5-omni-small", + "max_tokens": 32768, + "model_types": [ + "embedding" + ] + }, + { + "name": "jina-embeddings-v5-omni-nano", + "max_tokens": 8192, + "model_types": [ + "embedding" + ] + }, + { + "name": "jina-clip-v2", + "max_tokens": 8192, + "model_types": [ + "embedding" + ] + }, + { + "name": "jina-embeddings-v2-base-en", + "max_tokens": 8192, + "model_types": [ + "embedding" + ] + } + ] +} \ No newline at end of file diff --git a/internal/entity/models/factory.go b/internal/entity/models/factory.go index c11e47964..7540605d3 100644 --- a/internal/entity/models/factory.go +++ b/internal/entity/models/factory.go @@ -81,6 +81,8 @@ func (f *ModelFactory) CreateModelDriver(providerName string, baseURL map[string return NewStepFunModel(baseURL, urlSuffix), nil case "baichuan": return NewBaichuanModel(baseURL, urlSuffix), nil + case "jina": + return NewJinaModel(baseURL, urlSuffix), nil default: return NewDummyModel(baseURL, urlSuffix), nil } diff --git a/internal/entity/models/jina.go b/internal/entity/models/jina.go new file mode 100644 index 000000000..1a3d2ff9f --- /dev/null +++ b/internal/entity/models/jina.go @@ -0,0 +1,252 @@ +package models + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type JinaModel struct { + BaseURL map[string]string + URLSuffix URLSuffix + httpClient *http.Client +} + +func NewJinaModel(baseURL map[string]string, urlSuffix URLSuffix) *JinaModel { + return &JinaModel{ + BaseURL: baseURL, + URLSuffix: urlSuffix, + httpClient: &http.Client{ + Timeout: time.Second * 90, + }, + } +} + +func (j *JinaModel) NewInstance(baseURL map[string]string) ModelDriver { + return &JinaModel{ + BaseURL: baseURL, + URLSuffix: j.URLSuffix, + httpClient: &http.Client{ + Timeout: time.Second * 90, + }, + } +} + +func (j *JinaModel) Name() string { + return "jina" +} + +func (j *JinaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { + //TODO implement me: https://api.jina.ai/docs#/Search%20Foundation%20Models/chat_completions_v1_chat_completions_post + return nil, fmt.Errorf("jina does not implement ChatWithMessages(not available for now)") +} + +func (j *JinaModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { + //TODO implement me: https://api.jina.ai/docs#/Search%20Foundation%20Models/chat_completions_v1_chat_completions_post + return fmt.Errorf("jina does not implement ChatStreamlyWithSender(not available for now)") +} + +func (j *JinaModel) 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 + } + + url := fmt.Sprintf("%s/%s", j.BaseURL[region], j.URLSuffix.Embedding) + + reqBody := map[string]interface{}{ + "model": *modelName, + "input": texts, + } + + 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 := j.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("Jina embedding API error: status %d, body: %s", resp.StatusCode, string(body)) + } + + var parsedResponse struct { + Data []struct { + Embedding []float64 `json:"embedding"` + Index int `json:"index"` + } `json:"data"` + } + + if err = json.Unmarshal(body, &parsedResponse); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + if len(parsedResponse.Data) == 0 { + return nil, fmt.Errorf("Jina embedding response contains no data: %s", string(body)) + } + + var embeddings []EmbeddingData + for _, dataElem := range parsedResponse.Data { + embeddings = append(embeddings, EmbeddingData{ + Embedding: dataElem.Embedding, + Index: dataElem.Index, + }) + } + + return embeddings, nil +} + +func (j *JinaModel) 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 + } + + url := fmt.Sprintf("%s/%s", j.BaseURL[region], j.URLSuffix.Rerank) + + var topN = rerankConfig.TopN + if rerankConfig.TopN != 0 { + topN = rerankConfig.TopN + } + + 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("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + + resp, err := j.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("Jina 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 (j *JinaModel) ListModels(apiConfig *APIConfig) ([]string, error) { + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", j.BaseURL[region], j.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("Content-Type", "application/json") + + resp, err := j.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)) + } + + // Parse response + var result map[string]interface{} + if err = json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + // convert result["data"] to []map[string]interface{} + models := make([]string, 0) + for _, model := range result["data"].([]interface{}) { + modelMap := model.(map[string]interface{}) + modelName := modelMap["name"].(string) + models = append(models, modelName) + } + + return models, nil +} + +func (j *JinaModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { + return nil, fmt.Errorf("no such method") +} + +func (j *JinaModel) CheckConnection(apiConfig *APIConfig) error { + _, err := j.ListModels(apiConfig) + return err +}