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)
This commit is contained in:
Haruko386
2026-05-12 18:03:05 +08:00
committed by GitHub
parent 7d3836907a
commit 45ee5ca9cd
3 changed files with 354 additions and 0 deletions

100
conf/models/jina.json Normal file
View File

@ -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"
]
}
]
}

View File

@ -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
}

View File

@ -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
}