Skip to content

Commit 3ef9f38

Browse files
committed
add spark maas ai
Signed-off-by: dongjiang <dongjiang1989@126.com>
1 parent 62bd371 commit 3ef9f38

23 files changed

Lines changed: 428 additions & 7 deletions

File tree

go/adk/pkg/agent/agent.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,14 @@ func CreateLLM(ctx context.Context, m adk.Model, log logr.Logger) (adkmodel.LLM,
331331
}
332332
return models.NewSAPAICoreModelWithLogger(cfg, log)
333333

334+
case *adk.SparkMaaSAI:
335+
cfg := &models.OpenAIConfig{
336+
TransportConfig: transportConfigFromBase(m.BaseModel, nil),
337+
Model: m.Model,
338+
BaseUrl: m.BaseUrl,
339+
}
340+
return models.NewOpenAIModelWithLogger(cfg, log)
341+
334342
default:
335343
return nil, fmt.Errorf("unsupported model type: %s", m.GetType())
336344
}

go/api/adk/types.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ const (
104104
ModelTypeGemini = "gemini"
105105
ModelTypeBedrock = "bedrock"
106106
ModelTypeSAPAICore = "sap_ai_core"
107+
ModelTypeSparkMaaSAI = "spark_maas_ai"
107108
)
108109

109110
func (o *OpenAI) MarshalJSON() ([]byte, error) {
@@ -290,6 +291,26 @@ func (s *SAPAICore) GetType() string {
290291
return ModelTypeSAPAICore
291292
}
292293

294+
type SparkMaaSAI struct {
295+
BaseModel
296+
BaseUrl string `json:"base_url,omitempty"`
297+
}
298+
299+
func (s *SparkMaaSAI) MarshalJSON() ([]byte, error) {
300+
type Alias SparkMaaSAI
301+
return json.Marshal(&struct {
302+
Type string `json:"type"`
303+
*Alias
304+
}{
305+
Type: ModelTypeSparkMaaSAI,
306+
Alias: (*Alias)(s),
307+
})
308+
}
309+
310+
func (s *SparkMaaSAI) GetType() string {
311+
return ModelTypeSparkMaaSAI
312+
}
313+
293314
// GenericModel is a catch-all model type used by the Go ADK when the model
294315
// type doesn't match any known constant.
295316
type GenericModel struct {
@@ -358,6 +379,12 @@ func ParseModel(bytes []byte) (Model, error) {
358379
return nil, err
359380
}
360381
return &sapAICore, nil
382+
case ModelTypeSparkMaaSAI:
383+
var sparkMaaSAI SparkMaaSAI
384+
if err := json.Unmarshal(bytes, &sparkMaaSAI); err != nil {
385+
return nil, err
386+
}
387+
return &sparkMaaSAI, nil
361388
}
362389
return nil, fmt.Errorf("unknown model type: %s", model.Type)
363390
}
@@ -426,6 +453,9 @@ func ModelToEmbeddingConfig(m Model) *EmbeddingConfig {
426453
case *SAPAICore:
427454
e.Model = v.Model
428455
e.BaseUrl = v.BaseUrl
456+
case *SparkMaaSAI:
457+
e.Model = v.Model
458+
e.BaseUrl = v.BaseUrl
429459
default:
430460
e.Model = ""
431461
}

go/api/adk/types_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ func TestMarshalJSON_TypeDiscriminator(t *testing.T) {
1919
{name: "Ollama", model: &Ollama{BaseModel: BaseModel{Model: "llama3"}}, wantType: ModelTypeOllama},
2020
{name: "Gemini", model: &Gemini{BaseModel: BaseModel{Model: "gemini-pro"}}, wantType: ModelTypeGemini},
2121
{name: "Bedrock", model: &Bedrock{BaseModel: BaseModel{Model: "claude-v2"}}, wantType: ModelTypeBedrock},
22+
{name: "SAPAICore", model: &SAPAICore{BaseModel: BaseModel{Model: "claude-3-sonnet"}, BaseUrl: "https://api.example.com"}, wantType: ModelTypeSAPAICore},
23+
{name: "SparkMaaSAI", model: &SparkMaaSAI{BaseModel: BaseModel{Model: "Spark5.0-Pro"}, BaseUrl: "https://maas-api.cn-huabei-1.xf-yun.com.cn"}, wantType: ModelTypeSparkMaaSAI},
2224
}
2325

2426
for _, tt := range tests {
@@ -115,6 +117,7 @@ func TestMarshalJSON_BaseModelFields(t *testing.T) {
115117
{name: "Ollama", model: &Ollama{BaseModel: base}},
116118
{name: "Gemini", model: &Gemini{BaseModel: base}},
117119
{name: "Bedrock", model: &Bedrock{BaseModel: base}},
120+
{name: "SparkMaaSAI", model: &SparkMaaSAI{BaseModel: base}},
118121
}
119122

120123
for _, tt := range tests {
@@ -245,6 +248,30 @@ func TestMarshalJSON_TypeSpecificFields(t *testing.T) {
245248
t.Errorf("region = %v, want %q", raw["region"], "us-east-1")
246249
}
247250
})
251+
252+
t.Run("SparkMaaSAI base_url", func(t *testing.T) {
253+
m := &SparkMaaSAI{
254+
BaseModel: BaseModel{Model: "Spark5.0-Pro"},
255+
BaseUrl: "https://maas-api.cn-huabei-1.xf-yun.com.cn",
256+
}
257+
data, err := json.Marshal(m)
258+
if err != nil {
259+
t.Fatalf("MarshalJSON() error = %v", err)
260+
}
261+
var raw map[string]any
262+
if err := json.Unmarshal(data, &raw); err != nil {
263+
t.Fatalf("failed to unmarshal: %v", err)
264+
}
265+
if raw["type"] != "spark_maas_ai" {
266+
t.Errorf("type = %v, want %q", raw["type"], "spark_maas_ai")
267+
}
268+
if raw["base_url"] != "https://maas-api.cn-huabei-1.xf-yun.com.cn" {
269+
t.Errorf("base_url = %v, want %q", raw["base_url"], "https://maas-api.cn-huabei-1.xf-yun.com.cn")
270+
}
271+
if raw["model"] != "Spark5.0-Pro" {
272+
t.Errorf("model = %v, want %q", raw["model"], "Spark5.0-Pro")
273+
}
274+
})
248275
}
249276

250277
func TestAgentConfig_UnmarshalJSON_Network(t *testing.T) {
@@ -341,6 +368,14 @@ func TestParseModel_Roundtrip(t *testing.T) {
341368
},
342369
wantType: ModelTypeBedrock,
343370
},
371+
{
372+
name: "SparkMaaSAI roundtrip",
373+
model: &SparkMaaSAI{
374+
BaseModel: BaseModel{Model: "Spark5.0-Pro", Headers: map[string]string{"X-Custom": "val"}},
375+
BaseUrl: "https://maas-api.cn-huabei-1.xf-yun.com.cn",
376+
},
377+
wantType: ModelTypeSparkMaaSAI,
378+
},
344379
}
345380

346381
for _, tt := range tests {
@@ -759,6 +794,59 @@ func TestAgentConfig_UnmarshalJSON_InvalidJSON(t *testing.T) {
759794
}
760795
}
761796

797+
func TestParseModel_SparkMaaSAI(t *testing.T) {
798+
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}`)
799+
800+
parsed, err := ParseModel(data)
801+
if err != nil {
802+
t.Fatalf("ParseModel() error = %v", err)
803+
}
804+
805+
spark, ok := parsed.(*SparkMaaSAI)
806+
if !ok {
807+
t.Fatalf("ParseModel() returned %T, want *SparkMaaSAI", parsed)
808+
}
809+
if spark.Model != "Spark5.0-Pro" {
810+
t.Errorf("Model = %q, want %q", spark.Model, "Spark5.0-Pro")
811+
}
812+
if spark.BaseUrl != "https://maas-api.cn-huabei-1.xf-yun.com.cn" {
813+
t.Errorf("BaseUrl = %q, want %q", spark.BaseUrl, "https://maas-api.cn-huabei-1.xf-yun.com.cn")
814+
}
815+
if !spark.APIKeyPassthrough {
816+
t.Error("APIKeyPassthrough = false, want true")
817+
}
818+
if spark.Headers["X-Test"] != "v" {
819+
t.Errorf("Headers[X-Test] = %q, want %q", spark.Headers["X-Test"], "v")
820+
}
821+
}
822+
823+
func TestModelToEmbeddingConfig_SparkMaaSAI(t *testing.T) {
824+
m := &SparkMaaSAI{
825+
BaseModel: BaseModel{Model: "qwen-plus"},
826+
BaseUrl: "https://maas-api.cn-huabei-1.xf-yun.com.cn",
827+
}
828+
e := ModelToEmbeddingConfig(m)
829+
if e == nil {
830+
t.Fatal("ModelToEmbeddingConfig() returned nil")
831+
}
832+
if e.Provider != "spark_maas_ai" {
833+
t.Errorf("Provider = %q, want %q", e.Provider, "spark_maas_ai")
834+
}
835+
if e.Model != "qwen-plus" {
836+
t.Errorf("Model = %q, want %q", e.Model, "qwen-plus")
837+
}
838+
if e.BaseUrl != "https://maas-api.cn-huabei-1.xf-yun.com.cn" {
839+
t.Errorf("BaseUrl = %q, want %q", e.BaseUrl, "https://maas-api.cn-huabei-1.xf-yun.com.cn")
840+
}
841+
}
842+
843+
func TestSparkMaaSAI_GetType(t *testing.T) {
844+
s := &SparkMaaSAI{BaseModel: BaseModel{Model: "test"}}
845+
if got := s.GetType(); got != ModelTypeSparkMaaSAI {
846+
t.Errorf("GetType() = %q, want %q", got, ModelTypeSparkMaaSAI)
847+
}
848+
}
849+
762850
func TestAgentCompressionConfig_UnmarshalJSON_NoSummarizer(t *testing.T) {
763851
data := []byte(`{
764852
"compaction_interval": 5,

go/api/config/crd/bases/kagent.dev_modelconfigs.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ spec:
627627
- AnthropicVertexAI
628628
- Bedrock
629629
- SAPAICore
630+
- SparkMaaSAI
630631
type: string
631632
sapAICore:
632633
description: SAP AI Core-specific configuration
@@ -644,6 +645,14 @@ spec:
644645
required:
645646
- baseUrl
646647
type: object
648+
sparkMaaSAI:
649+
description: Spark MaaS AI-specific configuration
650+
properties:
651+
baseUrl:
652+
default: https://maas-api.cn-huabei-1.xf-yun.com.cn
653+
description: Base URL for the Spark MaaS API
654+
type: string
655+
type: object
647656
tls:
648657
description: |-
649658
TLS configuration for provider connections.
@@ -706,6 +715,10 @@ spec:
706715
rule: '!(has(self.bedrock) && self.provider != ''Bedrock'')'
707716
- message: provider.sapAICore must be nil if the provider is not SAPAICore
708717
rule: '!(has(self.sapAICore) && self.provider != ''SAPAICore'')'
718+
- message: provider.sparkMaaSAI must be nil if the provider is not SparkMaaSAI
719+
rule: '!(has(self.sparkMaaSAI) && self.provider != ''SparkMaaSAI'')'
720+
- message: provider.sparkMaaSAI must be set when provider is SparkMaaSAI
721+
rule: '!(self.provider == ''SparkMaaSAI'' && !has(self.sparkMaaSAI))'
709722
- message: apiKeySecret must be set if apiKeySecretKey is set
710723
rule: '!(has(self.apiKeySecretKey) && !has(self.apiKeySecret))'
711724
- message: apiKeySecretKey must be set if apiKeySecret is set (except

go/api/config/crd/bases/kagent.dev_modelproviderconfigs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ spec:
9191
- AnthropicVertexAI
9292
- Bedrock
9393
- SAPAICore
94+
- SparkMaaSAI
9495
type: string
9596
required:
9697
- type

go/api/v1alpha2/modelconfig_types.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const (
2727
)
2828

2929
// ModelProvider represents the model provider type
30-
// +kubebuilder:validation:Enum=Anthropic;OpenAI;AzureOpenAI;Ollama;Gemini;GeminiVertexAI;AnthropicVertexAI;Bedrock;SAPAICore
30+
// +kubebuilder:validation:Enum=Anthropic;OpenAI;AzureOpenAI;Ollama;Gemini;GeminiVertexAI;AnthropicVertexAI;Bedrock;SAPAICore;SparkMaaSAI
3131
type ModelProvider string
3232

3333
const (
@@ -40,6 +40,7 @@ const (
4040
ModelProviderAnthropicVertexAI ModelProvider = "AnthropicVertexAI"
4141
ModelProviderBedrock ModelProvider = "Bedrock"
4242
ModelProviderSAPAICore ModelProvider = "SAPAICore"
43+
ModelProviderSparkMaaSAI ModelProvider = "SparkMaaSAI"
4344
)
4445

4546
type BaseVertexAIConfig struct {
@@ -274,6 +275,14 @@ type SAPAICoreConfig struct {
274275
AuthURL string `json:"authUrl,omitempty"`
275276
}
276277

278+
// SparkMaaSAIConfig contains Spark MaaS AI-specific configuration options.
279+
type SparkMaaSAIConfig struct {
280+
// Base URL for the Spark MaaS API
281+
// +kubebuilder:default="https://maas-api.cn-huabei-1.xf-yun.com.cn"
282+
// +optional
283+
BaseURL string `json:"baseUrl,omitempty"`
284+
}
285+
277286
// TLSConfig contains TLS/SSL configuration options for model provider connections.
278287
// This enables agents to connect to internal LiteLLM gateways or other providers
279288
// that use self-signed certificates or custom certificate authorities.
@@ -321,6 +330,8 @@ type TLSConfig struct {
321330
// +kubebuilder:validation:XValidation:message="provider.anthropicVertexAI must be nil if the provider is not AnthropicVertexAI",rule="!(has(self.anthropicVertexAI) && self.provider != 'AnthropicVertexAI')"
322331
// +kubebuilder:validation:XValidation:message="provider.bedrock must be nil if the provider is not Bedrock",rule="!(has(self.bedrock) && self.provider != 'Bedrock')"
323332
// +kubebuilder:validation:XValidation:message="provider.sapAICore must be nil if the provider is not SAPAICore",rule="!(has(self.sapAICore) && self.provider != 'SAPAICore')"
333+
// +kubebuilder:validation:XValidation:message="provider.sparkMaaSAI must be nil if the provider is not SparkMaaSAI",rule="!(has(self.sparkMaaSAI) && self.provider != 'SparkMaaSAI')"
334+
// +kubebuilder:validation:XValidation:message="provider.sparkMaaSAI must be set when provider is SparkMaaSAI",rule="!(self.provider == 'SparkMaaSAI' && !has(self.sparkMaaSAI))"
324335
// +kubebuilder:validation:XValidation:message="apiKeySecret must be set if apiKeySecretKey is set",rule="!(has(self.apiKeySecretKey) && !has(self.apiKeySecret))"
325336
// +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')"
326337
// +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 {
397408
// +optional
398409
SAPAICore *SAPAICoreConfig `json:"sapAICore,omitempty"`
399410

411+
// Spark MaaS AI-specific configuration
412+
// +optional
413+
SparkMaaSAI *SparkMaaSAIConfig `json:"sparkMaaSAI,omitempty"`
414+
400415
// TLS configuration for provider connections.
401416
// Enables agents to connect to internal LiteLLM gateways or other providers
402417
// that use self-signed certificates or custom certificate authorities.

go/api/v1alpha2/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go/core/internal/controller/translator/agent/adk_api_translator.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,36 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC
753753
sapAICore.APIKeyPassthrough = model.Spec.APIKeyPassthrough
754754

755755
return sapAICore, modelDeploymentData, secretHashBytes, nil
756+
case v1alpha2.ModelProviderSparkMaaSAI:
757+
if !model.Spec.APIKeyPassthrough && model.Spec.APIKeySecret != "" {
758+
modelDeploymentData.EnvVars = append(modelDeploymentData.EnvVars, corev1.EnvVar{
759+
Name: env.OpenAIAPIKey.Name(),
760+
ValueFrom: &corev1.EnvVarSource{
761+
SecretKeyRef: &corev1.SecretKeySelector{
762+
LocalObjectReference: corev1.LocalObjectReference{
763+
Name: model.Spec.APIKeySecret,
764+
},
765+
Key: model.Spec.APIKeySecretKey,
766+
},
767+
},
768+
})
769+
}
770+
771+
sparkMaaSAI := &adk.SparkMaaSAI{
772+
BaseModel: adk.BaseModel{
773+
Model: model.Spec.Model,
774+
Headers: model.Spec.DefaultHeaders,
775+
},
776+
BaseUrl: "https://maas-api.cn-huabei-1.xf-yun.com.cn",
777+
}
778+
if model.Spec.SparkMaaSAI != nil && model.Spec.SparkMaaSAI.BaseURL != "" {
779+
sparkMaaSAI.BaseUrl = model.Spec.SparkMaaSAI.BaseURL
780+
}
781+
782+
populateTLSFields(&sparkMaaSAI.BaseModel, model.Spec.TLS)
783+
sparkMaaSAI.APIKeyPassthrough = model.Spec.APIKeyPassthrough
784+
785+
return sparkMaaSAI, modelDeploymentData, secretHashBytes, nil
756786
default:
757787
return nil, nil, nil, fmt.Errorf("unsupported model provider: %s", model.Spec.Provider)
758788
}

0 commit comments

Comments
 (0)