dify.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. package llm_container
  2. import (
  3. "context"
  4. "strings"
  5. "yunion.io/x/pkg/errors"
  6. computeapi "yunion.io/x/onecloud/pkg/apis/compute"
  7. api "yunion.io/x/onecloud/pkg/apis/llm"
  8. "yunion.io/x/onecloud/pkg/cloudcommon/validators"
  9. "yunion.io/x/onecloud/pkg/httperrors"
  10. "yunion.io/x/onecloud/pkg/llm/models"
  11. "yunion.io/x/onecloud/pkg/mcclient"
  12. )
  13. func init() {
  14. models.RegisterLLMContainerDriver(newDify())
  15. }
  16. type dify struct{}
  17. func newDify() models.ILLMContainerDriver {
  18. return new(dify)
  19. }
  20. func (d *dify) GetType() api.LLMContainerType {
  21. return api.LLM_CONTAINER_DIFY
  22. }
  23. func (d *dify) GetSpec(sku *models.SLLMSku) interface{} {
  24. if sku.LLMSpec == nil {
  25. return nil
  26. }
  27. return sku.LLMSpec.Dify
  28. }
  29. // mergeDifySpecInto merges src into dst. If fillEmpty is true, only fills dst when dst is empty; otherwise overwrites dst when src is non-empty.
  30. func mergeDifySpecInto(dst, src *api.LLMSpecDify, fillEmpty bool) {
  31. if dst == nil || src == nil {
  32. return
  33. }
  34. mergeStr := func(dstPtr *string, srcVal string) {
  35. if fillEmpty {
  36. if *dstPtr == "" && srcVal != "" {
  37. *dstPtr = srcVal
  38. }
  39. } else {
  40. if srcVal != "" {
  41. *dstPtr = srcVal
  42. }
  43. }
  44. }
  45. mergeStr(&dst.PostgresImageId, src.PostgresImageId)
  46. mergeStr(&dst.RedisImageId, src.RedisImageId)
  47. mergeStr(&dst.NginxImageId, src.NginxImageId)
  48. mergeStr(&dst.DifyApiImageId, src.DifyApiImageId)
  49. mergeStr(&dst.DifyPluginImageId, src.DifyPluginImageId)
  50. mergeStr(&dst.DifyWebImageId, src.DifyWebImageId)
  51. mergeStr(&dst.DifySandboxImageId, src.DifySandboxImageId)
  52. mergeStr(&dst.DifySSRFImageId, src.DifySSRFImageId)
  53. mergeStr(&dst.DifyWeaviateImageId, src.DifyWeaviateImageId)
  54. if (fillEmpty && len(dst.CustomizedEnvs) == 0 && len(src.CustomizedEnvs) > 0) || (!fillEmpty && len(src.CustomizedEnvs) > 0) {
  55. dst.CustomizedEnvs = src.CustomizedEnvs
  56. }
  57. }
  58. // mergeDify merges llm and sku Dify specs; llm takes priority, use sku when llm is nil or zero.
  59. func mergeDify(llm, sku *api.LLMSpecDify) *api.LLMSpecDify {
  60. if llm != nil && !llm.IsZero() {
  61. out := *llm
  62. if llm.CustomizedEnvs != nil {
  63. out.CustomizedEnvs = make([]*api.DifyCustomizedEnv, len(llm.CustomizedEnvs))
  64. copy(out.CustomizedEnvs, llm.CustomizedEnvs)
  65. }
  66. return &out
  67. }
  68. if sku != nil {
  69. out := *sku
  70. if sku.CustomizedEnvs != nil {
  71. out.CustomizedEnvs = make([]*api.DifyCustomizedEnv, len(sku.CustomizedEnvs))
  72. copy(out.CustomizedEnvs, sku.CustomizedEnvs)
  73. }
  74. return &out
  75. }
  76. return nil
  77. }
  78. func (d *dify) GetEffectiveSpec(llm *models.SLLM, sku *models.SLLMSku) interface{} {
  79. if sku == nil || sku.LLMSpec == nil {
  80. return nil
  81. }
  82. var llmDify *api.LLMSpecDify
  83. if llm != nil && llm.LLMSpec != nil {
  84. llmDify = llm.LLMSpec.Dify
  85. }
  86. return mergeDify(llmDify, sku.LLMSpec.Dify)
  87. }
  88. func (d *dify) GetPrimaryImageId(sku *models.SLLMSku) string {
  89. if spec := d.GetSpec(sku); spec != nil {
  90. s := spec.(*api.LLMSpecDify)
  91. if s.DifyApiImageId != "" {
  92. return s.DifyApiImageId
  93. }
  94. }
  95. return ""
  96. }
  97. func (d *dify) GetPrimaryContainer(ctx context.Context, llm *models.SLLM, containers []*computeapi.PodContainerDesc) (*computeapi.PodContainerDesc, error) {
  98. for _, ctr := range containers {
  99. if strings.HasSuffix(ctr.Name, api.DIFY_API_KEY) {
  100. return ctr, nil
  101. }
  102. }
  103. return nil, errors.Error("api container not found")
  104. }
  105. func (d *dify) ValidateLLMSkuCreateData(ctx context.Context, userCred mcclient.TokenCredential, input *api.LLMSkuCreateInput) (*api.LLMSkuCreateInput, error) {
  106. if input.LLMSpec == nil || input.LLMSpec.Dify == nil {
  107. return nil, errors.Wrap(httperrors.ErrInputParameter, "dify SKU requires llm_spec with type dify and image ids")
  108. }
  109. if input.MountedModels != nil {
  110. return nil, errors.Wrap(httperrors.ErrInputParameter, "dify SKU does not support mounted models")
  111. }
  112. // Reuse ValidateLLMCreateSpec to normalize/validate LLMSpec.
  113. spec, err := d.ValidateLLMCreateSpec(ctx, userCred, nil, input.LLMSpec)
  114. if err != nil {
  115. return nil, err
  116. }
  117. input.LLMSpec = spec
  118. if input.LLMSpec != nil && input.LLMSpec.Dify != nil {
  119. input.LLMImageId = input.LLMSpec.Dify.DifyApiImageId
  120. }
  121. return input, nil
  122. }
  123. func (d *dify) ValidateLLMSkuUpdateData(ctx context.Context, userCred mcclient.TokenCredential, sku *models.SLLMSku, input *api.LLMSkuUpdateInput) (*api.LLMSkuUpdateInput, error) {
  124. if input.MountedModels != nil {
  125. return nil, errors.Wrap(httperrors.ErrInputParameter, "dify SKU does not support mounted models")
  126. }
  127. if input.LLMSpec == nil {
  128. return input, nil
  129. }
  130. // Reuse ValidateLLMUpdateSpec by treating current SKU spec as the \"current llm spec\".
  131. fakeLLM := &models.SLLM{LLMSpec: sku.LLMSpec}
  132. spec, err := d.ValidateLLMUpdateSpec(ctx, userCred, fakeLLM, input.LLMSpec)
  133. if err != nil {
  134. return nil, err
  135. }
  136. input.LLMSpec = spec
  137. // if dify_api_image_id is set, use it as the primary image id
  138. if input.LLMSpec != nil && input.LLMSpec.Dify != nil && input.LLMSpec.Dify.DifyApiImageId != "" {
  139. input.LLMImageId = input.LLMSpec.Dify.DifyApiImageId
  140. }
  141. return input, nil
  142. }
  143. // ValidateLLMCreateSpec implements ILLMContainerDriver. Validates image ids and merges empty fields from SKU spec.
  144. func (d *dify) ValidateLLMCreateSpec(ctx context.Context, userCred mcclient.TokenCredential, sku *models.SLLMSku, input *api.LLMSpec) (*api.LLMSpec, error) {
  145. if input == nil || input.Dify == nil {
  146. return input, nil
  147. }
  148. difySpec := input.Dify
  149. // Merge empty fields from SKU so the stored spec is complete
  150. if sku != nil && sku.LLMSpec != nil && sku.LLMSpec.Dify != nil {
  151. mergeDifySpecInto(difySpec, sku.LLMSpec.Dify, true)
  152. }
  153. // Validate non-empty image ids
  154. for _, imgId := range []*string{&difySpec.PostgresImageId, &difySpec.RedisImageId, &difySpec.NginxImageId, &difySpec.DifyApiImageId, &difySpec.DifyPluginImageId, &difySpec.DifyWebImageId, &difySpec.DifySandboxImageId, &difySpec.DifySSRFImageId, &difySpec.DifyWeaviateImageId} {
  155. if *imgId == "" {
  156. continue
  157. }
  158. imgObj, err := validators.ValidateModel(ctx, userCred, models.GetLLMImageManager(), imgId)
  159. if err != nil {
  160. return nil, errors.Wrapf(err, "validate image_id %s", *imgId)
  161. }
  162. img := imgObj.(*models.SLLMImage)
  163. if img.LLMType != string(api.LLM_CONTAINER_DIFY) {
  164. return nil, errors.Wrapf(httperrors.ErrInvalidStatus, "image %s is not of type dify", *imgId)
  165. }
  166. *imgId = img.Id
  167. }
  168. return &api.LLMSpec{Dify: difySpec}, nil
  169. }
  170. // ValidateLLMUpdateSpec implements ILLMContainerDriver. Merges input with current LLM spec (non-empty overwrites); validates image ids.
  171. func (d *dify) ValidateLLMUpdateSpec(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, input *api.LLMSpec) (*api.LLMSpec, error) {
  172. if input == nil || input.Dify == nil {
  173. return input, nil
  174. }
  175. // Start from current LLM spec, or SKU spec as base
  176. var base *api.LLMSpecDify
  177. if llm != nil && llm.LLMSpec != nil && llm.LLMSpec.Dify != nil {
  178. b := *llm.LLMSpec.Dify
  179. base = &b
  180. if llm.LLMSpec.Dify.CustomizedEnvs != nil {
  181. base.CustomizedEnvs = make([]*api.DifyCustomizedEnv, len(llm.LLMSpec.Dify.CustomizedEnvs))
  182. copy(base.CustomizedEnvs, llm.LLMSpec.Dify.CustomizedEnvs)
  183. }
  184. } else if llm != nil {
  185. sku, err := llm.GetLLMSku(llm.LLMSkuId)
  186. if err == nil && sku != nil && sku.LLMSpec != nil && sku.LLMSpec.Dify != nil {
  187. b := *sku.LLMSpec.Dify
  188. base = &b
  189. if sku.LLMSpec.Dify.CustomizedEnvs != nil {
  190. base.CustomizedEnvs = make([]*api.DifyCustomizedEnv, len(sku.LLMSpec.Dify.CustomizedEnvs))
  191. copy(base.CustomizedEnvs, sku.LLMSpec.Dify.CustomizedEnvs)
  192. }
  193. }
  194. }
  195. if base == nil {
  196. base = &api.LLMSpecDify{}
  197. }
  198. mergeDifySpecInto(base, input.Dify, false)
  199. // Validate non-empty image ids
  200. for _, imgId := range []*string{&base.PostgresImageId, &base.RedisImageId, &base.NginxImageId, &base.DifyApiImageId, &base.DifyPluginImageId, &base.DifyWebImageId, &base.DifySandboxImageId, &base.DifySSRFImageId, &base.DifyWeaviateImageId} {
  201. if *imgId == "" {
  202. continue
  203. }
  204. imgObj, err := validators.ValidateModel(ctx, userCred, models.GetLLMImageManager(), imgId)
  205. if err != nil {
  206. return nil, errors.Wrapf(err, "validate image_id %s", *imgId)
  207. }
  208. img := imgObj.(*models.SLLMImage)
  209. if img.LLMType != string(api.LLM_CONTAINER_DIFY) {
  210. return nil, errors.Wrapf(httperrors.ErrInvalidStatus, "image %s is not of type dify", *imgId)
  211. }
  212. *imgId = img.Id
  213. }
  214. return &api.LLMSpec{Dify: base}, nil
  215. }
  216. // GetContainerSpec is required by ILLMContainerDriver but not used for Dify; pod creation uses GetContainerSpecs. Return the first container so the interface is satisfied.
  217. 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 {
  218. specs := d.GetContainerSpecs(ctx, llm, image, sku, props, devices, diskId)
  219. if len(specs) == 0 {
  220. return nil
  221. }
  222. return specs[0]
  223. }
  224. // GetContainerSpecs returns all Dify pod containers (postgres, redis, api, worker, nginx, etc.). Uses effective spec (llm + sku merged by driver).
  225. 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 {
  226. spec := d.GetEffectiveSpec(llm, sku)
  227. if spec == nil {
  228. return nil
  229. }
  230. return models.GetDifyContainersByNameAndSku(llm.GetName(), sku, nil, spec.(*api.LLMSpecDify))
  231. }
  232. func (d *dify) MatchContainerToUpdate(ctr *computeapi.SContainer, podCtrs []*computeapi.PodContainerCreateInput) (*computeapi.PodContainerCreateInput, error) {
  233. return MatchContainerToUpdateByName(ctr, podCtrs)
  234. }
  235. // StartLLM is a no-op for Dify; all services are started by their container entrypoints.
  236. func (d *dify) StartLLM(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM) error {
  237. return nil
  238. }
  239. // GetLLMAccessUrlInfo returns the Dify access URL (nginx port 80). Same pattern as vLLM/Ollama: guest network uses LLMIp, hostlocal uses host IP.
  240. func (d *dify) GetLLMAccessUrlInfo(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, input *models.LLMAccessInfoInput) (*api.LLMAccessUrlInfo, error) {
  241. return models.GetLLMAccessUrlInfo(ctx, userCred, llm, input, "http", 80)
  242. }