mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-16 12:47:19 +08:00
Go: implement Embed (embeddings) in Novita driver (#14895)
### What problem does this PR solve? Fixes #14893 The Novita Go driver landed in #14850 and shipped a stub `Embed` method that returned `"novita, no such method"`, so Novita could not be used as an embedding provider in RAGFlow. This PR fills that gap. Novita exposes a public embeddings endpoint at `POST https://api.novita.ai/v3/embeddings` that accepts the standard OpenAI-compatible request shape (`{model, input}`) with `Authorization: Bearer <api_key>`. Two embedding models are documented in Novita's model library: `baai/bge-m3` (multilingual, 8192 tokens) and `baai/bge-large-en-v1.5`. ### Changes - `internal/entity/models/novita.go`: implement `NovitaModel.Embed`. - Validate inputs (api key, model name) and short-circuit on empty texts. - Resolve region with the existing `baseURLForRegion` helper. - Build URL from `URLSuffix.Embedding` (the embeddings path lives under `/v3/`, separate from the chat path under `/openai/v1/`). - Send `{model, input}` POST body, add `dimensions` when `embeddingConfig.Dimension > 0` (matches the pattern in #14735). - Bearer auth + JSON content type, mirroring the chat path. - Parse `{data: [{embedding, index}]}` and reorder by `index`, rejecting out-of-range indices, duplicates, and missing entries so the output always lines up with the input. Same shape as the merged Mistral and Upstage Embed implementations. - `conf/models/novita.json`: - Add `"embedding": "v3/embeddings"` to `url_suffix`. - Add default embedding model entries for `baai/bge-m3` and `baai/bge-large-en-v1.5` so they appear in the model picker. ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Co-authored-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
},
|
||||
"url_suffix": {
|
||||
"chat": "openai/v1/chat/completions",
|
||||
"models": "openai/v1/models"
|
||||
"models": "openai/v1/models",
|
||||
"embedding": "openai/v1/embeddings"
|
||||
},
|
||||
"class": "novita",
|
||||
"models": [
|
||||
@@ -57,6 +58,13 @@
|
||||
"model_types": [
|
||||
"chat"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "baai/bge-m3",
|
||||
"max_tokens": 8192,
|
||||
"model_types": [
|
||||
"embedding"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -626,9 +626,111 @@ func (n *NovitaModel) CheckConnection(apiConfig *APIConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Embed is not exposed on Novita's OpenAI-compatible surface yet.
|
||||
type novitaEmbeddingData struct {
|
||||
Embedding []float64 `json:"embedding"`
|
||||
Object string `json:"object"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
type novitaEmbeddingResponse struct {
|
||||
Data []novitaEmbeddingData `json:"data"`
|
||||
Model string `json:"model"`
|
||||
Object string `json:"object"`
|
||||
}
|
||||
|
||||
// Embed turns a list of texts into embedding vectors using the Novita
|
||||
// /v3/embeddings endpoint. The output has one vector per input, in the
|
||||
// same order the inputs were given.
|
||||
func (n *NovitaModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) {
|
||||
return nil, fmt.Errorf("%s, no such method", n.Name())
|
||||
if len(texts) == 0 {
|
||||
return []EmbeddingData{}, nil
|
||||
}
|
||||
|
||||
if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" {
|
||||
return nil, fmt.Errorf("api key is required")
|
||||
}
|
||||
|
||||
if modelName == nil || *modelName == "" {
|
||||
return nil, fmt.Errorf("model name is required")
|
||||
}
|
||||
|
||||
region := "default"
|
||||
if apiConfig.Region != nil && *apiConfig.Region != "" {
|
||||
region = *apiConfig.Region
|
||||
}
|
||||
|
||||
baseURL, err := n.baseURLForRegion(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s", baseURL, n.URLSuffix.Embedding)
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"model": *modelName,
|
||||
"input": texts,
|
||||
}
|
||||
if embeddingConfig != nil && embeddingConfig.Dimension > 0 {
|
||||
reqBody["dimensions"] = embeddingConfig.Dimension
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "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 := n.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("Novita embeddings API error: %s, body: %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
var parsed novitaEmbeddingResponse
|
||||
if err = json.Unmarshal(body, &parsed); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
embeddings := make([]EmbeddingData, len(texts))
|
||||
filled := make([]bool, len(texts))
|
||||
for _, item := range parsed.Data {
|
||||
if item.Index < 0 || item.Index >= len(texts) {
|
||||
return nil, fmt.Errorf("novita: response index %d out of range for %d inputs", item.Index, len(texts))
|
||||
}
|
||||
if filled[item.Index] {
|
||||
return nil, fmt.Errorf("novita: duplicate embedding index %d in response", item.Index)
|
||||
}
|
||||
embeddings[item.Index] = EmbeddingData{
|
||||
Embedding: item.Embedding,
|
||||
Index: item.Index,
|
||||
}
|
||||
filled[item.Index] = true
|
||||
}
|
||||
for i, ok := range filled {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("novita: missing embedding for input index %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
return embeddings, nil
|
||||
}
|
||||
|
||||
// Rerank is not exposed by the Novita API.
|
||||
|
||||
Reference in New Issue
Block a user