feat(llm): add login-info api (#24431)

This commit is contained in:
Zexi Li
2026-03-12 07:21:20 +08:00
committed by GitHub
parent 86ab99b270
commit cf2f140c08
10 changed files with 138 additions and 50 deletions
+1
View File
@@ -19,6 +19,7 @@ func init() {
cmd.BatchPerform("restart", new(options.LLMRestartOptions))
cmd.Get("probed-models", new(options.LLMIdOptions))
cmd.Get("url", new(options.LLMIdOptions))
cmd.Get("login-info", new(options.LLMIdOptions))
cmd.Custom(shell.CustomActionGet, "available-network", new(options.LLMAvailableNetworkOptions))
cmd.Perform("save-instant-model", new(options.LLMSaveInstantModelOptions))
cmd.Perform("quick-models", new(options.LLMQuickModelsOptions))
+8
View File
@@ -202,3 +202,11 @@ type LLMVolumeInput struct {
VolumeId string `json:"volume_id"`
AutoStart bool `json:"auto_start"`
}
// LLMLoginInfo is the response for GET /llms/<id>/login-info: login URL and credentials.
type LLMLoginInfo struct {
LoginUrl string `json:"login_url"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Extra map[string]string `json:"extra,omitempty"`
}
+10
View File
@@ -79,3 +79,13 @@ const (
const (
LLM_PROBE_INSTANT_MODEl_INTERVAL_SECOND = 120 // 2 minute
)
type LLMEnvKey string
const (
LLM_OPENCLAW_GATEWAY_TOKEN = LLMEnvKey("OPENCLAW_GATEWAY_TOKEN")
LLM_OPENCLAW_AUTH_USERNAME = LLMEnvKey("AUTH_USERNAME")
LLM_OPENCLAW_CUSTOM_USER = LLMEnvKey("CUSTOM_USER")
LLM_OPENCLAW_AUTH_PASSWORD = LLMEnvKey("AUTH_PASSWORD")
LLM_OPENCLAW_PASSWORD = LLMEnvKey("PASSWORD")
)
@@ -29,6 +29,10 @@ func (b *baseDriver) GetPrimaryImageId(sku *models.SLLMSku) string {
return sku.LLMImageId
}
func (b *baseDriver) GetPrimaryContainer(ctx context.Context, llm *models.SLLM, containers []*computeapi.PodContainerDesc) (*computeapi.PodContainerDesc, error) {
return containers[0], nil
}
func (b *baseDriver) GetMountedModels(sku *models.SLLMSku) []string {
return sku.MountedModels
}
+10 -9
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"strings"
"yunion.io/x/pkg/errors"
@@ -78,6 +79,15 @@ func (d *dify) GetPrimaryImageId(sku *models.SLLMSku) string {
return ""
}
func (d *dify) GetPrimaryContainer(ctx context.Context, llm *models.SLLM, containers []*computeapi.PodContainerDesc) (*computeapi.PodContainerDesc, error) {
for _, ctr := range containers {
if strings.HasSuffix(ctr.Name, api.DIFY_API_KEY) {
return ctr, nil
}
}
return nil, errors.Error("api container not found")
}
func (d *dify) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, input *api.LLMSkuCreateInput) (*api.LLMSkuCreateInput, error) {
if input.LLMSpec == nil || input.LLMSpec.Dify == nil {
return nil, errors.Wrap(httperrors.ErrInputParameter, "dify SKU requires llm_spec with type dify and image ids")
@@ -154,15 +164,6 @@ func (d *dify) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCr
return input, nil
}
// GetContainerSpec is required by ILLMContainerDriver but not used for Dify; pod creation uses GetContainerSpecs. Return the first container so the interface is satisfied.
func (d *dify) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *models.SLLMImage, sku *models.SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) *computeapi.PodContainerCreateInput {
specs := d.GetContainerSpecs(ctx, llm, image, sku, props, devices, diskId)
if len(specs) == 0 {
return nil
}
return specs[0]
}
// GetContainerSpecs returns all Dify pod containers (postgres, redis, api, worker, nginx, etc.). Uses effective spec (llm + sku merged by driver).
func (d *dify) GetContainerSpecs(ctx context.Context, llm *models.SLLM, image *models.SLLMImage, sku *models.SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) []*computeapi.PodContainerCreateInput {
spec := d.GetEffectiveSpec(llm, sku)
+40 -4
View File
@@ -251,6 +251,8 @@ func (c *openclaw) GetContainerSpecs(ctx context.Context, llm *models.SLLM, imag
MountPath: openclawDataDir,
},
}
httpAuthUsername := "admin"
httpAuthPassword := "clawadmin@123"
openclawSpec := computeapi.ContainerSpec{
ContainerSpec: commonapi.ContainerSpec{
Image: image.ToContainerImage(),
@@ -270,10 +272,10 @@ func (c *openclaw) GetContainerSpecs(ctx context.Context, llm *models.SLLM, imag
// {Key: "MOONSHOT_API_KEY", Value: "abc"},
// {Key: "OPENCLAW_PRIMARY_MODEL", Value: "moonshot/kimi-k2.5"},
// Auth
{Key: "AUTH_USERNAME", Value: "admin"},
{Key: "CUSTOM_USER", Value: "admin"},
{Key: "AUTH_PASSWORD", Value: "clawadmin@123"},
{Key: "PASSWORD", Value: "clawadmin@123"},
{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},
// // Browser sidecar
// {Key: "BROWSER_CDP_URL", Value: "http://localhost" + ":" + openclawBrowserCDPPort},
// {Key: "BROWSER_DEFAULT_PROFILE", Value: "openclaw"},
@@ -341,6 +343,40 @@ func (c *openclaw) GetLLMUrl(ctx context.Context, userCred mcclient.TokenCredent
return fmt.Sprintf("https://%s:%d", firstIP, 3001), nil
}
// GetLoginInfo returns OpenClaw web UI login credentials (same defaults as container env).
func (c *openclaw) GetLoginInfo(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM) (*api.LLMLoginInfo, error) {
ctr, err := llm.GetLLMSContainer(ctx)
if err != 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.LLMLoginInfo{
Username: username,
Password: password,
Extra: map[string]string{
string(api.LLM_OPENCLAW_GATEWAY_TOKEN): gatewayToken,
},
}, nil
}
func (c *openclaw) GetProbedInstantModelsExt(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, mdlIds ...string) (map[string]api.LLMInternalInstantMdlInfo, error) {
return nil, nil
}
+46
View File
@@ -409,6 +409,23 @@ func (llm *SLLM) GetLLMContainer() (*SLLMContainer, error) {
return GetLLMContainerManager().FetchByLLMId(llm.Id)
}
func (llm *SLLM) SyncLLMContainer(ctx context.Context, userCred mcclient.TokenCredential, server *computeapi.ServerDetails) (*SLLMContainer, error) {
curCtr, _ := llm.GetLLMContainer()
if curCtr != nil {
return curCtr, nil
}
drv := llm.GetLLMContainerDriver()
ctr, err := drv.GetPrimaryContainer(ctx, llm, server.Containers)
if err != nil {
return nil, errors.Wrap(err, "GetPrimaryContainer")
}
llmCtr, err := GetLLMContainerManager().CreateOnLLM(ctx, userCred, llm.GetOwnerId(), llm, ctr.Id, ctr.Name)
if nil != err {
return nil, errors.Wrapf(err, "create llm container on llm %s", ctr.Id)
}
return llmCtr, nil
}
func (llm *SLLM) GetLLMContainerDriver() ILLMContainerDriver {
sku, _ := llm.GetLLMSku(llm.LLMSkuId)
return sku.GetLLMContainerDriver()
@@ -692,6 +709,35 @@ func (llm *SLLM) GetDetailsUrl(ctx context.Context, userCred mcclient.TokenCrede
return output, nil
}
func (llm *SLLM) GetDetailsLoginInfo(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*api.LLMLoginInfo, error) {
if llm.CmpId == "" {
return nil, nil
}
output := new(api.LLMLoginInfo)
loginUrl, err := llm.GetLLMUrl(ctx, userCred)
if err != nil {
return nil, errors.Wrap(err, "GetLLMUrl")
}
output.LoginUrl = loginUrl
drv := llm.GetLLMContainerDriver()
if loginInfoDrv, ok := drv.(ILLMContainerLoginInfo); ok {
info, err := loginInfoDrv.GetLoginInfo(ctx, userCred, llm)
if err != nil {
return nil, errors.Wrap(err, "GetLoginInfo")
}
if info.Username != "" {
output.Username = info.Username
}
if info.Password != "" {
output.Password = info.Password
}
if len(info.Extra) > 0 {
output.Extra = info.Extra
}
}
return output, nil
}
func fetchNetworks(ctx context.Context, userCred mcclient.TokenCredential, networkIds []string) (map[string]computeapi.NetworkDetails, error) {
s := auth.GetSession(ctx, userCred, "")
params := computeoptions.ServerListOptions{}
+8 -14
View File
@@ -78,15 +78,11 @@ type ILLMContainerInstantModel interface {
DownloadModel(ctx context.Context, userCred mcclient.TokenCredential, llm *SLLM, tmpDir string, modelName string, modelTag string) (string, []string, error)
}
// ILLMContainerDriverMultiContainer is an optional interface for drivers that create a pod with multiple containers (e.g. Dify). If not implemented, the driver is assumed to provide a single container via GetContainerSpec.
type ILLMContainerDriverMultiContainer interface {
GetContainerSpecs(ctx context.Context, llm *SLLM, image *SLLMImage, sku *SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) []*computeapi.PodContainerCreateInput
}
type ILLMContainerDriver interface {
GetType() llm.LLMContainerType
// GetContainerSpecs returns one or more container specs. If nil or empty, caller falls back to GetContainerSpec for a single container.
GetContainerSpec(ctx context.Context, llm *SLLM, image *SLLMImage, sku *SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) *computeapi.PodContainerCreateInput
GetContainerSpecs(ctx context.Context, llm *SLLM, image *SLLMImage, sku *SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) []*computeapi.PodContainerCreateInput
GetPrimaryContainer(ctx context.Context, llm *SLLM, containers []*computeapi.PodContainerDesc) (*computeapi.PodContainerDesc, error)
// StartLLM is called after the pod is running. For drivers that need to start the model process inside the container (e.g. vLLM), it runs the start command via exec and waits for health; on failure returns an error. For drivers that need no extra step (e.g. Ollama), it returns nil.
StartLLM(ctx context.Context, userCred mcclient.TokenCredential, llm *SLLM) error
@@ -117,6 +113,11 @@ type ILLMContainerMCPAgent interface {
GetLLMUrl(ctx context.Context, userCred mcclient.TokenCredential, llm *SLLM) (string, error)
}
// ILLMContainerLoginInfo is an optional interface for drivers that provide web login credentials (e.g. Dify, OpenClaw). If not implemented, GetDetailsLoginInfo returns only login_url.
type ILLMContainerLoginInfo interface {
GetLoginInfo(ctx context.Context, userCred mcclient.TokenCredential, llm *SLLM) (*llm.LLMLoginInfo, error)
}
var (
llmContainerDrivers = newDrivers()
)
@@ -146,12 +147,5 @@ func GetLLMContainerInstantModelDriver(typ llm.LLMContainerType) (ILLMContainerI
// GetDriverPodContainers returns the container(s) for the given driver. If the driver implements ILLMContainerDriverMultiContainer, GetContainerSpecs is used; otherwise a single-element slice from GetContainerSpec is returned.
func GetDriverPodContainers(ctx context.Context, drv ILLMContainerDriver, llm *SLLM, image *SLLMImage, sku *SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) []*computeapi.PodContainerCreateInput {
if multi, ok := drv.(ILLMContainerDriverMultiContainer); ok {
return multi.GetContainerSpecs(ctx, llm, image, sku, props, devices, diskId)
}
spec := drv.GetContainerSpec(ctx, llm, image, sku, props, devices, diskId)
if spec == nil {
return nil
}
return []*computeapi.PodContainerCreateInput{spec}
return drv.GetContainerSpecs(ctx, llm, image, sku, props, devices, diskId)
}
+7 -23
View File
@@ -148,23 +148,9 @@ func (task *LLMCreateTask) OnLLMRefreshStatusComplete(ctx context.Context, llm *
}
}
// 创建应用容器记录(多容器 driver 如 Dify 不创建 SLLMContainer)
drv := llm.GetLLMContainerDriver()
_, isMulti := drv.(models.ILLMContainerDriverMultiContainer)
if !isMulti {
if len(server.Containers) != 1 {
task.taskFailed(ctx, llm, errors.Errorf("expected 1 containers, but got %d", len(server.Containers)))
return
}
llmCtr := models.GetSvrLLMContainer(server.Containers)
if llmCtr == nil {
task.taskFailed(ctx, llm, errors.Errorf("cannot find app container"))
return
}
if _, err := models.GetLLMContainerManager().CreateOnLLM(ctx, task.GetUserCred(), llm.GetOwnerId(), llm, llmCtr.Id, llmCtr.Name); nil != err {
task.taskFailed(ctx, llm, errors.Wrap(err, "create llm container on llm"))
return
}
if _, err := llm.SyncLLMContainer(ctx, task.GetUserCred(), server); err != nil {
task.taskFailed(ctx, llm, errors.Wrap(err, "SyncLLMContainer"))
return
}
// When AutoStart was true, compute auto-starts the server so LLMStartTask is never run. We must run StartLLM here.
@@ -175,12 +161,10 @@ func (task *LLMCreateTask) OnLLMRefreshStatusComplete(ctx context.Context, llm *
task.taskFailed(ctx, llm, errors.Wrap(err, "WaitServerStatus VM_RUNNING"))
return
}
if !isMulti {
_, err = llm.WaitContainerStatus(ctx, task.GetUserCred(), []string{computeapi.CONTAINER_STATUS_RUNNING}, 120)
if err != nil {
task.taskFailed(ctx, llm, errors.Wrap(err, "WaitContainerStatus"))
return
}
_, err = llm.WaitContainerStatus(ctx, task.GetUserCred(), []string{computeapi.CONTAINER_STATUS_RUNNING}, 120)
if err != nil {
task.taskFailed(ctx, llm, errors.Wrap(err, "WaitContainerStatus"))
return
}
err = llm.GetLLMContainerDriver().StartLLM(ctx, task.GetUserCred(), llm)
if err != nil {
@@ -84,6 +84,10 @@ func (task *LLMSyncStatusTask) OnInit(ctx context.Context, obj db.IStandaloneMod
if err != nil {
return nil, errors.Wrap(err, "WaitServerStatus")
}
// 同步本地 llm container
if _, err := llm.SyncLLMContainer(ctx, task.UserCred, srv); err != nil {
return nil, errors.Wrap(err, "SyncLLMContainer")
}
if utils.IsInArray(srv.Status, []string{
computeapi.POD_STATUS_CRASH_LOOP_BACK_OFF,