loco_context.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. /*
  2. Copyright 2026 LocoStack.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package reconciler
  14. import (
  15. "context"
  16. "fmt"
  17. "maps"
  18. "sort"
  19. "github.com/LocoStack/loco-operator/api/v1alpha1"
  20. "github.com/LocoStack/loco-operator/pkg/templates/litellm"
  21. corev1 "k8s.io/api/core/v1"
  22. "k8s.io/apimachinery/pkg/runtime"
  23. "sigs.k8s.io/controller-runtime/pkg/client"
  24. )
  25. type LocoContextReconciler struct {
  26. *DefaultComponentReconciler
  27. client client.Client
  28. scheme *runtime.Scheme
  29. stack *v1alpha1.Stack
  30. component *v1alpha1.Component
  31. }
  32. func NewLocoContextReconciler(client client.Client, scheme *runtime.Scheme, stack *v1alpha1.Stack, component *v1alpha1.Component) *LocoContextReconciler {
  33. return &LocoContextReconciler{
  34. DefaultComponentReconciler: NewDefaultComponentReconciler(client, scheme, stack, component),
  35. client: client,
  36. scheme: scheme,
  37. stack: stack,
  38. component: component,
  39. }
  40. }
  41. func (r *LocoContextReconciler) ReconcileComponent(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) ([]client.Object, error) {
  42. endpointResolver := NewEndpointResolver(r.client, r.scheme, r.stack.Namespace)
  43. deps := make([]client.Object, 0)
  44. var embeddingModelEndpoint string
  45. var embeddingModelName string
  46. var rerankerModelName string
  47. var rerankerModelEndpoint string
  48. for _, dep := range r.component.Spec.Dependencies {
  49. obj, endpoint, err := endpointResolver.ResolveEndpoint(ctx, r.stack, dep)
  50. if err != nil {
  51. return nil, fmt.Errorf("Failed to resolve endpoint for %s %s: %w", dep.Kind, dep.Name, err)
  52. }
  53. var category string
  54. switch dep.Kind {
  55. case "ExternalModel":
  56. category = obj.(*v1alpha1.ExternalModel).Spec.Category
  57. switch category {
  58. case "embedding":
  59. embeddingModelEndpoint = endpoint
  60. embeddingModelName = obj.(*v1alpha1.ExternalModel).Spec.ModelName
  61. case "reranker":
  62. rerankerModelEndpoint = endpoint
  63. rerankerModelName = obj.(*v1alpha1.ExternalModel).Spec.ModelName
  64. }
  65. case "ManagedModel":
  66. category = obj.(*v1alpha1.ManagedModel).Spec.Category
  67. switch category {
  68. case "embedding":
  69. embeddingModelEndpoint = endpoint
  70. embeddingModelName = obj.(*v1alpha1.ManagedModel).Spec.ModelName
  71. case "reranker":
  72. rerankerModelEndpoint = endpoint
  73. rerankerModelName = obj.(*v1alpha1.ManagedModel).Spec.ModelName
  74. }
  75. }
  76. deps = append(deps, obj)
  77. }
  78. var vectorStoreEndpoint string
  79. var graphStoreEndpoint string
  80. if r.stack != nil {
  81. vectorStore := r.stack.Status.ComponentStatus.VectorStore
  82. if vectorStore != "" && vectorStore != "disabled" {
  83. if obj, endpoint, err := r.resolveComponentEndpoint(ctx, r.stack, "VectorStore"); err != nil {
  84. return nil, fmt.Errorf("Failed to resolve vector store endpoint: %w", err)
  85. } else if obj != nil {
  86. deps = append(deps, obj)
  87. vectorStoreEndpoint = endpoint
  88. }
  89. }
  90. graphStore := r.stack.Status.ComponentStatus.GraphStore
  91. if graphStore != "" && graphStore != "disabled" {
  92. if obj, endpoint, err := r.resolveComponentEndpoint(ctx, r.stack, "GraphStore"); err != nil {
  93. return nil, fmt.Errorf("Failed to resolve graph store endpoint: %w", err)
  94. } else if obj != nil {
  95. deps = append(deps, obj)
  96. graphStoreEndpoint = endpoint
  97. }
  98. }
  99. }
  100. gatewayComp, err := endpointResolver.resolveGatewayEndpoint(ctx, r.stack)
  101. if err != nil {
  102. return nil, fmt.Errorf("Failed to resolve gateway for %s: %w", r.component.Name, err)
  103. }
  104. masterKey, err := r.gatewayMasterKey(ctx, gatewayComp)
  105. if err != nil {
  106. return nil, fmt.Errorf("resolving gateway master key: %w", err)
  107. }
  108. // sort deps to make sure the order is deterministic
  109. deps = sortObjectsByName(deps)
  110. config := map[string]string{
  111. "embedding_model_endpoint": embeddingModelEndpoint,
  112. "embedding_model_api_key": masterKey,
  113. "embedding_model_name": embeddingModelName,
  114. "reranker_model_endpoint": rerankerModelEndpoint,
  115. "reranker_model_api_key": masterKey,
  116. "reranker_model_name": rerankerModelName,
  117. "vector_store_endpoint": vectorStoreEndpoint,
  118. "graph_store_endpoint": graphStoreEndpoint,
  119. }
  120. configHash, err := r.ReconcileConfig(ctx, "config.yaml", config, true)
  121. if err != nil {
  122. return nil, fmt.Errorf("Failed to reconcile config for %s: %w", r.component.Name, err)
  123. }
  124. if tmpl.Metadata.Annotations == nil {
  125. tmpl.Metadata.Annotations = make(map[string]string)
  126. }
  127. maps.Copy(tmpl.Metadata.Annotations, map[string]string{
  128. "locostack.com/configHash": configHash,
  129. })
  130. if _, err := r.DefaultComponentReconciler.ReconcileComponent(ctx, tmpl, variables); err != nil {
  131. return nil, err
  132. }
  133. return deps, nil
  134. }
  135. func (r *LocoContextReconciler) resolveComponentEndpoint(ctx context.Context, stack *v1alpha1.Stack, component string) (client.Object, string, error) {
  136. compList := &v1alpha1.ComponentList{}
  137. err := r.client.List(ctx, compList, client.InNamespace(stack.Namespace), client.MatchingLabels{"locostack.com/stack": stack.Name, "locostack.com/component": component})
  138. if err != nil {
  139. return nil, "", fmt.Errorf("failed to list gateway components: %v", err)
  140. }
  141. if len(compList.Items) == 0 {
  142. return nil, "", nil
  143. }
  144. endpointResolver := NewEndpointResolver(r.client, r.scheme, stack.Namespace)
  145. return endpointResolver.ResolveEndpoint(ctx, stack, corev1.ObjectReference{Kind: "Component", Name: compList.Items[0].Name})
  146. }
  147. func (r *LocoContextReconciler) gatewayMasterKey(ctx context.Context, gatewayComp *v1alpha1.Component) (string, error) {
  148. secret := &corev1.Secret{}
  149. secretName := litellm.MasterKeySecretName(gatewayComp.Name)
  150. if err := r.client.Get(ctx, client.ObjectKey{Name: secretName, Namespace: r.component.GetNamespace()}, secret); err != nil {
  151. return "", fmt.Errorf("Failed to get gateway master key secret %s: %w", secretName, err)
  152. }
  153. key, ok := secret.Data[litellm.LITELLM_AUTH_SECRET_KEY]
  154. if !ok {
  155. return "", fmt.Errorf("Secret %s does not contain key %q", secretName, litellm.LITELLM_AUTH_SECRET_KEY)
  156. }
  157. return string(key), nil
  158. }
  159. func sortObjectsByName(objs []client.Object) []client.Object {
  160. sorted := make([]client.Object, len(objs))
  161. copy(sorted, objs)
  162. // sort by namespace/name
  163. sort.Slice(sorted, func(i, j int) bool {
  164. return sorted[i].GetNamespace()+"/"+sorted[i].GetName() < sorted[j].GetNamespace()+"/"+sorted[j].GetName()
  165. })
  166. return sorted
  167. }