config.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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 litellm
  14. import (
  15. "fmt"
  16. "strconv"
  17. "strings"
  18. "github.com/LocoStack/loco-operator/api/v1alpha1"
  19. )
  20. type LiteLLMConfig struct {
  21. ModelList []modelEntry `yaml:"model_list"`
  22. MCPServers map[string]mcpServerEntry `yaml:"mcp_servers,omitempty"`
  23. LiteLLMSettings *liteLLMSettings `yaml:"litellm_settings,omitempty"`
  24. GeneralSettings *liteLLMGeneralSettings `yaml:"general_settings,omitempty"`
  25. EnvVars map[string]string `yaml:"environment_variables,omitempty"`
  26. }
  27. type modelEntry struct {
  28. ModelName string `yaml:"model_name"`
  29. LiteLLMParams map[string]any `yaml:"litellm_params"`
  30. }
  31. type mcpServerEntry struct {
  32. // url is the HTTP/SSE endpoint for sse and http transports.
  33. URL string `yaml:"url,omitempty"`
  34. // transport selects the MCP transport: sse (default), http, or stdio.
  35. Transport string `yaml:"transport,omitempty"`
  36. // --- authentication ---
  37. // authType selects the auth method: api_key, bearer_token, basic,
  38. // authorization, oauth2, or aws_sigv4.
  39. AuthType string `yaml:"auth_type,omitempty"`
  40. // authValue is the credential for api_key, bearer_token, basic, and
  41. // authorization auth types. May reference an env var via os.environ/VAR.
  42. AuthValue string `yaml:"auth_value,omitempty"`
  43. // --- headers ---
  44. // staticHeaders are key-value pairs sent with every request to this server.
  45. StaticHeaders map[string]string `yaml:"static_headers,omitempty"`
  46. // extraHeaders lists client request header names that LiteLLM should
  47. // forward to this MCP server.
  48. ExtraHeaders []string `yaml:"extra_headers,omitempty"`
  49. // allowAllKeys grants every LiteLLM API key access to this server when true.
  50. AllowAllKeys bool `yaml:"allow_all_keys,omitempty"`
  51. }
  52. type liteLLMSettings struct {
  53. SuccessCallback []string `yaml:"success_callback,omitempty"`
  54. FailureCallback []string `yaml:"failure_callback,omitempty"`
  55. JSONLogs bool `yaml:"json_logs,omitempty"`
  56. }
  57. type liteLLMGeneralSettings struct {
  58. MasterKey string `yaml:"master_key,omitempty"`
  59. }
  60. type LiteLLMConfigBuilder struct {
  61. MasterKeyEnvName string
  62. Stack *v1alpha1.Stack
  63. ObservabilityComponent *v1alpha1.Component
  64. ExternalModels []*v1alpha1.ExternalModel
  65. ManagedModels []*v1alpha1.ManagedModel
  66. ExternalTools []*v1alpha1.ExternalTool
  67. ManagedTools []*v1alpha1.ManagedTool
  68. }
  69. func (l *LiteLLMConfigBuilder) BuildLiteLLMConfig() (LiteLLMConfig, error) {
  70. modelList := []modelEntry{}
  71. for _, em := range l.ExternalModels {
  72. entry := buildExternalModelEntry(em)
  73. modelList = append(modelList, entry)
  74. }
  75. for _, mm := range l.ManagedModels {
  76. if mm.Status.Endpoint == "" {
  77. continue
  78. }
  79. entry := buildManagedModelEntry(mm)
  80. modelList = append(modelList, entry)
  81. }
  82. mcpServers := map[string]mcpServerEntry{}
  83. for _, et := range l.ExternalTools {
  84. entry := buildExternalToolEntry(et)
  85. mcpServers[et.Spec.ToolName] = entry
  86. }
  87. for _, mt := range l.ManagedTools {
  88. if mt.Status.Endpoint == "" {
  89. continue
  90. }
  91. entry := buildManagedToolEntry(mt)
  92. mcpServers[mt.Spec.ToolName] = entry
  93. }
  94. phoenixEnabled := phoenixEnabled(l.Stack)
  95. successCB := []string{}
  96. failureCB := []string{}
  97. if phoenixEnabled {
  98. successCB = append(successCB, "arize_phoenix")
  99. failureCB = append(failureCB, "arize_phoenix")
  100. }
  101. cfg := LiteLLMConfig{
  102. ModelList: modelList,
  103. MCPServers: mcpServers,
  104. LiteLLMSettings: &liteLLMSettings{
  105. SuccessCallback: successCB,
  106. FailureCallback: failureCB,
  107. JSONLogs: true,
  108. },
  109. GeneralSettings: &liteLLMGeneralSettings{
  110. MasterKey: "os.environ/" + l.MasterKeyEnvName,
  111. },
  112. }
  113. if phoenixEnabled {
  114. cfg.EnvVars = map[string]string{
  115. "PHOENIX_PROJECT_NAME": "locostack",
  116. "PHOENIX_COLLECTOR_HTTP_ENDPOINT": l.ObservabilityComponent.Status.Endpoint + "/v1/traces",
  117. }
  118. }
  119. return cfg, nil
  120. }
  121. func phoenixEnabled(stack *v1alpha1.Stack) bool {
  122. if stack == nil {
  123. return false
  124. }
  125. if stack.Spec.Observability == nil {
  126. return false
  127. }
  128. if !stack.Spec.Observability.Enabled {
  129. return false
  130. }
  131. if stack.Spec.Observability.Template != nil && stack.Spec.Observability.Template.Name != "phoenix" {
  132. return false
  133. }
  134. return true
  135. }
  136. func AuthEnvVarName(kind string, name string) string {
  137. return fmt.Sprintf("%s_%s_AUTH", strings.ToUpper(kind), strings.ToUpper(strings.ReplaceAll(name, "-", "_")))
  138. }
  139. // buildExternalModelEntry converts an ExternalModel into a LiteLLM model_list entry.
  140. func buildExternalModelEntry(em *v1alpha1.ExternalModel) modelEntry {
  141. params := map[string]any{
  142. "model": fmt.Sprintf("%s/%s", em.Spec.Provider, em.Spec.ProviderModel),
  143. }
  144. if em.Spec.Auth != nil && (em.Spec.Auth.APIKey != nil || em.Spec.Auth.BearerToken != nil) {
  145. params["api_key"] = "os.environ/" + AuthEnvVarName("ExternalModel", em.Name)
  146. }
  147. if em.Spec.APIBase != "" {
  148. params["api_base"] = em.Spec.APIBase
  149. }
  150. if em.Spec.APIVersion != "" {
  151. params["api_version"] = em.Spec.APIVersion
  152. }
  153. switch em.Spec.Category {
  154. case "embedding":
  155. params["mode"] = "embedding"
  156. case "reranker":
  157. params["mode"] = "rerank"
  158. }
  159. for k, v := range em.Spec.ExtraParams {
  160. params[k] = v
  161. }
  162. if em.Spec.DefaultInferenceParams != nil {
  163. applyInferenceParams(params, em.Spec.DefaultInferenceParams)
  164. }
  165. return modelEntry{
  166. ModelName: em.Spec.ModelName,
  167. LiteLLMParams: params,
  168. }
  169. }
  170. // buildManagedModelEntry converts a ManagedModel into a LiteLLM model_list entry.
  171. func buildManagedModelEntry(mm *v1alpha1.ManagedModel) modelEntry {
  172. // litellm's rerank dispatch does not recognize "openai" as a provider;
  173. // "hosted_vllm" is the generic self-hosted provider for the /rerank call type.
  174. provider := "openai"
  175. if mm.Spec.Category == "reranker" {
  176. provider = "hosted_vllm"
  177. }
  178. params := map[string]any{
  179. "model": fmt.Sprintf("%s/%s", provider, mm.Spec.ModelName),
  180. "api_base": mm.Status.Endpoint + "/v1",
  181. "api_key": "not-set",
  182. }
  183. switch mm.Spec.Category {
  184. case "embedding":
  185. params["mode"] = "embedding"
  186. case "reranker":
  187. params["mode"] = "rerank"
  188. }
  189. return modelEntry{
  190. ModelName: mm.Spec.ModelName,
  191. LiteLLMParams: params,
  192. }
  193. }
  194. func applyInferenceParams(params map[string]any, inf *v1alpha1.InferenceParameters) {
  195. if inf.Temperature != "" {
  196. if v, err := strconv.ParseFloat(inf.Temperature, 64); err == nil {
  197. params["temperature"] = v
  198. }
  199. }
  200. if inf.TopP != "" {
  201. if v, err := strconv.ParseFloat(inf.TopP, 64); err == nil {
  202. params["top_p"] = v
  203. }
  204. }
  205. if inf.MaxTokens != nil {
  206. params["max_tokens"] = *inf.MaxTokens
  207. }
  208. for k, v := range inf.Extra {
  209. params[k] = v
  210. }
  211. }
  212. // buildExternalToolEntry converts an ExternalTool into a LiteLLM mcp_server entry.
  213. func buildExternalToolEntry(et *v1alpha1.ExternalTool) mcpServerEntry {
  214. return buildToolEntry("ExternalTool", et.Spec.ToolName, et.Spec.Endpoint, et.Spec.Transport, et.Spec.Auth)
  215. }
  216. // buildManagedToolEntry converts a ManagedTool into a LiteLLM mcp_server entry.
  217. func buildManagedToolEntry(mt *v1alpha1.ManagedTool) mcpServerEntry {
  218. url := mt.Status.Endpoint
  219. switch mt.Spec.Transport {
  220. case "http":
  221. url = fmt.Sprintf("%s/mcp", strings.TrimSuffix(mt.Status.Endpoint, "/"))
  222. case "sse":
  223. url = fmt.Sprintf("%s/sse", strings.TrimSuffix(mt.Status.Endpoint, "/"))
  224. }
  225. return buildToolEntry("ManagedTool", mt.Spec.ToolName, url, mt.Spec.Transport, mt.Spec.Auth)
  226. }
  227. func buildToolEntry(toolKind string, toolName string, endpoint string, transport string, auth *v1alpha1.AuthSpec) mcpServerEntry {
  228. entry := mcpServerEntry{
  229. URL: endpoint,
  230. Transport: transport,
  231. }
  232. if auth == nil {
  233. return entry
  234. }
  235. if auth.BearerToken != nil {
  236. entry.AuthType = "bearer_token"
  237. entry.AuthValue = "os.environ/" + AuthEnvVarName(toolKind, toolName)
  238. } else if auth.APIKey != nil {
  239. headerName := auth.APIKey.HeaderName
  240. if headerName == "" {
  241. headerName = "X-Api-Key"
  242. }
  243. if entry.StaticHeaders == nil {
  244. entry.StaticHeaders = make(map[string]string)
  245. }
  246. entry.StaticHeaders[headerName] = "os.environ/" + AuthEnvVarName(toolKind, toolName)
  247. }
  248. for _, h := range auth.Headers {
  249. if entry.StaticHeaders == nil {
  250. entry.StaticHeaders = make(map[string]string)
  251. }
  252. entry.StaticHeaders[h.Name] = "os.environ/" + AuthEnvVarName(toolKind, toolName+"_"+h.Name)
  253. }
  254. return entry
  255. }