diff --git a/cmd/climc/shell/llm/llm.go b/cmd/climc/shell/llm/llm.go index 32b6fa854b..63d2197f8e 100644 --- a/cmd/climc/shell/llm/llm.go +++ b/cmd/climc/shell/llm/llm.go @@ -18,6 +18,7 @@ func init() { cmd.BatchPerform("start", new(options.LLMStartOptions)) cmd.Get("probed-models", new(options.LLMIdOptions)) cmd.Get("url", 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)) } diff --git a/cmd/climc/shell/llm/mcp_agent.go b/cmd/climc/shell/llm/mcp_agent.go index bb32b49001..6b100afb62 100644 --- a/cmd/climc/shell/llm/mcp_agent.go +++ b/cmd/climc/shell/llm/mcp_agent.go @@ -4,7 +4,7 @@ import ( "bufio" "fmt" "io" - "net/url" + "net/http" "strings" "yunion.io/x/onecloud/cmd/climc/shell" @@ -36,15 +36,24 @@ func chatStream(s *mcclient.ClientSession, args *options.MCPAgentMCPAgentRequest return err } - path := fmt.Sprintf("/mcp_agents/%s/chat-stream?message=%s", id, url.QueryEscape(args.Message)) + bodyJSON, err := args.Params() + if err != nil { + return fmt.Errorf("failed to build request params: %v", err) + } + headers := http.Header{} + headers.Set("Content-Type", "application/json") + + body := strings.NewReader(bodyJSON.String()) + + path := fmt.Sprintf("/mcp_agents/%s/chat-stream", id) resp, err := s.RawVersionRequest( modules.MCPAgent.ServiceType(), modules.MCPAgent.EndpointType(), - "GET", + "POST", path, - nil, - nil, + headers, + body, ) if err != nil { return err diff --git a/pkg/apis/llm/llm.go b/pkg/apis/llm/llm.go index db4901c9e2..0b425dde1a 100644 --- a/pkg/apis/llm/llm.go +++ b/pkg/apis/llm/llm.go @@ -4,6 +4,7 @@ import ( "time" "yunion.io/x/onecloud/pkg/apis" + computeapi "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" ) @@ -64,8 +65,7 @@ type LLMBaseCreateInput struct { PreferHost string `json:"prefer_host"` AutoStart bool `json:"auto_start"` - NetworkType string `json:"network_type"` - NetworkId string `json:"network_id"` + Nets []*computeapi.NetworkConfig `json:"nets"` BandwidthMB int `json:"bandwidth_mb"` DebugMode bool `json:"debug_mode"` diff --git a/pkg/apis/llm/mcp_agent.go b/pkg/apis/llm/mcp_agent.go index e2dca3d6d1..ae32700b1a 100644 --- a/pkg/apis/llm/mcp_agent.go +++ b/pkg/apis/llm/mcp_agent.go @@ -98,7 +98,14 @@ type LLMToolRequestInput struct { } type LLMMCPAgentRequestInput struct { - Message string `json:"message" help:"message to send to MCP agent"` + Message string `json:"message" help:"message to send to MCP agent"` + History []MCPAgentChatMessage `json:"history" help:"chat history"` +} + +type MCPAgentChatMessage struct { + Role string `json:"role"` + Content string `json:"content"` + // ToolCalls []MCPAgentToolCallRecord `json:"tool_calls,omitempty"` } // MCPAgentResponse 表示 Agent 响应 @@ -115,6 +122,7 @@ type MCPAgentResponse struct { // MCPAgentToolCallRecord 记录工具调用 type MCPAgentToolCallRecord struct { + Id string `json:"id,omitempty"` ToolName string `json:"tool_name"` Arguments map[string]interface{} `json:"arguments"` Result string `json:"result"` diff --git a/pkg/apis/llm/ollama_const.go b/pkg/apis/llm/ollama_const.go index a69f610f1d..9007f4b610 100644 --- a/pkg/apis/llm/ollama_const.go +++ b/pkg/apis/llm/ollama_const.go @@ -17,10 +17,10 @@ const ( LLM_OLLAMA_HOST_MANIFESTS_DIR = "/manifests" LLM_OLLAMA_CACHE_DIR = "/.llm_ollama_cache" LLM_OLLAMA_CACHE_MOUNT_PATH = "/usr/local" - LLM_OLLAMA_LIBRARY_BASE_URL = `https://registry.ollama.ai/v2/library/%s` + LLM_OLLAMA_LIBRARY_BASE_URL = `https://registry.ollama.ai/v2/%s` LLM_OLLAMA_BASE_PATH = "/root/.ollama/models" LLM_OLLAMA_BLOBS_DIR = "/blobs" - LLM_OLLAMA_MANIFESTS_BASE_PATH = "/manifests/registry.ollama.ai/library" + LLM_OLLAMA_MANIFESTS_BASE_PATH = "/manifests/registry.ollama.ai" ) const ( diff --git a/pkg/apis/llm/sku.go b/pkg/apis/llm/sku.go index c2b1610d81..93ff0d0d42 100644 --- a/pkg/apis/llm/sku.go +++ b/pkg/apis/llm/sku.go @@ -166,18 +166,17 @@ type LLMSkuListInput struct { type LLMSkuCreateInput struct { LLMSKuBaseCreateInput + MountedModelResourceCreateInput - LLMImageId string `json:"llm_image_id"` - LLMType string `json:"llm_type"` - LLMModelName string `json:"llm_model_name"` + LLMImageId string `json:"llm_image_id"` + LLMType string `json:"llm_type"` } type LLMSkuUpdateInput struct { LLMSkuBaseUpdateInput MountedModelResourceUpdateInput - LLMImageId string `json:"llm_image_id"` - LLMModelName string `json:"llm_model_name"` + LLMImageId string `json:"llm_image_id"` } // type LLMModelCloneInput struct { diff --git a/pkg/llm/drivers/llm_client/ollama.go b/pkg/llm/drivers/llm_client/ollama.go index ad29b44702..e749cd326d 100644 --- a/pkg/llm/drivers/llm_client/ollama.go +++ b/pkg/llm/drivers/llm_client/ollama.go @@ -220,6 +220,13 @@ func (o *ollama) NewUserMessage(content string) models.ILLMChatMessage { } } +func (o *ollama) NewAssistantMessage(content string) models.ILLMChatMessage { + return &OllamaChatMessage{ + Role: "assistant", + Content: content, + } +} + func (o *ollama) NewAssistantMessageWithToolCalls(toolCalls []models.ILLMToolCall) models.ILLMChatMessage { // to ollama tool calls ollamaToolCalls := make([]OllamaToolCall, len(toolCalls)) diff --git a/pkg/llm/drivers/llm_client/openai.go b/pkg/llm/drivers/llm_client/openai.go index 9ca75338a4..cdb3d824b6 100644 --- a/pkg/llm/drivers/llm_client/openai.go +++ b/pkg/llm/drivers/llm_client/openai.go @@ -194,8 +194,12 @@ func (o *openai) doChatStreamRequest(ctx context.Context, mcpAgent *models.SMCPA return errors.Wrap(err, "create request") } httpReq.Header.Set("Content-Type", "application/json") - if mcpAgent.ApiKey != "" { - httpReq.Header.Set("Authorization", "Bearer "+mcpAgent.ApiKey) + apiKey, err := mcpAgent.GetApiKey() + if err != nil { + return err + } + if apiKey != "" { + httpReq.Header.Set("Authorization", "Bearer "+apiKey) } client := &http.Client{ @@ -277,8 +281,12 @@ func (o *openai) doChatRequest(ctx context.Context, mcpAgent *models.SMCPAgent, return nil, errors.Wrap(err, "create request") } httpReq.Header.Set("Content-Type", "application/json") - if mcpAgent.ApiKey != "" { - httpReq.Header.Set("Authorization", "Bearer "+mcpAgent.ApiKey) + apiKey, err := mcpAgent.GetApiKey() + if err != nil { + return nil, errors.Wrap(err, "get apiKey") + } + if apiKey != "" { + httpReq.Header.Set("Authorization", "Bearer "+apiKey) } client := &http.Client{ @@ -319,6 +327,13 @@ func (o *openai) NewUserMessage(content string) models.ILLMChatMessage { } } +func (o *openai) NewAssistantMessage(content string) models.ILLMChatMessage { + return &OpenAIChatMessage{ + Role: "assistant", + Content: content, + } +} + func (o *openai) NewAssistantMessageWithToolCalls(toolCalls []models.ILLMToolCall) models.ILLMChatMessage { openaiToolCalls := make([]OpenAIToolCall, len(toolCalls)) for i, tc := range toolCalls { diff --git a/pkg/llm/drivers/llm_container/ollama.go b/pkg/llm/drivers/llm_container/ollama.go index 9a54102b3a..068e48ad79 100644 --- a/pkg/llm/drivers/llm_container/ollama.go +++ b/pkg/llm/drivers/llm_container/ollama.go @@ -48,7 +48,7 @@ func (o *ollama) GetContainerSpec(ctx context.Context, llm *models.SLLM, image * }, } - if len(devices) == 0 && len(*sku.Devices) > 0 { + if len(devices) == 0 && (sku.Devices != nil && len(*sku.Devices) > 0) { for i := range *sku.Devices { index := i spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{ @@ -159,7 +159,8 @@ func (o *ollama) GetContainerSpec(ctx context.Context, llm *models.SLLM, image * func (o *ollama) DownloadModel(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, tmpDir string, modelName string, modelTag string) (string, []string, error) { // 1. download manifest from registry - manifestsUrl := fmt.Sprintf(api.LLM_OLLAMA_LIBRARY_BASE_URL, fmt.Sprintf("%s/manifests/%s", modelName, modelTag)) + namespace, repo := getNamespaceAndRepo(modelName) + manifestsUrl := fmt.Sprintf(api.LLM_OLLAMA_LIBRARY_BASE_URL, fmt.Sprintf("%s/%s/manifests/%s", namespace, repo, modelTag)) log.Infof("Downloading manifest from %s", manifestsUrl) manifestContent, err := llm.HttpGet(ctx, manifestsUrl) @@ -187,9 +188,9 @@ func (o *ollama) DownloadModel(ctx context.Context, userCred mcclient.TokenCrede // 4. create directory structure // tmpDir/blobs/ - // tmpDir/manifests/registry.ollama.ai/library// + // tmpDir/manifests/registry.ollama.ai// blobsDir := path.Join(tmpDir, "blobs") - manifestsDir := path.Join(tmpDir, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, modelName) + manifestsDir := path.Join(tmpDir, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, namespace, repo) if err := os.MkdirAll(blobsDir, 0755); err != nil { return "", nil, errors.Wrapf(err, "failed to create blobs directory %s", blobsDir) } @@ -210,8 +211,8 @@ func (o *ollama) DownloadModel(ctx context.Context, userCred mcclient.TokenCrede } // download blob from registry - // URL format: https://registry.ollama.ai/v2/library//blobs/ - blobUrl := fmt.Sprintf(api.LLM_OLLAMA_LIBRARY_BASE_URL, fmt.Sprintf("%s/blobs/%s", modelName, digest)) + // URL format: https://registry.ollama.ai/v2///blobs/ + blobUrl := fmt.Sprintf(api.LLM_OLLAMA_LIBRARY_BASE_URL, fmt.Sprintf("%s/%s/blobs/%s", namespace, repo, digest)) log.Infof("Downloading blob %s from %s", blobFileName, blobUrl) if err := llm.HttpDownloadFile(ctx, blobUrl, blobPath); err != nil { @@ -232,7 +233,7 @@ func (o *ollama) DownloadModel(ctx context.Context, userCred mcclient.TokenCrede blobFileName := strings.Replace(blob, ":", "-", 1) mounts[i] = path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_BLOBS_DIR, blobFileName) } - mounts = append(mounts, path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, modelName, modelTag)) + mounts = append(mounts, path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, namespace, repo, modelTag)) return modelId, mounts, nil } @@ -245,7 +246,8 @@ func (o *ollama) PreInstallModel(ctx context.Context, userCred mcclient.TokenCre } // mkdir llm-registry-base-path / modelname - mkdirReigtryBasePaht := fmt.Sprintf("mkdir -p %s", path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, instMdl.ModelName)) + namespace, repo := getNamespaceAndRepo(instMdl.ModelName) + mkdirReigtryBasePaht := fmt.Sprintf("mkdir -p %s", path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, namespace, repo)) _, err = exec(ctx, lc.CmpId, mkdirReigtryBasePaht, 10) if err != nil { return errors.Wrap(err, "failed to mkdir llm-registry-base-path / modelname") @@ -296,13 +298,11 @@ func (o *ollama) GetInstantModelIdByPostOverlay(postOverlay *commonapi.Container for k := range postOverlay.Image.PathMap { idx := strings.Index(k, api.LLM_OLLAMA_MANIFESTS_BASE_PATH) if idx != -1 { - suffix := k[idx+len(api.LLM_OLLAMA_MANIFESTS_BASE_PATH):] - parts := strings.Split(strings.Trim(suffix, "/"), "/") - if len(parts) >= 2 { - modelName := parts[len(parts)-2] - modelTag := parts[len(parts)-1] - log.Infof("In GetInstantModelIdByPostOverlay, Extracted modelName: %s, modelTag: %s, Got modelId: %s", modelName, modelTag, mdlNameToId[modelName+":"+modelTag]) - return mdlNameToId[modelName+":"+modelTag] + path := k[idx:] + nameTag := parseModelName(path) + if nameTag != "" { + log.Infof("In GetInstantModelIdByPostOverlay, Extracted nameTag: %s, Got modelId: %s", nameTag, mdlNameToId[nameTag]) + return mdlNameToId[nameTag] } } } @@ -320,7 +320,8 @@ func (o *ollama) DetectModelPaths(ctx context.Context, userCred mcclient.TokenCr for idx, blob := range pkgInfo.Blobs { originBlobs[idx] = path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_BLOBS_DIR, blob) } - originManifests := path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, pkgInfo.Name, pkgInfo.Tag) + namespace, repo := getNamespaceAndRepo(pkgInfo.Name) + originManifests := path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, namespace, repo, pkgInfo.Tag) var checks []string for _, blob := range originBlobs { @@ -509,7 +510,8 @@ func exec(ctx context.Context, containerId string, cmd string, timeoutSec int64) } func getManifestsPath(modelName, modelTag string) string { - return path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, modelName, modelTag) + namespace, repo := getNamespaceAndRepo(modelName) + return path.Join(api.LLM_OLLAMA_BASE_PATH, api.LLM_OLLAMA_MANIFESTS_BASE_PATH, namespace, repo, modelTag) } type Layer struct { @@ -591,6 +593,9 @@ func parseModelName(path string) string { name := model[:lastSlash] tag := model[lastSlash+1:] tag = strings.TrimRight(tag, `\`) + if after, ok := strings.CutPrefix(name, "library/"); ok { + name = after + } return name + ":" + tag } return strings.TrimRight(model, `\`) @@ -642,3 +647,11 @@ func (o *ollama) GetLLMUrl(ctx context.Context, userCred mcclient.TokenCredentia return fmt.Sprintf("http://%s:%d", server.HostAccessIp, accessInfo.AccessPort), nil } } + +func getNamespaceAndRepo(modelName string) (string, string) { + if strings.Contains(modelName, "/") { + parts := strings.Split(modelName, "/") + return parts[0], parts[1] + } + return "library", modelName +} diff --git a/pkg/llm/models/dify_sku.go b/pkg/llm/models/dify_sku.go index c45416b8e1..83c8f9beec 100644 --- a/pkg/llm/models/dify_sku.go +++ b/pkg/llm/models/dify_sku.go @@ -41,15 +41,15 @@ type SDifySkuManager struct { type SDifySku struct { SLLMSkuBase - PostgresImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` - RedisImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` - NginxImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` - DifyApiImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` - DifyPluginImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` - DifyWebImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` - DifySandboxImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` - DifySSRFImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` - DifyWeaviateImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` + PostgresImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` + RedisImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` + NginxImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` + DifyApiImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` + DifyPluginImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` + DifyWebImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` + DifySandboxImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` + DifySSRFImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` + DifyWeaviateImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` } func (man *SDifySkuManager) ListItemFilter( diff --git a/pkg/llm/models/llm.go b/pkg/llm/models/llm.go index f6c11d6329..c0d891c802 100644 --- a/pkg/llm/models/llm.go +++ b/pkg/llm/models/llm.go @@ -361,11 +361,7 @@ func (llm *SLLM) GetLLMSku(skuId string) (*SLLMSku, error) { func (llm *SLLM) GetLargeLanguageModelName(name string) (modelName string, modelTag string, err error) { if name == "" { - sku, err := llm.GetLLMSku("") - if err != nil { - return "", "", err - } - name = sku.LLMModelName + return "", "", errors.Wrap(errors.ErrInvalidStatus, "model name is empty") } parts := strings.Split(name, ":") modelName = parts[0] @@ -578,3 +574,33 @@ func fetchNetworks(ctx context.Context, userCred mcclient.TokenCredential, netwo } return networks, nil } + +func (man *SLLMManager) GetAvailableNetwork(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) { + s := auth.GetSession(ctx, userCred, "") + + ret := jsonutils.NewDict() + + q := jsonutils.NewDict() + if query != nil { + q.Update(query) + } + q.Set("server_type", jsonutils.NewString(string(computeapi.NETWORK_TYPE_HOSTLOCAL))) + q.Set("is_auto_alloc", jsonutils.NewBool(true)) + q.Set("status", jsonutils.NewString(computeapi.NETWORK_STATUS_AVAILABLE)) + q.Set("limit", jsonutils.NewInt(1)) + + result, err := compute.Networks.List(s, q) + if err == nil && result.Total > 0 { + ret.Add(jsonutils.NewInt(int64(result.Total)), "auto_alloc_network_hostlocal_count") + } + + q.Set("server_type", jsonutils.NewString(string(computeapi.NETWORK_TYPE_GUEST))) + q.Set("vpc_id", jsonutils.NewString(computeapi.DEFAULT_VPC_ID)) + + resultGuest, err := compute.Networks.List(s, q) + if err == nil && resultGuest.Total > 0 { + ret.Add(jsonutils.NewInt(int64(resultGuest.Total)), "auto_alloc_network_guest_count") + } + + return ret, nil +} diff --git a/pkg/llm/models/llm_base.go b/pkg/llm/models/llm_base.go index 807f653d23..d2b224b1ea 100644 --- a/pkg/llm/models/llm_base.go +++ b/pkg/llm/models/llm_base.go @@ -97,23 +97,58 @@ func (man *SLLMBaseManager) ValidateCreateData(ctx context.Context, userCred mcc input.PreferHost = hostDetails.Id } - if len(input.NetworkType) > 0 && !api.IsLLMSkuBaseNetworkType(input.NetworkType) { - return input, errors.Wrapf(httperrors.ErrInputParameter, "invalid network type %s", input.NetworkType) - } - - if len(input.NetworkId) > 0 { - s := auth.GetSession(ctx, userCred, "") - netObj, err := compute.Networks.Get(s, input.NetworkId, nil) - if err != nil { - return input, errors.Wrapf(httperrors.ErrInputParameter, "invalid network_id %s", input.NetworkId) + // 处理网络配置 + var firstNet *computeapi.NetworkConfig + if len(input.Nets) > 0 { + firstNet = input.Nets[0] + firstNet.Index = 0 + if len(string(firstNet.NetType)) > 0 && !api.IsLLMSkuBaseNetworkType(string(firstNet.NetType)) { + return input, errors.Wrapf(httperrors.ErrInputParameter, "invalid network type %s", firstNet.NetType) } - input.NetworkId, _ = netObj.GetString("id") - input.NetworkType, _ = netObj.GetString("server_type") + if len(firstNet.Network) > 0 { + s := auth.GetSession(ctx, userCred, "") + netObj, err := compute.Networks.Get(s, firstNet.Network, nil) + if err != nil { + return input, errors.Wrapf(httperrors.ErrInputParameter, "invalid network_id %s", firstNet.Network) + } + netId, _ := netObj.GetString("id") + netType, _ := netObj.GetString("server_type") + firstNet.Network = netId + if len(string(firstNet.NetType)) == 0 { + firstNet.NetType = computeapi.TNetworkType(netType) + } + } + } else { + return input, errors.Wrap(httperrors.ErrInputParameter, "nets cannot be empty") } return input, nil } +func (llmBase *SLLMBase) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error { + err := llmBase.SVirtualResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data) + if err != nil { + return errors.Wrap(err, "SVirtualResourceBase.CustomizeCreate") + } + + var input api.LLMBaseCreateInput + if err := data.Unmarshal(&input); err != nil { + return errors.Wrap(err, "unmarshal LLMBaseCreateInput") + } + + if len(input.Nets) > 0 { + firstNet := input.Nets[0] + if len(string(firstNet.NetType)) > 0 { + llmBase.NetworkType = string(firstNet.NetType) + } + if len(firstNet.Network) > 0 { + llmBase.NetworkId = firstNet.Network + } + } + + return nil +} + func GetServerIdsByHost(ctx context.Context, userCred mcclient.TokenCredential, hostId string) ([]string, error) { s := auth.GetSession(ctx, userCred, options.Options.Region) params := computeoptions.ServerListOptions{} diff --git a/pkg/llm/models/llm_base_pod.go b/pkg/llm/models/llm_base_pod.go index 219a986b78..8fd83eb8dc 100644 --- a/pkg/llm/models/llm_base_pod.go +++ b/pkg/llm/models/llm_base_pod.go @@ -109,21 +109,27 @@ func GetLLMBasePodCreateInput( }) } } - bandwidth := llmBase.BandwidthMb - if bandwidth == 0 { - bandwidth = skuBase.Bandwidth + var network *computeapi.NetworkConfig + if len(input.Nets) > 0 { + network = input.Nets[0] + networkCopy := *network + network = &networkCopy + network.Index = 0 } - network := &computeapi.NetworkConfig{ - BwLimit: bandwidth, - NetType: computeapi.TNetworkType(llmBase.NetworkType), + bandwidth := input.BandwidthMB + if bandwidth == 0 && network.BwLimit != 0 { + bandwidth = network.BwLimit } - if llmBase.NetworkType == string(computeapi.NETWORK_TYPE_HOSTLOCAL) { + if bandwidth == 0 && skuBase.Bandwidth != 0 { + bandwidth = skuBase.Bandwidth + } + network.BwLimit = bandwidth + + networkType := string(network.NetType) + if networkType == string(computeapi.NETWORK_TYPE_HOSTLOCAL) { network.PortMappings = portMappings } - if len(llmBase.NetworkId) > 0 { - network.Network = llmBase.NetworkId - } data.Networks = []*computeapi.NetworkConfig{ network, diff --git a/pkg/llm/models/llm_client_driver.go b/pkg/llm/models/llm_client_driver.go index 63fef565f7..26f6727630 100644 --- a/pkg/llm/models/llm_client_driver.go +++ b/pkg/llm/models/llm_client_driver.go @@ -57,6 +57,7 @@ type ILLMClient interface { ChatStream(ctx context.Context, mcpAgent *SMCPAgent, messages interface{}, tools interface{}, onChunk func(ILLMChatResponse) error) error NewUserMessage(content string) ILLMChatMessage + NewAssistantMessage(content string) ILLMChatMessage NewAssistantMessageWithToolCalls(toolCalls []ILLMToolCall) ILLMChatMessage NewToolMessage(toolId string, toolName string, content string) ILLMChatMessage NewSystemMessage(content string) ILLMChatMessage @@ -64,6 +65,32 @@ type ILLMClient interface { ConvertMCPTools(mcpTools []mcp.Tool) []ILLMTool } +type SLLMToolCall struct { + Id string + Function SLLMFunctionCall +} + +func (tc *SLLMToolCall) GetId() string { + return tc.Id +} + +func (tc *SLLMToolCall) GetFunction() ILLMFunctionCall { + return &tc.Function +} + +type SLLMFunctionCall struct { + Name string + Arguments map[string]interface{} +} + +func (fc *SLLMFunctionCall) GetName() string { + return fc.Name +} + +func (fc *SLLMFunctionCall) GetArguments() map[string]interface{} { + return fc.Arguments +} + var ( llmClientDrivers = newDrivers() ) diff --git a/pkg/llm/models/llm_save_instant_model.go b/pkg/llm/models/llm_save_instant_model.go index dfde2a9746..1a035fbeab 100644 --- a/pkg/llm/models/llm_save_instant_model.go +++ b/pkg/llm/models/llm_save_instant_model.go @@ -207,6 +207,9 @@ func (llm *SLLM) HttpGet(ctx context.Context, url string) ([]byte, error) { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + return nil, httperrors.NewResourceNotFoundError("url %s not found", url) + } return nil, errors.Errorf("unexpected status code: %d", resp.StatusCode) } @@ -231,6 +234,9 @@ func (llm *SLLM) HttpDownloadFile(ctx context.Context, url string, filePath stri defer resp.Body.Close() if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + return errors.Wrapf(httperrors.ErrResourceNotFound, "url %s not found", url) + } return errors.Errorf("unexpected status code: %d", resp.StatusCode) } diff --git a/pkg/llm/models/llm_sku.go b/pkg/llm/models/llm_sku.go index 85f750b988..8f5f11ee97 100644 --- a/pkg/llm/models/llm_sku.go +++ b/pkg/llm/models/llm_sku.go @@ -52,9 +52,8 @@ type SLLMSku struct { SLLMSkuBase SMountedModelsResource - LLMImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` - LLMType string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` - LLMModelName string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` + LLMImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` + LLMType string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required"` } func (man *SLLMSkuManager) ListItemFilter( @@ -161,10 +160,29 @@ func (man *SLLMSkuManager) ValidateCreateData(ctx context.Context, userCred mccl return input, errors.Wrap(httperrors.ErrInputParameter, "llm_type must be one of "+strings.Join(api.LLM_CONTAINER_TYPES.List(), ",")) } - _, err = validators.ValidateModel(ctx, userCred, GetLLMImageManager(), &input.LLMImageId) + imgObj, err := validators.ValidateModel(ctx, userCred, GetLLMImageManager(), &input.LLMImageId) if err != nil { return input, errors.Wrapf(err, "validate image_id %s", input.LLMImageId) } + llmImage := imgObj.(*SLLMImage) + if llmImage.LLMType != input.LLMType { + return input, errors.Wrapf(httperrors.ErrInvalidStatus, "image %s is not of type %s", input.LLMImageId, input.LLMType) + } + input.LLMImageId = llmImage.Id + + if input.MountedModels != nil { + for i, mdl := range input.MountedModels { + instMdl, err := GetInstantModelManager().FetchByIdOrName(ctx, userCred, mdl) + if err != nil { + return input, errors.Wrapf(err, "validate mounted model %s", mdl) + } + instantModle := instMdl.(*SInstantModel) + if instantModle.LlmType != input.LLMType { + return input, errors.Wrapf(httperrors.ErrInvalidStatus, "mounted model %s is not of type %s", mdl, input.LLMType) + } + input.MountedModels[i] = instantModle.GetId() + } + } input.Status = api.STATUS_READY return input, nil @@ -200,7 +218,12 @@ func (sku *SLLMSku) ValidateUpdateData(ctx context.Context, userCred mcclient.To if err != nil { return input, errors.Wrapf(err, "validate image_id %s", input.LLMImageId) } - input.LLMImageId = imgObj.GetId() + llmImage := imgObj.(*SLLMImage) + if llmImage.LLMType != sku.LLMType { + return input, errors.Wrapf(httperrors.ErrInvalidStatus, "image %s is not of type %s", input.LLMImageId, sku.LLMType) + } + input.LLMImageId = llmImage.Id + log.Infof("update llm_image_id %s to %s", sku.LLMImageId, input.LLMImageId) } return input, nil diff --git a/pkg/llm/models/mcp_agent.go b/pkg/llm/models/mcp_agent.go index 414ff89162..a5ee18a768 100644 --- a/pkg/llm/models/mcp_agent.go +++ b/pkg/llm/models/mcp_agent.go @@ -10,6 +10,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" + seclib "yunion.io/x/pkg/utils" "yunion.io/x/sqlchemy" api "yunion.io/x/onecloud/pkg/apis/llm" @@ -68,6 +69,48 @@ type SMCPAgent struct { McpServer string `width:"512" charset:"utf8" nullable:"false" list:"user" create:"optional" update:"user"` } +func (mcp *SMCPAgent) BeforeInsert() { + if len(mcp.Id) == 0 { + mcp.Id = db.DefaultUUIDGenerator() + } + if len(mcp.ApiKey) > 0 { + sec, err := seclib.EncryptAESBase64(mcp.Id, mcp.ApiKey) + if err != nil { + log.Errorf("EncryptAESBase64 fail %s", err) + } else { + mcp.ApiKey = sec + } + } + mcp.SSharableVirtualResourceBase.BeforeInsert() +} + +func (mcp *SMCPAgent) BeforeUpdate() { + if len(mcp.ApiKey) > 0 { + // heuristic to check if it is plaintext + _, err := seclib.DescryptAESBase64(mcp.Id, mcp.ApiKey) + if err != nil { + sec, err := seclib.EncryptAESBase64(mcp.Id, mcp.ApiKey) + if err != nil { + log.Errorf("EncryptAESBase64 fail %s", err) + } else { + mcp.ApiKey = sec + } + } + } +} + +func (mcp *SMCPAgent) GetApiKey() (string, error) { + if len(mcp.ApiKey) == 0 { + return "", nil + } + // try decrypt + key, err := seclib.DescryptAESBase64(mcp.Id, mcp.ApiKey) + if err == nil { + return key, nil + } + return mcp.ApiKey, nil +} + func (man *SMCPAgentManager) CustomizeHandlerInfo(info *appsrv.SHandlerInfo) { man.SSharableVirtualResourceBaseManager.CustomizeHandlerInfo(info) @@ -100,12 +143,20 @@ func (man *SMCPAgentManager) ValidateCreateData(ctx context.Context, userCred mc } input.LLMUrl = llmUrl - sku, err := llm.GetLLMSku("") - if err != nil { - return input, errors.Wrapf(err, "get LLM Sku from LLM %s", input.LLMId) - } if len(input.Model) == 0 { - input.Model = sku.LLMModelName + mdlInfos, err := llm.getProbedInstantModelsExt(ctx, userCred) + if err != nil { + return input, errors.Wrap(err, "get probed models from LLM instance") + } + if len(mdlInfos) == 0 { + return input, httperrors.NewBadRequestError("no available models found in LLM instance %s", input.LLMId) + } + var firstModel api.LLMInternalInstantMdlInfo + for _, mdlInfo := range mdlInfos { + firstModel = mdlInfo + break + } + input.Model = fmt.Sprintf("%s:%s", firstModel.Name, firstModel.Tag) } } @@ -159,11 +210,22 @@ func (man *SMCPAgentManager) ValidateUpdateData(ctx context.Context, userCred mc } input.LLMUrl = &llmUrl - sku, err := llm.GetLLMSku("") - if err != nil { - return input, errors.Wrapf(err, "get LLM Sku from LLM %s", *input.LLMId) + if input.Model == nil || len(*input.Model) == 0 { + mdlInfos, err := llm.getProbedInstantModelsExt(ctx, userCred) + if err != nil { + return input, errors.Wrap(err, "get probed models from LLM instance") + } + if len(mdlInfos) == 0 { + return input, httperrors.NewBadRequestError("no available models found in LLM instance %s", *input.LLMId) + } + var firstModel api.LLMInternalInstantMdlInfo + for _, mdlInfo := range mdlInfos { + firstModel = mdlInfo + break + } + modelStr := fmt.Sprintf("%s:%s", firstModel.Name, firstModel.Tag) + input.Model = &modelStr } - input.Model = &sku.LLMModelName } // 如果更新 llm_driver,验证其值 @@ -294,9 +356,10 @@ func (mcp *SMCPAgent) GetDetailsToolRequest( // return jsonutils.Marshal(result), nil // } -func (mcp *SMCPAgent) GetDetailsChatStream( +func (mcp *SMCPAgent) PerformChatStream( ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, input api.LLMMCPAgentRequestInput, ) (jsonutils.JSONObject, error) { appParams := appsrv.AppContextGetParams(ctx) @@ -361,11 +424,22 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden systemPrompt := buildSystemPrompt() // 初始化消息历史 - messages := []ILLMChatMessage{ - llmClient.NewSystemMessage(systemPrompt), - llmClient.NewUserMessage(req.Message), + messages := make([]ILLMChatMessage, 0) + messages = append(messages, llmClient.NewSystemMessage(systemPrompt)) + + // 处理历史消息 + if len(req.History) > 0 { + historyMessages := processHistoryMessages( + req.History, + llmClient, + options.Options.MCPAgentUserCharLimit, + options.Options.MCPAgentAssistantCharLimit, + ) + messages = append(messages, historyMessages...) } + messages = append(messages, llmClient.NewUserMessage(req.Message)) + // 记录工具调用 var toolCallRecords []api.MCPAgentToolCallRecord @@ -406,37 +480,14 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden toolCalls := resp.GetToolCalls() log.Infof("Got %d tool calls from Phase 1", len(toolCalls)) - // 将助手决定调用工具的消息加入历史 - messages = append(messages, llmClient.NewAssistantMessageWithToolCalls(toolCalls)) - - // 执行每个工具调用 - for _, tc := range toolCalls { - fc := tc.GetFunction() - toolName := fc.GetName() - arguments := fc.GetArguments() - - if arguments == nil { - arguments = make(map[string]interface{}) - } - - log.Infof("Calling tool: %s with arguments: %v", toolName, arguments) - - // 调用 MCP 工具 - result, err := mcpClient.CallTool(ctx, toolName, arguments) - resultText := utils.FormatToolResult(toolName, result, err) - log.Infoln("Get result from mcp query", resultText) - - // 记录 - toolCallRecords = append(toolCallRecords, api.MCPAgentToolCallRecord{ - ToolName: toolName, - Arguments: arguments, - Result: resultText, - }) - - // 将工具执行结果加入历史 - messages = append(messages, llmClient.NewToolMessage(tc.GetId(), toolName, resultText)) + toolCallRecords, toolMessages, err := processToolCalls(ctx, toolCalls, mcpClient, llmClient) + if err != nil { + return nil, errors.Wrap(err, "process tool calls") } + // 将工具调用相关的消息加入历史 + messages = append(messages, toolMessages...) + log.Infof("Phase 2: Streaming Response...") var finalAnswer strings.Builder @@ -472,3 +523,90 @@ func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCreden func buildSystemPrompt() string { return api.MCP_AGENT_SYSTEM_PROMPT } + +func processHistoryMessages( + history []api.MCPAgentChatMessage, + llmClient ILLMClient, + maxUserChars int, + maxAssistantChars int, +) []ILLMChatMessage { + if len(history) == 0 { + return []ILLMChatMessage{} + } + + var userChars, assistantChars int + processedMessages := make([]ILLMChatMessage, 0) + + // 从最新的消息开始遍历,保留最新消息,丢弃最旧消息 + for i := len(history) - 1; i >= 0; i-- { + msg := history[i] + msgChars := len(msg.Content) + + switch msg.Role { + case "user": + if userChars+msgChars > maxUserChars { + break + } + userChars += msgChars + processedMessages = append(processedMessages, llmClient.NewUserMessage(msg.Content)) + case "assistant": + if assistantChars+msgChars > maxAssistantChars { + break + } + assistantChars += msgChars + + if len(msg.Content) > 0 { + processedMessages = append(processedMessages, llmClient.NewAssistantMessage(msg.Content)) + } + } + } + + for i, j := 0, len(processedMessages)-1; i < j; i, j = i+1, j-1 { + processedMessages[i], processedMessages[j] = processedMessages[j], processedMessages[i] + } + + return processedMessages +} + +// processToolCalls 处理工具调用 +func processToolCalls( + ctx context.Context, + toolCalls []ILLMToolCall, + mcpClient *utils.MCPClient, + llmClient ILLMClient, +) ([]api.MCPAgentToolCallRecord, []ILLMChatMessage, error) { + toolCallRecords := make([]api.MCPAgentToolCallRecord, 0) + messagesToAdd := make([]ILLMChatMessage, 0) + + messagesToAdd = append(messagesToAdd, llmClient.NewAssistantMessageWithToolCalls(toolCalls)) + + // 执行每个工具调用 + for _, tc := range toolCalls { + fc := tc.GetFunction() + toolName := fc.GetName() + arguments := fc.GetArguments() + + if arguments == nil { + arguments = make(map[string]interface{}) + } + + log.Infof("Calling tool: %s with arguments: %v", toolName, arguments) + + // 调用 MCP 工具 + result, err := mcpClient.CallTool(ctx, toolName, arguments) + resultText := utils.FormatToolResult(toolName, result, err) + log.Infoln("Get result from mcp query", resultText) + + toolCallRecords = append(toolCallRecords, api.MCPAgentToolCallRecord{ + Id: tc.GetId(), + ToolName: toolName, + Arguments: arguments, + Result: resultText, + }) + + // 将工具执行结果加入历史 + messagesToAdd = append(messagesToAdd, llmClient.NewToolMessage(tc.GetId(), toolName, resultText)) + } + + return toolCallRecords, messagesToAdd, nil +} diff --git a/pkg/llm/options/option.go b/pkg/llm/options/option.go index 5a12236cae..e41c471473 100644 --- a/pkg/llm/options/option.go +++ b/pkg/llm/options/option.go @@ -30,6 +30,9 @@ type LLMOptions struct { // MCP Agent 配置 MCPServerURL string `help:"MCP Server URL" default:"http://default-mcp-server:30876"` MCPAgentTimeout int `help:"MCP Agent request timeout in seconds" default:"120"` + + MCPAgentUserCharLimit int `help:"MCP Agent user char limit" default:"3200"` + MCPAgentAssistantCharLimit int `help:"MCP Agent assistant char limit" default:"6400"` } var ( diff --git a/pkg/llm/service/handler.go b/pkg/llm/service/handler.go index e38249c697..22aecf01e1 100644 --- a/pkg/llm/service/handler.go +++ b/pkg/llm/service/handler.go @@ -2,13 +2,18 @@ package service import ( "context" + "fmt" "net/http" + "yunion.io/x/jsonutils" "yunion.io/x/onecloud/pkg/appsrv" "yunion.io/x/onecloud/pkg/appsrv/dispatcher" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/cloudcommon/policy" + "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/llm/models" + "yunion.io/x/onecloud/pkg/mcclient/auth" ) func handleOllamaRegistryYAML(ctx context.Context, w http.ResponseWriter, r *http.Request) { @@ -17,6 +22,34 @@ func handleOllamaRegistryYAML(ctx context.Context, w http.ResponseWriter, r *htt appsrv.Send(w, yamlContent) } +func AddAvailableNetworkHandler(prefix string, app *appsrv.Application) { + app.AddHandler2("GET", fmt.Sprintf("%s/available-network", prefix), auth.Authenticate(handleLLMAvailableNetwork), nil, "get_llm_available_network", nil) +} + +func handleLLMAvailableNetwork(ctx context.Context, w http.ResponseWriter, r *http.Request) { + userCred := auth.FetchUserCredential(ctx, policy.FilterPolicyCredential) + if userCred == nil { + httperrors.UnauthorizedError(ctx, w, "Unauthorized") + return + } + query, err := jsonutils.ParseQueryString(r.URL.RawQuery) + if err != nil { + httperrors.InvalidInputError(ctx, w, "Parse query string %q: %v", r.URL.RawQuery, err) + return + } + ret, err := models.GetLLMManager().GetAvailableNetwork(ctx, userCred, query) + if err != nil { + httperrors.GeneralServerError(ctx, w, err) + return + } + + wrapped := jsonutils.NewDict() + if ret != nil { + wrapped.Add(ret, "llm") + } + appsrv.SendJSON(w, wrapped) +} + func InitHandlers(app *appsrv.Application, isSlave bool) { db.InitAllManagers() db.RegistUserCredCacheUpdater() @@ -25,6 +58,8 @@ func InitHandlers(app *appsrv.Application, isSlave bool) { app.AddHandler("GET", "/ollama-registry.yaml", handleOllamaRegistryYAML) + AddAvailableNetworkHandler(models.GetLLMManager().KeywordPlural(), app) + for _, manager := range []db.IModelManager{ taskman.TaskManager, taskman.SubTaskManager, diff --git a/pkg/mcclient/modules/llm/mod_llm.go b/pkg/mcclient/modules/llm/mod_llm.go index 9ef82d791a..ee119c7af8 100644 --- a/pkg/mcclient/modules/llm/mod_llm.go +++ b/pkg/mcclient/modules/llm/mod_llm.go @@ -1,6 +1,11 @@ package llm import ( + "fmt" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/modulebase" "yunion.io/x/onecloud/pkg/mcclient/modules" ) @@ -21,3 +26,14 @@ func init() { type LLMManager struct { modulebase.ResourceManager } + +func (this *LLMManager) GetAvailableNetwork(session *mcclient.ClientSession, params jsonutils.JSONObject) (jsonutils.JSONObject, error) { + path := fmt.Sprintf("/%s/available-network", this.KeywordPlural) + if params != nil { + qs := params.QueryString() + if len(qs) > 0 { + path = fmt.Sprintf("%s?%s", path, qs) + } + } + return modulebase.Get(this.ResourceManager, session, path, this.Keyword) +} diff --git a/pkg/mcclient/options/llm/dify.go b/pkg/mcclient/options/llm/dify.go index b53427b185..be9aca83bd 100644 --- a/pkg/mcclient/options/llm/dify.go +++ b/pkg/mcclient/options/llm/dify.go @@ -2,7 +2,10 @@ package llm import ( "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + computeapi "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/cmdline" "yunion.io/x/onecloud/pkg/mcclient/options" ) @@ -38,7 +41,21 @@ type DifyCreateOptions struct { } func (o *DifyCreateOptions) Params() (jsonutils.JSONObject, error) { - return jsonutils.Marshal(o), nil + params := jsonutils.Marshal(o) + + if len(o.Net) > 0 { + nets := make([]*computeapi.NetworkConfig, 0) + for i, n := range o.Net { + net, err := cmdline.ParseNetworkConfig(n, i) + if err != nil { + return nil, errors.Wrapf(err, "parse network config %s", n) + } + nets = append(nets, net) + } + params.(*jsonutils.JSONDict).Add(jsonutils.Marshal(nets), "nets") + } + + return params, nil } func (o *DifyCreateOptions) GetCountParam() int { diff --git a/pkg/mcclient/options/llm/llm.go b/pkg/mcclient/options/llm/llm.go index d3c0f35b10..409b9600ea 100644 --- a/pkg/mcclient/options/llm/llm.go +++ b/pkg/mcclient/options/llm/llm.go @@ -2,8 +2,11 @@ package llm import ( "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + computeapi "yunion.io/x/onecloud/pkg/apis/compute" api "yunion.io/x/onecloud/pkg/apis/llm" + "yunion.io/x/onecloud/pkg/cloudcommon/cmdline" "yunion.io/x/onecloud/pkg/mcclient/options" ) @@ -55,8 +58,7 @@ type LLMBaseCreateOptions struct { ProjectId string PreferHost string - NETWORK_TYPE string `json:"network_type" choices:"guest|hostlocal"` - NetworkId string `help:"id of network" json:"network_id"` + Net []string `help:"Network descriptions"` BandwidthMb int @@ -70,7 +72,21 @@ type LLMCreateOptions struct { } func (o *LLMCreateOptions) Params() (jsonutils.JSONObject, error) { - return jsonutils.Marshal(o), nil + params := jsonutils.Marshal(o) + + if len(o.Net) > 0 { + nets := make([]*computeapi.NetworkConfig, 0) + for i, n := range o.Net { + net, err := cmdline.ParseNetworkConfig(n, i) + if err != nil { + return nil, errors.Wrapf(err, "parse network config %s", n) + } + nets = append(nets, net) + } + params.(*jsonutils.JSONDict).Add(jsonutils.Marshal(nets), "nets") + } + + return params, nil } func (o *LLMCreateOptions) GetCountParam() int { @@ -117,6 +133,19 @@ func (opts *LLMIdOptions) Params() (jsonutils.JSONObject, error) { return jsonutils.Marshal(opts), nil } +type LLMAvailableNetworkOptions struct { + NetworkType string `help:"network server_type filter, e.g. guest|hostlocal"` + VpcId string `help:"vpc id filter"` +} + +func (opts *LLMAvailableNetworkOptions) GetId() string { + return "" +} + +func (opts *LLMAvailableNetworkOptions) Params() (jsonutils.JSONObject, error) { + return options.StructToParams(opts) +} + type LLMSaveInstantModelOptions struct { LLMIdOptions diff --git a/pkg/mcclient/options/llm/llm_sku.go b/pkg/mcclient/options/llm/llm_sku.go index e0a1cef579..f800a92aa1 100644 --- a/pkg/mcclient/options/llm/llm_sku.go +++ b/pkg/mcclient/options/llm/llm_sku.go @@ -27,9 +27,10 @@ func (o *LLMSkuShowOptions) Params() (jsonutils.JSONObject, error) { type LLMSkuCreateOptions struct { LLMSkuBaseCreateOptions - LLM_IMAGE_ID string `json:"llm_image_id"` - LLM_TYPE string `json:"llm_type" choices:"ollama"` - LLM_MODEL_NAME string `help:"specific model of large language model, for example: qwen3:32b" json:"llm_model_name"` + MountedModels []string `help:"mounted models, e.g. qwen2:0.5b-dup" json:"mounted_models"` + + LLM_IMAGE_ID string `json:"llm_image_id"` + LLM_TYPE string `json:"llm_type" choices:"ollama"` } func (o *LLMSkuCreateOptions) Params() (jsonutils.JSONObject, error) { @@ -38,6 +39,7 @@ func (o *LLMSkuCreateOptions) Params() (jsonutils.JSONObject, error) { obj.Unmarshal(dict) o.LLMSkuBaseCreateOptions.Params(dict) + fetchMountedModels(o.MountedModels, dict) return dict, nil } @@ -56,10 +58,9 @@ func (o *LLMSkuDeleteOptions) Params() (jsonutils.JSONObject, error) { type LLMSkuUpdateOptions struct { LLMSkuBaseUpdateOptions - MountedModels []string `help:"mounted models, @: e.g. 6f48b936a09f@qwen2:0.5b" json:"mounted_models"` + MountedModels []string `help:"mounted models, e.g. qwen2:0.5b-dup" json:"mounted_models"` - LlmImageId string - LlmModelName string + LlmImageId string `json:"llm_image_id"` } func (o *LLMSkuUpdateOptions) GetId() string { diff --git a/pkg/mcclient/options/llm/mcp_agent.go b/pkg/mcclient/options/llm/mcp_agent.go index dd19704151..72218e82a1 100644 --- a/pkg/mcclient/options/llm/mcp_agent.go +++ b/pkg/mcclient/options/llm/mcp_agent.go @@ -1,6 +1,7 @@ package llm import ( + "fmt" "strings" "yunion.io/x/jsonutils" @@ -143,12 +144,28 @@ func (opts *MCPAgentToolRequestOptions) Params() (jsonutils.JSONObject, error) { type MCPAgentMCPAgentRequestOptions struct { MCPAgentIdOptions - Message string `help:"message to send to MCP agent" json:"message"` + MESSAGE string `help:"message to send to MCP agent" json:"message"` + History string `help:"chat history as JSON string, e.g. '[{\"role\":\"user\",\"content\":\"hello\"}]'" json:"history,omitempty"` } func (opts *MCPAgentMCPAgentRequestOptions) Params() (jsonutils.JSONObject, error) { input := api.LLMMCPAgentRequestInput{ - Message: opts.Message, + Message: opts.MESSAGE, + History: []api.MCPAgentChatMessage{}, } + + if len(opts.History) > 0 { + historyJSON, err := jsonutils.ParseString(opts.History) + if err != nil { + return nil, fmt.Errorf("failed to parse history JSON: %v", err) + } + if historyJSON != nil { + err = historyJSON.Unmarshal(&input.History) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal history: %v", err) + } + } + } + return jsonutils.Marshal(input), nil }