Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions go/adk/pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown
Contributor

@supreme-gg-gg supreme-gg-gg May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is completely OpenAI compatible, wouldn't you be able to use this already with Kagent's BYO OpenAI by just setting the baseUrl, something like this? Could you explain why would we need to add explicit support in the API?

apiVersion: kagent.dev/v1alpha2
kind: ModelConfig
metadata:
  name: my-spark-model
  namespace: kagent
spec:
  apiKeySecret: kagent-my-provider
  apiKeySecretKey: ${PROVIDER_API_KEY}
  model: Spark5.0-Pro
  provider: OpenAI
  openAI:
    baseUrl: "https://maas-api.cn-huabei-1.xf-yun.com.cn"

Copy link
Copy Markdown
Contributor Author

@dongjiang1989 dongjiang1989 May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @supreme-gg-gg

The iFlytek Spark-x model supports the standard WebSocket protocol for model invocation, which is a universal and standard model interaction protocol. I plan to implement access support for it via type configuration, and this is exactly the core motivation and original intention behind submitting this PR.

ref: https://www.xfyun.cn/doc/spark/Web.html

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


default:
return nil, fmt.Errorf("unsupported model type: %s", m.GetType())
}
Expand Down
30 changes: 30 additions & 0 deletions go/api/adk/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const (
ModelTypeGemini = "gemini"
ModelTypeBedrock = "bedrock"
ModelTypeSAPAICore = "sap_ai_core"
ModelTypeSparkMaaSAI = "spark_maas_ai"
)

func (o *OpenAI) MarshalJSON() ([]byte, error) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 = ""
}
Expand Down
88 changes: 88 additions & 0 deletions go/api/adk/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions go/api/config/crd/bases/kagent.dev_modelconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ spec:
- AnthropicVertexAI
- Bedrock
- SAPAICore
- SparkMaaSAI
type: string
sapAICore:
description: SAP AI Core-specific configuration
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ spec:
- AnthropicVertexAI
- Bedrock
- SAPAICore
- SparkMaaSAI
type: string
required:
- type
Expand Down
17 changes: 16 additions & 1 deletion go/api/v1alpha2/modelconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -40,6 +40,7 @@ const (
ModelProviderAnthropicVertexAI ModelProvider = "AnthropicVertexAI"
ModelProviderBedrock ModelProvider = "Bedrock"
ModelProviderSAPAICore ModelProvider = "SAPAICore"
ModelProviderSparkMaaSAI ModelProvider = "SparkMaaSAI"
)

type BaseVertexAIConfig struct {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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')"
Comment thread
dongjiang1989 marked this conversation as resolved.
// +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)"
Expand Down Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions go/api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Comment thread
dongjiang1989 marked this conversation as resolved.
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)
}
Expand Down
Loading
Loading