diff --git a/go/adk/pkg/agent/agent.go b/go/adk/pkg/agent/agent.go index 1aaa21af4..ce8c43b70 100644 --- a/go/adk/pkg/agent/agent.go +++ b/go/adk/pkg/agent/agent.go @@ -331,6 +331,14 @@ func CreateLLM(ctx context.Context, m adk.Model, log logr.Logger) (adkmodel.LLM, } return models.NewSAPAICoreModelWithLogger(cfg, log) + case *adk.SparkMaaSAI: + cfg := &models.OpenAIConfig{ + TransportConfig: transportConfigFromBase(m.BaseModel, nil), + Model: m.Model, + BaseUrl: m.BaseUrl, + } + return models.NewOpenAIModelWithLogger(cfg, log) + default: return nil, fmt.Errorf("unsupported model type: %s", m.GetType()) } diff --git a/go/api/adk/types.go b/go/api/adk/types.go index 602a45798..c6f46aef7 100644 --- a/go/api/adk/types.go +++ b/go/api/adk/types.go @@ -104,6 +104,7 @@ const ( ModelTypeGemini = "gemini" ModelTypeBedrock = "bedrock" ModelTypeSAPAICore = "sap_ai_core" + ModelTypeSparkMaaSAI = "spark_maas_ai" ) func (o *OpenAI) MarshalJSON() ([]byte, error) { @@ -290,6 +291,26 @@ func (s *SAPAICore) GetType() string { return ModelTypeSAPAICore } +type SparkMaaSAI struct { + BaseModel + BaseUrl string `json:"base_url,omitempty"` +} + +func (s *SparkMaaSAI) MarshalJSON() ([]byte, error) { + type Alias SparkMaaSAI + return json.Marshal(&struct { + Type string `json:"type"` + *Alias + }{ + Type: ModelTypeSparkMaaSAI, + Alias: (*Alias)(s), + }) +} + +func (s *SparkMaaSAI) GetType() string { + return ModelTypeSparkMaaSAI +} + // GenericModel is a catch-all model type used by the Go ADK when the model // type doesn't match any known constant. type GenericModel struct { @@ -358,6 +379,12 @@ func ParseModel(bytes []byte) (Model, error) { return nil, err } return &sapAICore, nil + case ModelTypeSparkMaaSAI: + var sparkMaaSAI SparkMaaSAI + if err := json.Unmarshal(bytes, &sparkMaaSAI); err != nil { + return nil, err + } + return &sparkMaaSAI, nil } return nil, fmt.Errorf("unknown model type: %s", model.Type) } @@ -426,6 +453,9 @@ func ModelToEmbeddingConfig(m Model) *EmbeddingConfig { case *SAPAICore: e.Model = v.Model e.BaseUrl = v.BaseUrl + case *SparkMaaSAI: + e.Model = v.Model + e.BaseUrl = v.BaseUrl default: e.Model = "" } diff --git a/go/api/adk/types_test.go b/go/api/adk/types_test.go index d1d9c8c06..eac8dff96 100644 --- a/go/api/adk/types_test.go +++ b/go/api/adk/types_test.go @@ -19,6 +19,8 @@ func TestMarshalJSON_TypeDiscriminator(t *testing.T) { {name: "Ollama", model: &Ollama{BaseModel: BaseModel{Model: "llama3"}}, wantType: ModelTypeOllama}, {name: "Gemini", model: &Gemini{BaseModel: BaseModel{Model: "gemini-pro"}}, wantType: ModelTypeGemini}, {name: "Bedrock", model: &Bedrock{BaseModel: BaseModel{Model: "claude-v2"}}, wantType: ModelTypeBedrock}, + {name: "SAPAICore", model: &SAPAICore{BaseModel: BaseModel{Model: "claude-3-sonnet"}, BaseUrl: "https://api.example.com"}, wantType: ModelTypeSAPAICore}, + {name: "SparkMaaSAI", model: &SparkMaaSAI{BaseModel: BaseModel{Model: "Spark5.0-Pro"}, BaseUrl: "https://maas-api.cn-huabei-1.xf-yun.com.cn"}, wantType: ModelTypeSparkMaaSAI}, } for _, tt := range tests { @@ -115,6 +117,7 @@ func TestMarshalJSON_BaseModelFields(t *testing.T) { {name: "Ollama", model: &Ollama{BaseModel: base}}, {name: "Gemini", model: &Gemini{BaseModel: base}}, {name: "Bedrock", model: &Bedrock{BaseModel: base}}, + {name: "SparkMaaSAI", model: &SparkMaaSAI{BaseModel: base}}, } for _, tt := range tests { @@ -245,6 +248,30 @@ func TestMarshalJSON_TypeSpecificFields(t *testing.T) { t.Errorf("region = %v, want %q", raw["region"], "us-east-1") } }) + + t.Run("SparkMaaSAI base_url", func(t *testing.T) { + m := &SparkMaaSAI{ + BaseModel: BaseModel{Model: "Spark5.0-Pro"}, + BaseUrl: "https://maas-api.cn-huabei-1.xf-yun.com.cn", + } + data, err := json.Marshal(m) + if err != nil { + t.Fatalf("MarshalJSON() error = %v", err) + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + if raw["type"] != "spark_maas_ai" { + t.Errorf("type = %v, want %q", raw["type"], "spark_maas_ai") + } + if raw["base_url"] != "https://maas-api.cn-huabei-1.xf-yun.com.cn" { + t.Errorf("base_url = %v, want %q", raw["base_url"], "https://maas-api.cn-huabei-1.xf-yun.com.cn") + } + if raw["model"] != "Spark5.0-Pro" { + t.Errorf("model = %v, want %q", raw["model"], "Spark5.0-Pro") + } + }) } func TestAgentConfig_UnmarshalJSON_Network(t *testing.T) { @@ -341,6 +368,14 @@ func TestParseModel_Roundtrip(t *testing.T) { }, wantType: ModelTypeBedrock, }, + { + name: "SparkMaaSAI roundtrip", + model: &SparkMaaSAI{ + BaseModel: BaseModel{Model: "Spark5.0-Pro", Headers: map[string]string{"X-Custom": "val"}}, + BaseUrl: "https://maas-api.cn-huabei-1.xf-yun.com.cn", + }, + wantType: ModelTypeSparkMaaSAI, + }, } for _, tt := range tests { @@ -759,6 +794,59 @@ func TestAgentConfig_UnmarshalJSON_InvalidJSON(t *testing.T) { } } +func TestParseModel_SparkMaaSAI(t *testing.T) { + data := []byte(`{"type":"spark_maas_ai","model":"Spark5.0-Pro","base_url":"https://maas-api.cn-huabei-1.xf-yun.com.cn","headers":{"X-Test":"v"},"api_key_passthrough":true}`) + + parsed, err := ParseModel(data) + if err != nil { + t.Fatalf("ParseModel() error = %v", err) + } + + spark, ok := parsed.(*SparkMaaSAI) + if !ok { + t.Fatalf("ParseModel() returned %T, want *SparkMaaSAI", parsed) + } + if spark.Model != "Spark5.0-Pro" { + t.Errorf("Model = %q, want %q", spark.Model, "Spark5.0-Pro") + } + if spark.BaseUrl != "https://maas-api.cn-huabei-1.xf-yun.com.cn" { + t.Errorf("BaseUrl = %q, want %q", spark.BaseUrl, "https://maas-api.cn-huabei-1.xf-yun.com.cn") + } + if !spark.APIKeyPassthrough { + t.Error("APIKeyPassthrough = false, want true") + } + if spark.Headers["X-Test"] != "v" { + t.Errorf("Headers[X-Test] = %q, want %q", spark.Headers["X-Test"], "v") + } +} + +func TestModelToEmbeddingConfig_SparkMaaSAI(t *testing.T) { + m := &SparkMaaSAI{ + BaseModel: BaseModel{Model: "qwen-plus"}, + BaseUrl: "https://maas-api.cn-huabei-1.xf-yun.com.cn", + } + e := ModelToEmbeddingConfig(m) + if e == nil { + t.Fatal("ModelToEmbeddingConfig() returned nil") + } + if e.Provider != "spark_maas_ai" { + t.Errorf("Provider = %q, want %q", e.Provider, "spark_maas_ai") + } + if e.Model != "qwen-plus" { + t.Errorf("Model = %q, want %q", e.Model, "qwen-plus") + } + if e.BaseUrl != "https://maas-api.cn-huabei-1.xf-yun.com.cn" { + t.Errorf("BaseUrl = %q, want %q", e.BaseUrl, "https://maas-api.cn-huabei-1.xf-yun.com.cn") + } +} + +func TestSparkMaaSAI_GetType(t *testing.T) { + s := &SparkMaaSAI{BaseModel: BaseModel{Model: "test"}} + if got := s.GetType(); got != ModelTypeSparkMaaSAI { + t.Errorf("GetType() = %q, want %q", got, ModelTypeSparkMaaSAI) + } +} + func TestAgentCompressionConfig_UnmarshalJSON_NoSummarizer(t *testing.T) { data := []byte(`{ "compaction_interval": 5, diff --git a/go/api/config/crd/bases/kagent.dev_modelconfigs.yaml b/go/api/config/crd/bases/kagent.dev_modelconfigs.yaml index 00b21b6da..41dc8c62c 100644 --- a/go/api/config/crd/bases/kagent.dev_modelconfigs.yaml +++ b/go/api/config/crd/bases/kagent.dev_modelconfigs.yaml @@ -627,6 +627,7 @@ spec: - AnthropicVertexAI - Bedrock - SAPAICore + - SparkMaaSAI type: string sapAICore: description: SAP AI Core-specific configuration @@ -644,6 +645,14 @@ spec: required: - baseUrl type: object + sparkMaaSAI: + description: Spark MaaS AI-specific configuration + properties: + baseUrl: + default: https://maas-api.cn-huabei-1.xf-yun.com.cn + description: Base URL for the Spark MaaS API + type: string + type: object tls: description: |- TLS configuration for provider connections. @@ -706,6 +715,10 @@ spec: rule: '!(has(self.bedrock) && self.provider != ''Bedrock'')' - message: provider.sapAICore must be nil if the provider is not SAPAICore rule: '!(has(self.sapAICore) && self.provider != ''SAPAICore'')' + - message: provider.sparkMaaSAI must be nil if the provider is not SparkMaaSAI + rule: '!(has(self.sparkMaaSAI) && self.provider != ''SparkMaaSAI'')' + - message: provider.sparkMaaSAI must be set when provider is SparkMaaSAI + rule: '!(self.provider == ''SparkMaaSAI'' && !has(self.sparkMaaSAI))' - message: apiKeySecret must be set if apiKeySecretKey is set rule: '!(has(self.apiKeySecretKey) && !has(self.apiKeySecret))' - message: apiKeySecretKey must be set if apiKeySecret is set (except diff --git a/go/api/config/crd/bases/kagent.dev_modelproviderconfigs.yaml b/go/api/config/crd/bases/kagent.dev_modelproviderconfigs.yaml index 493e817e9..bca1bdd4c 100644 --- a/go/api/config/crd/bases/kagent.dev_modelproviderconfigs.yaml +++ b/go/api/config/crd/bases/kagent.dev_modelproviderconfigs.yaml @@ -91,6 +91,7 @@ spec: - AnthropicVertexAI - Bedrock - SAPAICore + - SparkMaaSAI type: string required: - type diff --git a/go/api/v1alpha2/modelconfig_types.go b/go/api/v1alpha2/modelconfig_types.go index 0d0892868..130ccf1dc 100644 --- a/go/api/v1alpha2/modelconfig_types.go +++ b/go/api/v1alpha2/modelconfig_types.go @@ -27,7 +27,7 @@ const ( ) // ModelProvider represents the model provider type -// +kubebuilder:validation:Enum=Anthropic;OpenAI;AzureOpenAI;Ollama;Gemini;GeminiVertexAI;AnthropicVertexAI;Bedrock;SAPAICore +// +kubebuilder:validation:Enum=Anthropic;OpenAI;AzureOpenAI;Ollama;Gemini;GeminiVertexAI;AnthropicVertexAI;Bedrock;SAPAICore;SparkMaaSAI type ModelProvider string const ( @@ -40,6 +40,7 @@ const ( ModelProviderAnthropicVertexAI ModelProvider = "AnthropicVertexAI" ModelProviderBedrock ModelProvider = "Bedrock" ModelProviderSAPAICore ModelProvider = "SAPAICore" + ModelProviderSparkMaaSAI ModelProvider = "SparkMaaSAI" ) type BaseVertexAIConfig struct { @@ -274,6 +275,14 @@ type SAPAICoreConfig struct { AuthURL string `json:"authUrl,omitempty"` } +// SparkMaaSAIConfig contains Spark MaaS AI-specific configuration options. +type SparkMaaSAIConfig struct { + // Base URL for the Spark MaaS API + // +kubebuilder:default="https://maas-api.cn-huabei-1.xf-yun.com.cn" + // +optional + BaseURL string `json:"baseUrl,omitempty"` +} + // TLSConfig contains TLS/SSL configuration options for model provider connections. // This enables agents to connect to internal LiteLLM gateways or other providers // that use self-signed certificates or custom certificate authorities. @@ -321,6 +330,8 @@ type TLSConfig struct { // +kubebuilder:validation:XValidation:message="provider.anthropicVertexAI must be nil if the provider is not AnthropicVertexAI",rule="!(has(self.anthropicVertexAI) && self.provider != 'AnthropicVertexAI')" // +kubebuilder:validation:XValidation:message="provider.bedrock must be nil if the provider is not Bedrock",rule="!(has(self.bedrock) && self.provider != 'Bedrock')" // +kubebuilder:validation:XValidation:message="provider.sapAICore must be nil if the provider is not SAPAICore",rule="!(has(self.sapAICore) && self.provider != 'SAPAICore')" +// +kubebuilder:validation:XValidation:message="provider.sparkMaaSAI must be nil if the provider is not SparkMaaSAI",rule="!(has(self.sparkMaaSAI) && self.provider != 'SparkMaaSAI')" +// +kubebuilder:validation:XValidation:message="provider.sparkMaaSAI must be set when provider is SparkMaaSAI",rule="!(self.provider == 'SparkMaaSAI' && !has(self.sparkMaaSAI))" // +kubebuilder:validation:XValidation:message="apiKeySecret must be set if apiKeySecretKey is set",rule="!(has(self.apiKeySecretKey) && !has(self.apiKeySecret))" // +kubebuilder:validation:XValidation:message="apiKeySecretKey must be set if apiKeySecret is set (except for Bedrock and SAPAICore providers)",rule="!(has(self.apiKeySecret) && !has(self.apiKeySecretKey) && self.provider != 'Bedrock' && self.provider != 'SAPAICore')" // +kubebuilder:validation:XValidation:message="apiKeyPassthrough and apiKeySecret are mutually exclusive",rule="!(has(self.apiKeyPassthrough) && self.apiKeyPassthrough && has(self.apiKeySecret) && size(self.apiKeySecret) > 0)" @@ -397,6 +408,10 @@ type ModelConfigSpec struct { // +optional SAPAICore *SAPAICoreConfig `json:"sapAICore,omitempty"` + // Spark MaaS AI-specific configuration + // +optional + SparkMaaSAI *SparkMaaSAIConfig `json:"sparkMaaSAI,omitempty"` + // TLS configuration for provider connections. // Enables agents to connect to internal LiteLLM gateways or other providers // that use self-signed certificates or custom certificate authorities. diff --git a/go/api/v1alpha2/zz_generated.deepcopy.go b/go/api/v1alpha2/zz_generated.deepcopy.go index 136058bce..510f550aa 100644 --- a/go/api/v1alpha2/zz_generated.deepcopy.go +++ b/go/api/v1alpha2/zz_generated.deepcopy.go @@ -1038,6 +1038,11 @@ func (in *ModelConfigSpec) DeepCopyInto(out *ModelConfigSpec) { *out = new(SAPAICoreConfig) **out = **in } + if in.SparkMaaSAI != nil { + in, out := &in.SparkMaaSAI, &out.SparkMaaSAI + *out = new(SparkMaaSAIConfig) + **out = **in + } if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSConfig) @@ -1755,6 +1760,21 @@ func (in *SkillsInitContainer) DeepCopy() *SkillsInitContainer { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SparkMaaSAIConfig) DeepCopyInto(out *SparkMaaSAIConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SparkMaaSAIConfig. +func (in *SparkMaaSAIConfig) DeepCopy() *SparkMaaSAIConfig { + if in == nil { + return nil + } + out := new(SparkMaaSAIConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSConfig) DeepCopyInto(out *TLSConfig) { *out = *in diff --git a/go/core/internal/controller/translator/agent/adk_api_translator.go b/go/core/internal/controller/translator/agent/adk_api_translator.go index 0ee9528f8..582f405ac 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator.go @@ -753,6 +753,36 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC sapAICore.APIKeyPassthrough = model.Spec.APIKeyPassthrough return sapAICore, modelDeploymentData, secretHashBytes, nil + case v1alpha2.ModelProviderSparkMaaSAI: + if !model.Spec.APIKeyPassthrough && model.Spec.APIKeySecret != "" { + modelDeploymentData.EnvVars = append(modelDeploymentData.EnvVars, corev1.EnvVar{ + Name: env.OpenAIAPIKey.Name(), + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: model.Spec.APIKeySecret, + }, + Key: model.Spec.APIKeySecretKey, + }, + }, + }) + } + + sparkMaaSAI := &adk.SparkMaaSAI{ + BaseModel: adk.BaseModel{ + Model: model.Spec.Model, + Headers: model.Spec.DefaultHeaders, + }, + BaseUrl: "https://maas-api.cn-huabei-1.xf-yun.com.cn", + } + if model.Spec.SparkMaaSAI != nil && model.Spec.SparkMaaSAI.BaseURL != "" { + sparkMaaSAI.BaseUrl = model.Spec.SparkMaaSAI.BaseURL + } + + populateTLSFields(&sparkMaaSAI.BaseModel, model.Spec.TLS) + sparkMaaSAI.APIKeyPassthrough = model.Spec.APIKeyPassthrough + + return sparkMaaSAI, modelDeploymentData, secretHashBytes, nil default: return nil, nil, nil, fmt.Errorf("unsupported model provider: %s", model.Spec.Provider) } diff --git a/go/core/internal/controller/translator/agent/adk_api_translator_test.go b/go/core/internal/controller/translator/agent/adk_api_translator_test.go index 6f0dbb80f..6078b2903 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator_test.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator_test.go @@ -359,6 +359,141 @@ func Test_AdkApiTranslator_CrossNamespaceRemoteMCPServer(t *testing.T) { } } +func Test_AdkApiTranslator_SparkMaaSAIProvider(t *testing.T) { + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + + namespace := "test-ns" + modelName := "spark-model" + agentName := "test-agent" + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: namespace}, + } + + apiSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "spark-api-key", Namespace: namespace}, + Data: map[string][]byte{"api-key": []byte("test-spark-key-123")}, + } + + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{Name: modelName, Namespace: namespace}, + Spec: v1alpha2.ModelConfigSpec{ + Model: "Spark5.0-Pro", + Provider: v1alpha2.ModelProviderSparkMaaSAI, + APIKeySecret: "spark-api-key", + APIKeySecretKey: "api-key", + SparkMaaSAI: &v1alpha2.SparkMaaSAIConfig{ + BaseURL: "https://maas-api.cn-huabei-1.xf-yun.com.cn", + }, + }, + } + + agent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: agentName, Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Description: "Test Agent", + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "System message", + ModelConfig: modelName, + }, + }, + } + + kubeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns, apiSecret, modelConfig, agent). + Build() + + defaultModel := types.NamespacedName{Namespace: namespace, Name: modelName} + + trans := translator.NewAdkApiTranslator(kubeClient, defaultModel, nil, "", nil) + + outputs, err := translator.TranslateAgent(context.Background(), trans, agent) + require.NoError(t, err) + require.NotNil(t, outputs) + require.NotNil(t, outputs.Config) + + // Verify model type is SparkMaaSAI + sparkModel, ok := outputs.Config.Model.(*adk.SparkMaaSAI) + require.True(t, ok, "Expected model to be *adk.SparkMaaSAI, got %T", outputs.Config.Model) + assert.Equal(t, "Spark5.0-Pro", sparkModel.Model) + assert.Equal(t, "https://maas-api.cn-huabei-1.xf-yun.com.cn", sparkModel.BaseUrl) + assert.False(t, sparkModel.APIKeyPassthrough) + + // Verify env var for OPENAI_API_KEY is set + var foundOpenAIAPIKey bool + for _, obj := range outputs.Manifest { + if dep, ok := obj.(*appsv1.Deployment); ok { + for _, env := range dep.Spec.Template.Spec.Containers[0].Env { + if env.Name == "OPENAI_API_KEY" && env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil { + assert.Equal(t, "spark-api-key", env.ValueFrom.SecretKeyRef.Name) + assert.Equal(t, "api-key", env.ValueFrom.SecretKeyRef.Key) + foundOpenAIAPIKey = true + } + } + } + } + assert.True(t, foundOpenAIAPIKey, "OPENAI_API_KEY env var should be set from secret") +} + +func Test_AdkApiTranslator_SparkMaaSAIProvider_Passthrough(t *testing.T) { + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + + namespace := "test-ns" + modelName := "spark-pt" + + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{Name: modelName, Namespace: namespace}, + Spec: v1alpha2.ModelConfigSpec{ + Model: "qwen-plus", + Provider: v1alpha2.ModelProviderSparkMaaSAI, + APIKeyPassthrough: true, + SparkMaaSAI: &v1alpha2.SparkMaaSAIConfig{}, + }, + } + + agent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "pt-agent", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Description: "Test Agent", + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "System message", + ModelConfig: modelName, + }, + }, + } + + kubeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns, modelConfig, agent). + Build() + + trans := translator.NewAdkApiTranslator(kubeClient, types.NamespacedName{Namespace: namespace, Name: modelName}, nil, "", nil) + + outputs, err := translator.TranslateAgent(context.Background(), trans, agent) + require.NoError(t, err) + + sparkModel, ok := outputs.Config.Model.(*adk.SparkMaaSAI) + require.True(t, ok) + assert.True(t, sparkModel.APIKeyPassthrough) + assert.Equal(t, "qwen-plus", sparkModel.Model) + + // Verify no OPENAI_API_KEY env var is set when using passthrough + for _, obj := range outputs.Manifest { + if dep, ok := obj.(*appsv1.Deployment); ok { + for _, env := range dep.Spec.Template.Spec.Containers[0].Env { + assert.NotEqual(t, "OPENAI_API_KEY", env.Name, "OPENAI_API_KEY should not be set with passthrough") + } + } + } +} + func Test_AdkApiTranslator_OllamaOptions(t *testing.T) { scheme := schemev1.Scheme require.NoError(t, v1alpha2.AddToScheme(scheme)) diff --git a/go/core/internal/httpserver/handlers/modelproviderconfig.go b/go/core/internal/httpserver/handlers/modelproviderconfig.go index 226df54d3..dfbc5b487 100644 --- a/go/core/internal/httpserver/handlers/modelproviderconfig.go +++ b/go/core/internal/httpserver/handlers/modelproviderconfig.go @@ -51,6 +51,8 @@ func getRequiredKeysForModelProvider(providerType v1alpha2.ModelProvider) []stri return []string{"region"} case v1alpha2.ModelProviderSAPAICore: return []string{"baseUrl"} + case v1alpha2.ModelProviderSparkMaaSAI: + return []string{} case v1alpha2.ModelProviderOpenAI, v1alpha2.ModelProviderAnthropic, v1alpha2.ModelProviderOllama: // These providers currently have no fields marked as strictly required in the API definition return []string{} @@ -128,6 +130,7 @@ func (h *ModelProviderConfigHandler) HandleListSupportedModelProviders(w ErrorRe {v1alpha2.ModelProviderAnthropicVertexAI, reflect.TypeFor[v1alpha2.AnthropicVertexAIConfig]()}, {v1alpha2.ModelProviderBedrock, reflect.TypeFor[v1alpha2.BedrockConfig]()}, {v1alpha2.ModelProviderSAPAICore, reflect.TypeFor[v1alpha2.SAPAICoreConfig]()}, + {v1alpha2.ModelProviderSparkMaaSAI, reflect.TypeFor[v1alpha2.SparkMaaSAIConfig]()}, } providersResponse := []map[string]any{} diff --git a/go/core/internal/httpserver/handlers/models.go b/go/core/internal/httpserver/handlers/models.go index ec71c2d04..b823fc2db 100644 --- a/go/core/internal/httpserver/handlers/models.go +++ b/go/core/internal/httpserver/handlers/models.go @@ -157,6 +157,24 @@ func (h *ModelHandler) HandleListSupportedModels(w ErrorResponseWriter, r *http. {Name: "sonar", FunctionCalling: false}, {Name: "sap-abap-1", FunctionCalling: false}, }, + v1alpha2.ModelProviderSparkMaaSAI: { + {Name: "Spark5.0-Pro", FunctionCalling: true}, + {Name: "Spark5.0-Lite", FunctionCalling: true}, + {Name: "Spark4.0-Ultra", FunctionCalling: true}, + {Name: "Spark4.0-Max", FunctionCalling: true}, + {Name: "Spark-Max-32K", FunctionCalling: true}, + {Name: "Spark-Pro-128K", FunctionCalling: true}, + {Name: "Spark-Lite", FunctionCalling: true}, + {Name: "deepseek-v3", FunctionCalling: true}, + {Name: "deepseek-r1", FunctionCalling: true}, + {Name: "qwen-plus", FunctionCalling: true}, + {Name: "qwen-turbo", FunctionCalling: true}, + {Name: "qwen-max", FunctionCalling: true}, + {Name: "qwen-long", FunctionCalling: true}, + {Name: "glm-4-plus", FunctionCalling: true}, + {Name: "glm-4-flash", FunctionCalling: true}, + {Name: "MiniMax-M1", FunctionCalling: true}, + }, } log.Info("Successfully listed supported models", "count", len(supportedModels)) diff --git a/helm/kagent-crds/templates/kagent.dev_modelconfigs.yaml b/helm/kagent-crds/templates/kagent.dev_modelconfigs.yaml index 00b21b6da..41dc8c62c 100644 --- a/helm/kagent-crds/templates/kagent.dev_modelconfigs.yaml +++ b/helm/kagent-crds/templates/kagent.dev_modelconfigs.yaml @@ -627,6 +627,7 @@ spec: - AnthropicVertexAI - Bedrock - SAPAICore + - SparkMaaSAI type: string sapAICore: description: SAP AI Core-specific configuration @@ -644,6 +645,14 @@ spec: required: - baseUrl type: object + sparkMaaSAI: + description: Spark MaaS AI-specific configuration + properties: + baseUrl: + default: https://maas-api.cn-huabei-1.xf-yun.com.cn + description: Base URL for the Spark MaaS API + type: string + type: object tls: description: |- TLS configuration for provider connections. @@ -706,6 +715,10 @@ spec: rule: '!(has(self.bedrock) && self.provider != ''Bedrock'')' - message: provider.sapAICore must be nil if the provider is not SAPAICore rule: '!(has(self.sapAICore) && self.provider != ''SAPAICore'')' + - message: provider.sparkMaaSAI must be nil if the provider is not SparkMaaSAI + rule: '!(has(self.sparkMaaSAI) && self.provider != ''SparkMaaSAI'')' + - message: provider.sparkMaaSAI must be set when provider is SparkMaaSAI + rule: '!(self.provider == ''SparkMaaSAI'' && !has(self.sparkMaaSAI))' - message: apiKeySecret must be set if apiKeySecretKey is set rule: '!(has(self.apiKeySecretKey) && !has(self.apiKeySecret))' - message: apiKeySecretKey must be set if apiKeySecret is set (except diff --git a/helm/kagent-crds/templates/kagent.dev_modelproviderconfigs.yaml b/helm/kagent-crds/templates/kagent.dev_modelproviderconfigs.yaml index 493e817e9..bca1bdd4c 100644 --- a/helm/kagent-crds/templates/kagent.dev_modelproviderconfigs.yaml +++ b/helm/kagent-crds/templates/kagent.dev_modelproviderconfigs.yaml @@ -91,6 +91,7 @@ spec: - AnthropicVertexAI - Bedrock - SAPAICore + - SparkMaaSAI type: string required: - type diff --git a/python/packages/kagent-adk/src/kagent/adk/types.py b/python/packages/kagent-adk/src/kagent/adk/types.py index 5e2f4a97a..6ea2b6fef 100644 --- a/python/packages/kagent-adk/src/kagent/adk/types.py +++ b/python/packages/kagent-adk/src/kagent/adk/types.py @@ -250,7 +250,14 @@ class SAPAICore(BaseLLM): type: Literal["sap_ai_core"] -ModelUnion = Union[OpenAI, Anthropic, GeminiVertexAI, GeminiAnthropic, Ollama, AzureOpenAI, Gemini, Bedrock, SAPAICore] +class SparkMaaSAI(BaseLLM): + base_url: str | None = None + type: Literal["spark_maas_ai"] + + +ModelUnion = Union[ + OpenAI, Anthropic, GeminiVertexAI, GeminiAnthropic, Ollama, AzureOpenAI, Gemini, Bedrock, SAPAICore, SparkMaaSAI +] class ContextCompressionSettings(BaseModel): @@ -612,6 +619,15 @@ def _create_llm_from_model_config(model_config: ModelUnion): auth_url=model_config.auth_url, **_transport_kwargs(model_config), ) + if model_config.type == "spark_maas_ai": + spark_base_url = base_url or "https://maas-api.cn-huabei-1.xf-yun.com.cn" + return OpenAINative( + type="openai", + base_url=spark_base_url, + default_headers=extra_headers, + model=model_config.model, + **_transport_kwargs(model_config), + ) raise ValueError(f"Invalid model type: {model_config.type}") diff --git a/python/packages/kagent-adk/tests/unittests/models/test_sap_ai_core.py b/python/packages/kagent-adk/tests/unittests/models/test_sap_ai_core.py index aace9575a..fab636b54 100644 --- a/python/packages/kagent-adk/tests/unittests/models/test_sap_ai_core.py +++ b/python/packages/kagent-adk/tests/unittests/models/test_sap_ai_core.py @@ -17,7 +17,6 @@ _parse_orchestration_chunk, ) - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/python/packages/kagent-core/src/kagent/core/a2a/__init__.py b/python/packages/kagent-core/src/kagent/core/a2a/__init__.py index 3de48d635..63c200a9f 100644 --- a/python/packages/kagent-core/src/kagent/core/a2a/__init__.py +++ b/python/packages/kagent-core/src/kagent/core/a2a/__init__.py @@ -1,5 +1,4 @@ from ._config import get_a2a_max_content_length -from ._context import get_request_user_id, set_request_user_id from ._consts import ( A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY, A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT, @@ -18,6 +17,7 @@ get_kagent_metadata_key, read_metadata_value, ) +from ._context import get_request_user_id, set_request_user_id from ._hitl_utils import ( DecisionType, HitlPartInfo, diff --git a/ui/src/app/models/new/page.tsx b/ui/src/app/models/new/page.tsx index 5a73e583d..c7ff680a4 100644 --- a/ui/src/app/models/new/page.tsx +++ b/ui/src/app/models/new/page.tsx @@ -21,6 +21,7 @@ import type { AnthropicVertexAIConfig, BedrockConfig, SAPAICoreConfigPayload, + SparkMaaSAIConfigPayload, ProviderModelsResponse, } from "@/types"; import { toast } from "sonner"; @@ -248,7 +249,7 @@ function ModelPageContent() { const spec = modelData.spec; const fetchedParams: Record = (spec.openAI ?? spec.anthropic ?? spec.azureOpenAI ?? spec.ollama ?? - spec.gemini ?? spec.geminiVertexAI ?? spec.anthropicVertexAI ?? spec.bedrock ?? spec.sapAICore ?? {}) as Record; + spec.gemini ?? spec.geminiVertexAI ?? spec.anthropicVertexAI ?? spec.bedrock ?? spec.sapAICore ?? spec.sparkMaaSAI ?? {}) as Record; if (provider?.type === 'Ollama') { setModelTag(extractedTag || 'latest'); @@ -612,6 +613,9 @@ function ModelPageContent() { case 'SAPAICore': spec.sapAICore = providerParams as SAPAICoreConfigPayload; break; + case 'SparkMaaSAI': + spec.sparkMaaSAI = providerParams as SparkMaaSAIConfigPayload; + break; default: console.error("Unsupported provider type during payload construction:", providerType); toast.error("Internal error: Unsupported provider type."); diff --git a/ui/src/components/ModelProviderCombobox.tsx b/ui/src/components/ModelProviderCombobox.tsx index 26da4affc..80f4aee8e 100644 --- a/ui/src/components/ModelProviderCombobox.tsx +++ b/ui/src/components/ModelProviderCombobox.tsx @@ -13,6 +13,7 @@ import { Azure } from './icons/Azure'; import { Gemini } from './icons/Gemini'; import { Bedrock } from './icons/Bedrock'; import { SAPAICore } from './icons/SAPAICore'; +import { SparkMaaSAI } from './icons/SparkMaaSAI'; interface ComboboxOption { label: string; // e.g., "OpenAI - gpt-4o" @@ -68,6 +69,7 @@ export function ModelProviderCombobox({ 'AnthropicVertexAI': Anthropic, 'Bedrock': Bedrock, 'SAPAICore': SAPAICore, + 'SparkMaaSAI': SparkMaaSAI, }; if (!providerKey || !PROVIDER_ICONS[providerKey]) { return null; diff --git a/ui/src/components/ProviderCombobox.tsx b/ui/src/components/ProviderCombobox.tsx index 4202ac031..fd7e27078 100644 --- a/ui/src/components/ProviderCombobox.tsx +++ b/ui/src/components/ProviderCombobox.tsx @@ -13,6 +13,7 @@ import { Azure } from './icons/Azure'; import { Gemini } from './icons/Gemini'; import { Bedrock } from './icons/Bedrock'; import { SAPAICore } from './icons/SAPAICore'; +import { SparkMaaSAI } from './icons/SparkMaaSAI'; const PROVIDER_ICONS: Record> = { 'OpenAI': OpenAI, @@ -24,6 +25,7 @@ const PROVIDER_ICONS: Record + + + ); +} diff --git a/ui/src/components/models/ModelsListSection.tsx b/ui/src/components/models/ModelsListSection.tsx index c9c5f68a9..da5a523e8 100644 --- a/ui/src/components/models/ModelsListSection.tsx +++ b/ui/src/components/models/ModelsListSection.tsx @@ -25,6 +25,7 @@ function getProviderParams(spec: ModelConfigSpec) { spec.anthropicVertexAI ?? spec.bedrock ?? spec.sapAICore ?? + spec.sparkMaaSAI ?? undefined ); } diff --git a/ui/src/lib/providers.ts b/ui/src/lib/providers.ts index 8bcf889ca..4758da01f 100644 --- a/ui/src/lib/providers.ts +++ b/ui/src/lib/providers.ts @@ -1,6 +1,6 @@ -export type BackendModelProviderType = "OpenAI" | "AzureOpenAI" | "Anthropic" | "Ollama" | "Gemini" | "GeminiVertexAI" | "AnthropicVertexAI" | "Bedrock" | "SAPAICore"; -export const modelProviders = ["OpenAI", "AzureOpenAI", "Anthropic", "Ollama", "Gemini", "GeminiVertexAI", "AnthropicVertexAI", "Bedrock", "SAPAICore"] as const; +export type BackendModelProviderType = "OpenAI" | "AzureOpenAI" | "Anthropic" | "Ollama" | "Gemini" | "GeminiVertexAI" | "AnthropicVertexAI" | "Bedrock" | "SAPAICore" | "SparkMaaSAI"; +export const modelProviders = ["OpenAI", "AzureOpenAI", "Anthropic", "Ollama", "Gemini", "GeminiVertexAI", "AnthropicVertexAI", "Bedrock", "SAPAICore", "SparkMaaSAI"] as const; export type ModelProviderKey = typeof modelProviders[number]; @@ -76,6 +76,13 @@ export const PROVIDERS_INFO: { modelDocsLink: "https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/models-and-scenarios-in-generative-ai-hub", help: "Create a K8s Secret with client_id and client_secret from your SAP AI Core service key." }, + SparkMaaSAI: { + name: "Spark MaaS", + type: "SparkMaaSAI", + apiKeyLink: "https://www.xfyun.cn/", + modelDocsLink: "https://www.xfyun.cn/doc/spark/推理服务-http.html", + help: "Configure your Spark MaaS API key. Uses OpenAI-compatible protocol." + }, }; export const isValidProviderInfoKey = (key: string): key is ModelProviderKey => { diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index b4a441ce8..3f65b2f1d 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -73,6 +73,10 @@ export interface SAPAICoreConfigPayload { authUrl?: string; } +export interface SparkMaaSAIConfigPayload { + baseUrl?: string; +} + export interface BedrockConfig { region: string; } @@ -101,6 +105,7 @@ export interface ModelConfigSpec { anthropicVertexAI?: AnthropicVertexAIConfig; bedrock?: BedrockConfig; sapAICore?: SAPAICoreConfigPayload; + sparkMaaSAI?: SparkMaaSAIConfigPayload; } export interface ModelConfig {