Files
multica/server/pkg/agent/deveco_models.go
Jordan c27919a4d0 feat(agents): add DevEco Code (deveco) runtime agent (MUL-4050) (#4916)
Adds DevEco Code (Huawei's HarmonyOS coding agent, built on the OpenCode engine) as a first-class runtime provider: backend/model parser, daemon discovery (probe + login-shell list + Windows .cmd native resolver), runtime profile migration (protocol_family whitelist), provider UI, metrics, and four-language docs. MCP injection is deferred (UI gates it off). Migration numbered 175.
2026-07-14 15:21:45 +08:00

83 lines
2.7 KiB
Go

package agent
// DevEco Code model discovery. Self-contained and independent of the OpenCode
// model parser: DevEco maintains its own catalog and CLI, so its discovery code
// lives here rather than reusing parseOpenCodeModels. A missing CLI or empty
// catalog returns an empty list so the caller (the runtime model picker) falls
// back to manual model entry instead of erroring.
import (
"context"
"os/exec"
"strings"
"time"
)
// discoverDevecoModels enumerates the `deveco models` catalog. It shells out to
// the DevEco Code CLI and parses `provider/model` rows. Discovery failures
// (binary missing, non-zero exit, unparseable output) silently yield an empty
// list so model selection degrades to manual entry rather than blocking.
func discoverDevecoModels(ctx context.Context, executablePath string) ([]Model, error) {
if executablePath == "" {
executablePath = "deveco"
}
if _, err := exec.LookPath(executablePath); err != nil {
return []Model{}, nil
}
// `deveco models` may sync its hosted catalog over the network on first
// run; allow a generous timeout so the picker isn't empty on a cold start.
runCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
cmd := exec.CommandContext(runCtx, executablePath, "models")
hideAgentWindow(cmd)
out, _ := cmd.Output()
models := parseDevecoModels(string(out))
if len(models) == 0 {
return []Model{}, nil
}
return models, nil
}
// parseDevecoModels extracts `provider/model` IDs from `deveco models` output.
// Each non-empty line's first whitespace-delimited token is treated as a model
// id when it contains a `/` and does not look like a header row or JSON. Output
// is de-duplicated, preserving first-seen order. Per-model variant/thinking
// annotation is not parsed here; the thinking-level picker is not offered for
// DevEco in this revision (see thinking.go).
func parseDevecoModels(output string) []Model {
var models []Model
seen := map[string]bool{}
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) == 0 {
continue
}
id := fields[0]
// Skip JSON objects/arrays, quoted strings, and header rows such as
// "PROVIDER/MODEL".
if strings.HasPrefix(id, "{") || strings.HasPrefix(id, "[") || strings.HasPrefix(id, "\"") {
continue
}
if !strings.Contains(id, "/") {
continue
}
if id == strings.ToUpper(id) {
continue
}
if seen[id] {
continue
}
seen[id] = true
provider := ""
if slash := strings.Index(id, "/"); slash > 0 {
provider = id[:slash]
}
models = append(models, Model{ID: id, Label: id, Provider: provider})
}
return models
}