feat(llm): support hermes-agent (#24697)

This commit is contained in:
Zexi Li
2026-04-20 12:47:30 +08:00
committed by GitHub
parent ff89a3839b
commit 54f4b36e05
4 changed files with 208 additions and 16 deletions
+7 -5
View File
@@ -9,11 +9,12 @@ import (
type LLMImageType string
const (
LLM_IMAGE_TYPE_OLLAMA LLMImageType = "ollama"
LLM_IMAGE_TYPE_VLLM LLMImageType = "vllm"
LLM_IMAGE_TYPE_DIFY LLMImageType = "dify"
LLM_IMAGE_TYPE_COMFYUI LLMImageType = "comfyui"
LLM_IMAGE_TYPE_OPENCLAW LLMImageType = "openclaw"
LLM_IMAGE_TYPE_OLLAMA LLMImageType = "ollama"
LLM_IMAGE_TYPE_VLLM LLMImageType = "vllm"
LLM_IMAGE_TYPE_DIFY LLMImageType = "dify"
LLM_IMAGE_TYPE_COMFYUI LLMImageType = "comfyui"
LLM_IMAGE_TYPE_OPENCLAW LLMImageType = "openclaw"
LLM_IMAGE_TYPE_HERMES_AGENT LLMImageType = "hermes-agent"
)
var (
@@ -23,6 +24,7 @@ var (
string(LLM_IMAGE_TYPE_DIFY),
string(LLM_IMAGE_TYPE_COMFYUI),
string(LLM_IMAGE_TYPE_OPENCLAW),
string(LLM_IMAGE_TYPE_HERMES_AGENT),
)
)
+7 -5
View File
@@ -9,11 +9,12 @@ import (
type LLMContainerType string
const (
LLM_CONTAINER_OLLAMA LLMContainerType = "ollama"
LLM_CONTAINER_VLLM LLMContainerType = "vllm"
LLM_CONTAINER_DIFY LLMContainerType = "dify"
LLM_CONTAINER_COMFYUI LLMContainerType = "comfyui"
LLM_CONTAINER_OPENCLAW LLMContainerType = "openclaw"
LLM_CONTAINER_OLLAMA LLMContainerType = "ollama"
LLM_CONTAINER_VLLM LLMContainerType = "vllm"
LLM_CONTAINER_DIFY LLMContainerType = "dify"
LLM_CONTAINER_COMFYUI LLMContainerType = "comfyui"
LLM_CONTAINER_OPENCLAW LLMContainerType = "openclaw"
LLM_CONTAINER_HERMES_AGENT LLMContainerType = "hermes-agent"
)
var (
@@ -23,6 +24,7 @@ var (
string(LLM_CONTAINER_DIFY),
string(LLM_CONTAINER_COMFYUI),
string(LLM_CONTAINER_OPENCLAW),
string(LLM_CONTAINER_HERMES_AGENT),
)
)
+9 -6
View File
@@ -23,11 +23,12 @@ import (
// LLMSpec is the flat spec for LLM SKU: optional ollama/vllm/dify payload. Type is on LLMSku.LLMType.
type LLMSpec struct {
Ollama *LLMSpecOllama `json:"ollama,omitempty"`
Vllm *LLMSpecVllm `json:"vllm,omitempty"`
Dify *LLMSpecDify `json:"dify,omitempty"`
ComfyUI *LLMSpecComfyUI `json:"comfyui,omitempty"`
OpenClaw *LLMSpecOpenClaw `json:"openclaw,omitempty"`
Ollama *LLMSpecOllama `json:"ollama,omitempty"`
Vllm *LLMSpecVllm `json:"vllm,omitempty"`
Dify *LLMSpecDify `json:"dify,omitempty"`
ComfyUI *LLMSpecComfyUI `json:"comfyui,omitempty"`
OpenClaw *LLMSpecOpenClaw `json:"openclaw,omitempty"`
HermesAgent *LLMSpecHermesAgent `json:"hermes_agent,omitempty"`
}
func (s *LLMSpec) String() string {
@@ -38,7 +39,7 @@ func (s *LLMSpec) IsZero() bool {
if s == nil {
return true
}
return s.Ollama == nil && s.Vllm == nil && s.Dify == nil && s.ComfyUI == nil && s.OpenClaw == nil
return s.Ollama == nil && s.Vllm == nil && s.Dify == nil && s.ComfyUI == nil && s.OpenClaw == nil && s.HermesAgent == nil
}
// LLMSpecOllama holds type-specific fields for ollama SKUs.
@@ -147,6 +148,8 @@ type LLMSpecOpenClawWorkspaceTemplates struct {
UserMD string `json:"user_md"`
}
type LLMSpecHermesAgent struct{}
func init() {
gotypes.RegisterSerializable(reflect.TypeOf(new(LLMSpec)), func() gotypes.ISerializable {
return new(LLMSpec)
@@ -0,0 +1,185 @@
package llm_container
import (
"context"
"database/sql"
"fmt"
"strings"
"yunion.io/x/pkg/errors"
commonapi "yunion.io/x/onecloud/pkg/apis"
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
api "yunion.io/x/onecloud/pkg/apis/llm"
"yunion.io/x/onecloud/pkg/llm/models"
"yunion.io/x/onecloud/pkg/mcclient"
)
const (
hermesAgentDataDir = "/opt/data"
)
func init() {
models.RegisterLLMContainerDriver(newHermesAgent())
}
type hermesAgent struct {
baseDriver
}
func newHermesAgent() models.ILLMContainerDriver {
return &hermesAgent{
baseDriver: newBaseDriver(api.LLM_CONTAINER_HERMES_AGENT),
}
}
func (h *hermesAgent) GetSpec(sku *models.SLLMSku) interface{} {
if sku == nil || sku.LLMSpec == nil {
return nil
}
return sku.LLMSpec.HermesAgent
}
func (h *hermesAgent) GetEffectiveSpec(llm *models.SLLM, sku *models.SLLMSku) interface{} {
if llm != nil && llm.LLMSpec != nil && llm.LLMSpec.HermesAgent != nil {
return llm.LLMSpec.HermesAgent
}
return h.GetSpec(sku)
}
func (h *hermesAgent) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *models.SLLMImage, sku *models.SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) *computeapi.PodContainerCreateInput {
diskIndex := 0
hermesVols := []*commonapi.ContainerVolumeMount{
{
Disk: &commonapi.ContainerVolumeMountDisk{
Index: &diskIndex,
SubDirectory: "home",
},
Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
MountPath: homeDir,
},
{
Disk: &commonapi.ContainerVolumeMountDisk{
Index: &diskIndex,
SubDirectory: "data",
},
Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
MountPath: hermesAgentDataDir,
},
}
httpAuthUsername := "admin"
httpAuthPassword := openclawFixed9DigitPassword(llm.GetId())
hermesSpec := computeapi.ContainerSpec{
ContainerSpec: commonapi.ContainerSpec{
Image: image.ToContainerImage(),
ImageCredentialId: image.CredentialId,
EnableLxcfs: true,
AlwaysRestart: true,
ShmSizeMB: 2048,
DisableNoNewPrivs: true,
Envs: []*commonapi.ContainerKeyValue{
// Desktop env
// {Key: "TZ", Value: "Etc/UTC"},
{Key: "TZ", Value: "Asia/Shanghai"},
{Key: "PUID", Value: "1000"},
{Key: "PGID", Value: "1000"},
{Key: "LC_ALL", Value: "zh_CN.UTF-8"},
// webtop envs: https://github.com/linuxserver/docker-webtop?tab=readme-ov-file#advanced-configuration
// {Key: "DISABLE_SUDO", Value: "true"},
// Provider
// {Key: "MOONSHOT_API_KEY", Value: "abc"},
// {Key: "OPENCLAW_PRIMARY_MODEL", Value: "moonshot/kimi-k2.5"},
// Auth
{Key: string(api.LLM_OPENCLAW_AUTH_USERNAME), Value: httpAuthUsername},
{Key: string(api.LLM_OPENCLAW_CUSTOM_USER), Value: httpAuthUsername},
{Key: string(api.LLM_OPENCLAW_AUTH_PASSWORD), Value: httpAuthPassword},
{Key: string(api.LLM_OPENCLAW_PASSWORD), Value: httpAuthPassword},
// Brew env
{Key: "HOMEBREW_PREFIX", Value: "/home/linuxbrew/.linuxbrew"},
{Key: "HOMEBREW_CELLAR", Value: "/home/linuxbrew/.linuxbrew/Cellar"},
{Key: "HOMEBREW_REPOSITORY", Value: "/home/linuxbrew/.linuxbrew/Homebrew"},
// Selkies env
{Key: "SELKIES_UI_TITLE", Value: "Cloudpods Desktop"},
{Key: "SELKIES_UI_SHOW_LOGO", Value: "False"},
{Key: "SELKIES_UI_SIDEBAR_SHOW_APPS", Value: "False"},
{Key: "SELKIES_UI_SIDEBAR_SHOW_GAMEPADS", Value: "False"},
// Hermes env
models.NewEnv("HERMES_HOME", hermesAgentDataDir),
{Key: "HERMES_WEB_DIST", Value: "/opt/hermes/hermes_cli/web_dist"},
},
},
VolumeMounts: hermesVols,
RootFs: &commonapi.ContainerRootfs{
Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
Disk: &commonapi.ContainerVolumeMountDisk{
Index: &diskIndex,
SubDirectory: "rootfs",
},
Persistent: false,
},
}
// inject credential envs
// spec := c.GetEffectiveSpec(llm, sku)
if llm.LLMSpec == nil || llm.LLMSpec.HermesAgent == nil {
return &computeapi.PodContainerCreateInput{
Name: fmt.Sprintf("%s-%d", llm.GetName(), 0),
ContainerSpec: hermesSpec,
}
}
return &computeapi.PodContainerCreateInput{
Name: fmt.Sprintf("%s-%d", llm.GetName(), 0),
ContainerSpec: hermesSpec,
}
}
func (h *hermesAgent) GetContainerSpecs(ctx context.Context, llm *models.SLLM, image *models.SLLMImage, sku *models.SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) []*computeapi.PodContainerCreateInput {
return []*computeapi.PodContainerCreateInput{
h.GetContainerSpec(ctx, llm, image, sku, props, devices, diskId),
}
}
func (h *hermesAgent) GetLLMAccessUrlInfo(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, input *models.LLMAccessInfoInput) (*api.LLMAccessUrlInfo, error) {
//TODO implement me
return models.GetLLMAccessUrlInfo(ctx, userCred, llm, input, "https", api.LLM_OPENCLAW_DEFAULT_PORT)
}
// GetLoginInfo returns OpenClaw web UI login credentials (same defaults as container env).
func (h *hermesAgent) GetLoginInfo(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM) (*api.LLMAccessInfo, error) {
ctr, err := llm.GetLLMSContainer(ctx)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows || strings.Contains(strings.ToLower(err.Error()), "not found") {
return nil, nil
}
return nil, errors.Wrap(err, "get llm cloud container")
}
if ctr.Spec == nil {
return nil, errors.Wrap(errors.ErrEmpty, "no Spec")
}
var (
username string
password string
gatewayToken string
)
for _, env := range ctr.Spec.Envs {
if env.Key == string(api.LLM_OPENCLAW_AUTH_USERNAME) {
username = env.Value
}
if env.Key == string(api.LLM_OPENCLAW_AUTH_PASSWORD) {
password = env.Value
}
if env.Key == string(api.LLM_OPENCLAW_GATEWAY_TOKEN) {
gatewayToken = env.Value
}
}
return &api.LLMAccessInfo{
Username: username,
Password: password,
Extra: map[string]string{
string(api.LLM_OPENCLAW_GATEWAY_TOKEN): gatewayToken,
},
}, nil
}