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:
Panda Dev
2026-05-18 06:02:28 +02:00
committed by GitHub
parent e98f3e5c0d
commit 6794ad2f70
2 changed files with 113 additions and 3 deletions

View File

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

View File

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