mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 04:56:20 +02:00
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.
83 lines
2.7 KiB
Go
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
|
|
}
|