From dc01e0e51c20391ce8023806bd4fada2b011d20c Mon Sep 17 00:00:00 2001 From: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com> Date: Wed, 20 May 2026 08:48:44 -0400 Subject: [PATCH] Go: implement Embed (embeddings) in TogetherAI driver (#15017) ### What problem does this PR solve? Fixes #15015 The TogetherAI Go driver in `internal/entity/models/togetherai.go` shipped a stub `Embed` method that returned `"TogetherAI, no such method"`, so TogetherAI could not be used as an embedding provider in RAGFlow. This PR fills that gap. TogetherAI exposes a public OpenAI-compatible embeddings endpoint at `POST https://api.together.ai/v1/embeddings` that accepts the standard `{model, input}` shape with `Authorization: Bearer ` (confirmed in TogetherAI's official docs: https://docs.together.ai/docs/embeddings-overview). Documented embedding models include `intfloat/multilingual-e5-large-instruct`, `BAAI/bge-large-en-v1.5`, and `BAAI/bge-base-en-v1.5`. ### Changes - `internal/entity/models/togetherai.go`: implement `TogetherAIModel.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`. - 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, Upstage, and Novita Embed implementations. - `conf/models/togetherai.json`: - Add `"embedding": "embeddings"` to `url_suffix`. - Add default embedding model entries for `intfloat/multilingual-e5-large-instruct`, `BAAI/bge-large-en-v1.5`, and `BAAI/bge-base-en-v1.5`. ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- conf/models/togetherai.json | 24 ++++++- internal/entity/models/togetherai.go | 102 ++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/conf/models/togetherai.json b/conf/models/togetherai.json index f8660e059..0608641a6 100644 --- a/conf/models/togetherai.json +++ b/conf/models/togetherai.json @@ -5,7 +5,8 @@ }, "url_suffix": { "chat": "chat/completions", - "models": "models" + "models": "models", + "embedding": "embeddings" }, "class": "together", "models": [ @@ -29,6 +30,27 @@ "model_types": [ "chat" ] + }, + { + "name": "intfloat/multilingual-e5-large-instruct", + "max_tokens": 514, + "model_types": [ + "embedding" + ] + }, + { + "name": "BAAI/bge-large-en-v1.5", + "max_tokens": 512, + "model_types": [ + "embedding" + ] + }, + { + "name": "BAAI/bge-base-en-v1.5", + "max_tokens": 512, + "model_types": [ + "embedding" + ] } ] } diff --git a/internal/entity/models/togetherai.go b/internal/entity/models/togetherai.go index 7e9219180..6aea12c20 100644 --- a/internal/entity/models/togetherai.go +++ b/internal/entity/models/togetherai.go @@ -385,8 +385,108 @@ func (t *TogetherAIModel) CheckConnection(apiConfig *APIConfig) error { return err } +type togetherAIEmbeddingData struct { + Embedding []float64 `json:"embedding"` + Object string `json:"object"` + Index int `json:"index"` +} + +type togetherAIEmbeddingResponse struct { + Data []togetherAIEmbeddingData `json:"data"` + Model string `json:"model"` + Object string `json:"object"` +} + func (t *TogetherAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { - return nil, fmt.Errorf("%s, no such method", t.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 || strings.TrimSpace(*modelName) == "" { + return nil, fmt.Errorf("model name is required") + } + + region := "default" + if apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + baseURL, err := t.baseURLForRegion(region) + if err != nil { + return nil, err + } + url := fmt.Sprintf("%s/%s", baseURL, t.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 := t.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("TogetherAI embeddings API error: %s, body: %s", resp.Status, string(body)) + } + + var parsed togetherAIEmbeddingResponse + 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("togetherai: response index %d out of range for %d inputs", item.Index, len(texts)) + } + if filled[item.Index] { + return nil, fmt.Errorf("togetherai: 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("togetherai: missing embedding for input index %d", i) + } + } + + return embeddings, nil } func (t *TogetherAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) {