Go: implement ListModels in Volcengine driver (#14702)

### What problem does this PR solve?

The VolcEngine Go driver in `internal/entity/models/volcengine.go`
shipped with a
`ListModels` stub that returned `volcengine, no such method`.
`conf/models/volcengine.json`
also did not declare a `models` URL suffix, so the model picker had
nothing to call even
if the method body were filled in.

A tenant who configured Volcengine (Doubao / Ark) as a provider could
not see the list of
available endpoints from the RAGFlow UI. Several other Go drivers
already implement
`ListModels` against the OpenAI-compatible `/models` endpoint (deepseek,
gitee, nvidia,
openai, siliconflow), so the interface and pattern are well-established.

This PR fills the gap.

### What this PR includes

* `conf/models/volcengine.json`: declare the `models` URL suffix
alongside the existing
  `chat`, `files`, and `embedding` entries. The Ark v3 API exposes
`https://ark.cn-beijing.volces.com/api/v3/models`, so the suffix is just
`models`.
* `internal/entity/models/volcengine.go`: replace the `ListModels` stub
with a real
implementation. Reuses the package-level `DSModelList` / `DSModel` types
that
DeepSeek, Gitee, and SiliconFlow already use to parse the
OpenAI-compatible models
  response shape.

No factory change. No interface change.

### How the driver works

* Resolves the region with a default fallback, the same way the other
VolcEngine methods
  in this driver already do.
* Builds the URL from `BaseURL[region] + URLSuffix.Models`, with
`strings.TrimSuffix` on
  the base to keep the join robust.
* Issues a `GET` with optional `Authorization: Bearer <api_key>` (the
header is omitted
when no key is configured, mirroring the existing NVIDIA `ListModels`).
* Reads the response body once, surfaces a non-200 with the upstream
status line plus
  body, and parses the JSON via the shared `DSModelList` type.
* Returns the model id list in input order. When the response includes
an `owned_by`
field, the entry is rendered as `id@owned_by`, matching the convention
used by the
  other Go drivers.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)


### How was this tested?

* `go build ./internal/entity/models/...` returns exit 0.
* `go vet ./internal/entity/models/...` is clean.
* `gofmt -l internal/entity/models/volcengine.go` is clean.
* The full method set on `VolcEngine` still matches the `ModelDriver`
interface.
* Endpoint reachability check: `GET
https://ark.cn-beijing.volces.com/api/v3/models`
returns `401 Unauthorized` without an API key, confirming the path
exists and accepts
  Bearer authentication.
* Pattern parity with DeepSeek, Gitee, NVIDIA, and SiliconFlow
`ListModels`.

Fixes #14701

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
BitToby
2026-05-10 16:59:18 -10:00
committed by GitHub
parent 08bb53bbb1
commit 39a1773f7f
2 changed files with 56 additions and 2 deletions

View File

@@ -6,7 +6,8 @@
"url_suffix": {
"chat": "chat/completions",
"files": "files",
"embedding": "embeddings/multimodal"
"embedding": "embeddings/multimodal",
"models": "models"
},
"class": "volcengine",
"models": [

View File

@@ -496,7 +496,60 @@ func (z *VolcEngine) Rerank(modelName *string, query string, documents []string,
}
func (z *VolcEngine) ListModels(apiConfig *APIConfig) ([]string, error) {
return nil, fmt.Errorf("%s, no such method", z.Name())
var region = "default"
if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" {
region = *apiConfig.Region
}
baseURL := z.BaseURL[region]
if baseURL == "" {
baseURL = z.BaseURL["default"]
}
if baseURL == "" {
return nil, fmt.Errorf("volcengine: no base URL configured for region %q", region)
}
url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), z.URLSuffix.Models)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
if apiConfig != nil && apiConfig.ApiKey != nil && *apiConfig.ApiKey != "" {
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("VolcEngine models API error: %s, body: %s", resp.Status, string(body))
}
var modelList DSModelList
if err = json.Unmarshal(body, &modelList); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
models := make([]string, 0, len(modelList.Models))
for _, model := range modelList.Models {
modelName := model.ID
if model.OwnedBy != "" {
modelName = model.ID + "@" + model.OwnedBy
}
models = append(models, modelName)
}
return models, nil
}
func (z *VolcEngine) Balance(apiConfig *APIConfig) (map[string]interface{}, error) {