From a82ae4a991d1fd535f41b8eb5d2710e06458ecf5 Mon Sep 17 00:00:00 2001 From: Panda Dev <56657208+pandadev66@users.noreply.github.com> Date: Fri, 8 May 2026 07:58:25 +0200 Subject: [PATCH] Go: implement Encode (embeddings) in Aliyun driver (#14647) ### What problem does this PR solve? The Aliyun Go driver shipped with a stub \`Encode\` method that returned \`no such method\`, even though \`conf/models/aliyun.json\` already wires the OpenAI-compatible embeddings URL suffix at \`compatible-mode/v1/embeddings\`. The same config also did not list any embedding models, so the picker had nothing to select. So an Aliyun tenant who wanted to use Tongyi text-embedding-v3 or v4 in the Go layer could not, even though the upstream endpoint is public and uses the standard \`POST /v1/embeddings\` shape that the SiliconFlow and ZhipuAI drivers already support. This PR fills the gap. ### What this PR includes - \`conf/models/aliyun.json\`: add \`text-embedding-v4\` and \`text-embedding-v3\` to the \`models\` array. - \`internal/entity/models/aliyun.go\`: replace the \`Encode\` stub with a real implementation. Adds a small local response type that matches the OpenAI-compatible shape. No factory change. No interface change. ### How the driver works - Validate \`apiConfig\` and the API key, validate the model name, resolve the region with a default fallback, build the URL from \`BaseURL[region] + URLSuffix.Embedding\`. - Send all input texts in one request as the \`input\` array, the same OpenAI-compatible shape the SiliconFlow \`Encode\` uses. - Parse \`data[*].embedding\` and copy each slice into a \`[][]float64\` indexed by \`data[*].index\` so the output order matches the input order even if the API returns items in a different order. - Handle both \`float64\` and \`float32\` element types. - Empty input returns \`[][]float64{}\` with no HTTP call. - Non-200 responses propagate the upstream status line and body. - A final pass checks every input slot got a vector and returns a clear error if any slot is still nil. ### Type of change - [x] New Feature (non-breaking change which adds functionality) ### How was this tested? - \`go build ./internal/entity/models/...\` in a clean go 1.25 image returns exit 0. - The full method set on \`AliyunModel\` still matches the \`ModelDriver\` interface. - Pattern parity with the existing SiliconFlow Encode implementation. Closes #14646 --------- Co-authored-by: Jin Hai --- conf/models/aliyun.json | 14 +++++ internal/entity/models/aliyun.go | 105 ++++++++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/conf/models/aliyun.json b/conf/models/aliyun.json index b0cad72e4d..32a5221da4 100644 --- a/conf/models/aliyun.json +++ b/conf/models/aliyun.json @@ -17,6 +17,20 @@ "model_types": [ "chat" ] + }, + { + "name": "text-embedding-v4", + "max_tokens": 8192, + "model_types": [ + "embedding" + ] + }, + { + "name": "text-embedding-v3", + "max_tokens": 8192, + "model_types": [ + "embedding" + ] } ], "features": { diff --git a/internal/entity/models/aliyun.go b/internal/entity/models/aliyun.go index 2019f1db4b..c1cd7d5564 100644 --- a/internal/entity/models/aliyun.go +++ b/internal/entity/models/aliyun.go @@ -19,6 +19,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -350,9 +351,111 @@ func (z *AliyunModel) ChatStreamlyWithSender(modelName string, messages []Messag return scanner.Err() } +type aliyunEmbeddingResponse struct { + Data []struct { + Index int `json:"index"` + Embedding []interface{} `json:"embedding"` + } `json:"data"` +} + // Encode encodes a list of texts into embeddings func (z *AliyunModel) Encode(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([][]float64, error) { - return nil, fmt.Errorf("%s, no such method", z.Name()) + if len(texts) == 0 { + return [][]float64{}, 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 := z.BaseURL["default"] + if region != "default" { + if regional, ok := z.BaseURL[region]; ok && regional != "" { + baseURL = regional + } + } + if baseURL == "" { + return nil, fmt.Errorf("aliyun: no base URL configured for default region") + } + + url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), z.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) + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + 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 := z.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("Aliyun embeddings API error: %s, body: %s", resp.Status, string(body)) + } + + var parsed aliyunEmbeddingResponse + if err = json.Unmarshal(body, &parsed); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + embeddings := make([][]float64, len(texts)) + for _, item := range parsed.Data { + if item.Index < 0 || item.Index >= len(texts) { + return nil, fmt.Errorf("unexpected embedding index %d for %d inputs", item.Index, len(texts)) + } + vec := make([]float64, len(item.Embedding)) + for j, v := range item.Embedding { + switch val := v.(type) { + case float64: + vec[j] = val + case float32: + vec[j] = float64(val) + default: + return nil, fmt.Errorf("unexpected embedding value type at item %d index %d", item.Index, j) + } + } + embeddings[item.Index] = vec + } + + for i, vec := range embeddings { + if vec == nil { + return nil, fmt.Errorf("missing embedding for input at index %d", i) + } + } + + return embeddings, nil } // Rerank calculates similarity scores between query and texts