From e0a680e51da57bd8041743151853a626b7b8b5c3 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Fri, 31 Jul 2026 05:58:04 +0530 Subject: [PATCH] feat: add StepFun provider support --- catalog/live/fetchers.go | 1 + catalog/live/fetchers_providers.go | 61 ++++++++++++++++++++++++++ catalog/provider_live_parity_test.go | 4 +- catalog/registry/provider_spec_test.go | 8 ++-- catalog/registry/providers.go | 8 ++++ catalog/v1_defaults.go | 2 + config/config_test.go | 4 +- config/profiles.go | 15 ++++++- config/provider_env.go | 17 +++++++ config/providers.go | 1 + 10 files changed, 111 insertions(+), 10 deletions(-) diff --git a/catalog/live/fetchers.go b/catalog/live/fetchers.go index a61de7d1..929fa0e8 100644 --- a/catalog/live/fetchers.go +++ b/catalog/live/fetchers.go @@ -85,6 +85,7 @@ var Registry = map[string]FetchFunc{ "poolside": FetchPoolside, "groq": FetchGroq, "clinepass": FetchClinePass, + "stepfun": FetchStepFun, } // Fetch runs a registered live fetcher. diff --git a/catalog/live/fetchers_providers.go b/catalog/live/fetchers_providers.go index 22a6580a..b312d9aa 100644 --- a/catalog/live/fetchers_providers.go +++ b/catalog/live/fetchers_providers.go @@ -1058,3 +1058,64 @@ func FetchClinePass(env map[string]string) ([]Entry, error) { } return entries, nil } + +// FetchStepFun lists models from the StepFun OpenAI-compatible API. +// Docs: https://platform.stepfun.ai/docs/en/guides/basic-concepts +// Pricing: https://platform.stepfun.ai/docs/en/guides/pricing/details +func FetchStepFun(env map[string]string) ([]Entry, error) { + entries, err := fetchOpenAICompatModels( + context.Background(), + envOr(env, "STEP_BASE_URL", "https://api.stepfun.ai/v1"), + env["STEP_API_KEY"], "Bearer", + ) + if err != nil { + return nil, err + } + for i := range entries { + entries[i] = enrichStepFunEntry(entries[i]) + } + return entries, nil +} + +var stepFunOfficialTextSpecs = map[string]struct { + ContextWindow, MaxOutput int + Tools, Vision, Thinking bool + InputPrice, OutputPrice float64 +}{ + "step-3.7-flash": {256_000, 65_536, true, true, true, 0.20, 1.15}, + "step-3.5-flash": {256_000, 65_536, true, false, true, 0.10, 0.30}, + "step-3.5-flash-2603": {256_000, 65_536, true, false, true, 0.10, 0.30}, +} + +func enrichStepFunEntry(e Entry) Entry { + id := strings.TrimSpace(e.ID) + if i := strings.LastIndex(id, "/"); i >= 0 { + id = id[i+1:] + } + spec, ok := stepFunOfficialTextSpecs[id] + if !ok { + return e + } + if e.ContextWindow <= 0 { + e.ContextWindow = spec.ContextWindow + } + if e.MaxOutput <= 0 { + e.MaxOutput = spec.MaxOutput + } + if spec.Tools { + e.Features = appendUnique(e.Features, "tools") + } + if spec.Vision { + e.ImageInput = true + e.Features = appendUnique(e.Features, "image_input") + } + if spec.Thinking { + e.ThinkingEnabled = true + e.Features = appendUnique(e.Features, "thinking:enabled") + } + if spec.InputPrice >= 0 && spec.OutputPrice >= 0 { + e.InputPricePer1M = spec.InputPrice + e.OutputPricePer1M = spec.OutputPrice + } + return e +} diff --git a/catalog/provider_live_parity_test.go b/catalog/provider_live_parity_test.go index 603d72fd..2c7198f2 100644 --- a/catalog/provider_live_parity_test.go +++ b/catalog/provider_live_parity_test.go @@ -11,8 +11,8 @@ import ( func TestAllProviders_LiveFetchParity(t *testing.T) { t.Parallel() specs := registry.All() - if len(specs) != 25 { - t.Fatalf("expected 25 providers, got %d", len(specs)) + if len(specs) != 26 { + t.Fatalf("expected 26 providers, got %d", len(specs)) } for _, spec := range specs { t.Run(spec.ProviderID, func(t *testing.T) { diff --git a/catalog/registry/provider_spec_test.go b/catalog/registry/provider_spec_test.go index 1aa797be..84ed9d3f 100644 --- a/catalog/registry/provider_spec_test.go +++ b/catalog/registry/provider_spec_test.go @@ -9,8 +9,8 @@ import ( func TestAllProviders_Count(t *testing.T) { t.Parallel() - if n := len(registry.All()); n != 25 { - t.Fatalf("expected 25 providers, got %d", n) + if n := len(registry.All()); n != 26 { + t.Fatalf("expected 26 providers, got %d", n) } } @@ -42,8 +42,8 @@ func TestProviderSpecs_AgnesOpenAIOnlyLongCatOpenAIPrimary(t *testing.T) { func TestLiveFetcherKeys_AllProviders(t *testing.T) { t.Parallel() keys := registry.LiveFetcherKeys() - if len(keys) != 25 { - t.Fatalf("expected 25 live fetcher keys, got %d", len(keys)) + if len(keys) != 26 { + t.Fatalf("expected 26 live fetcher keys, got %d", len(keys)) } } diff --git a/catalog/registry/providers.go b/catalog/registry/providers.go index 99d36388..50a5e497 100644 --- a/catalog/registry/providers.go +++ b/catalog/registry/providers.go @@ -173,6 +173,14 @@ func providerSpecs() []ProviderSpec { PublicModelCatalog: true, ProtocolID: "openai-responses", AdapterID: "concentrate-responses", RuntimeProfileKey: "concentrate", }, + { + ProviderID: "stepfun", DisplayName: "StepFun", DeploymentID: "stepfun-direct", SortOrder: 2, ChatPreference: 27, + RequiresKey: true, CredentialEnv: "STEP_API_KEY", + BaseURLEnv: []string{"STEP_BASE_URL"}, + ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.stepfun.ai/v1", + LiveFetcherKey: "stepfun", LiveCatalogKey: "stepfun", + ProtocolID: "openai-chat-completions", AdapterID: "openai", RuntimeProfileKey: "stepfun", + }, { ProviderID: "agnes", DisplayName: "Agnes AI", DeploymentID: "agnes-direct", SortOrder: 1, ChatPreference: 25, RequiresKey: true, CredentialEnv: "AGNES_API_KEY", diff --git a/catalog/v1_defaults.go b/catalog/v1_defaults.go index 31881199..663aa044 100644 --- a/catalog/v1_defaults.go +++ b/catalog/v1_defaults.go @@ -26,6 +26,7 @@ func defaultProviders() map[string]Provider { "xiaomi_mimo_payg": {ID: "xiaomi_mimo_payg", Name: "Xiaomi MiMo (Pay-as-you-go)"}, "xiaomi_mimo_token_plan": {ID: "xiaomi_mimo_token_plan", Name: "Xiaomi MiMo (Token Plan)"}, "deepseek": {ID: "deepseek", Name: "DeepSeek"}, + "stepfun": {ID: "stepfun", Name: "StepFun"}, } } @@ -59,6 +60,7 @@ func defaultDeployments() map[string]Deployment { "xiaomi_mimo_payg-direct": deployment("xiaomi_mimo_payg-direct", "Xiaomi MiMo Pay-as-you-go", "xiaomi_mimo_payg", "openai-chat-completions", "xiaomi_mimo", NativeModelIDDiscovered), "xiaomi_mimo_token_plan-direct": deployment("xiaomi_mimo_token_plan-direct", "Xiaomi MiMo Token Plan", "xiaomi_mimo_token_plan", "openai-chat-completions", "xiaomi_mimo", NativeModelIDDiscovered), "deepseek-direct": deployment("deepseek-direct", "DeepSeek", "deepseek", "openai-chat-completions", "deepseek", NativeModelIDCatalogKnown), + "stepfun-direct": deployment("stepfun-direct", "StepFun", "stepfun", "openai-chat-completions", "openai", NativeModelIDDiscovered), } } diff --git a/config/config_test.go b/config/config_test.go index bc91bc52..e9dcf6c2 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -87,8 +87,8 @@ func TestNormalizeOllamaOpenAIBaseURL(t *testing.T) { func TestProviderDetectionOrder(t *testing.T) { t.Parallel() - if len(APIProviderDetectionOrder) != 23 { - t.Errorf("expected 23 providers in detection order, got %d", len(APIProviderDetectionOrder)) + if len(APIProviderDetectionOrder) != 24 { + t.Errorf("expected 24 providers in detection order, got %d", len(APIProviderDetectionOrder)) } if APIProviderDetectionOrder[0] != ProviderAnthropic { t.Error("expected anthropic first in detection order") diff --git a/config/profiles.go b/config/profiles.go index 84f7312b..3c477eed 100644 --- a/config/profiles.go +++ b/config/profiles.go @@ -30,6 +30,7 @@ const ( ProviderPoolside APIProvider = "poolside" ProviderGroq APIProvider = "groq" ProviderClinePass APIProvider = "clinepass" + ProviderStepFun APIProvider = "stepfun" ) // RuntimeProviderProfile defines how a provider is detected and configured at runtime. @@ -212,13 +213,20 @@ var ( BaseURLEnv: []string{"CLINE_API_BASE"}, APIKeys: []APIKeyDef{{Env: "CLINE_API_KEY", Source: "clinepass"}}, } + StepFunRuntimeProfile = RuntimeProviderProfile{ + Mode: "openai", DefaultBaseURL: DefaultStepFunOpenAIBaseURL, + DetectionEnv: []string{"STEP_API_KEY"}, + ModelEnv: []string{"STEP_MODEL", "OPENAI_MODEL"}, + BaseURLEnv: []string{"STEP_BASE_URL"}, + APIKeys: []APIKeyDef{{Env: "STEP_API_KEY", Source: "stepfun"}}, + } ) // APIProviderDetectionOrder is the priority order for provider detection. var APIProviderDetectionOrder = []APIProvider{ ProviderAnthropic, ProviderConcentrate, ProviderOpenRouter, ProviderGrok, ProviderGemini, ProviderVertex, ProviderBedrock, ProviderZAICoding, ProviderZAIPayg, ProviderCanopyWave, ProviderDeepSeek, ProviderPoolside, ProviderGroq, ProviderClinePass, ProviderAzure, ProviderOpenAI, ProviderOpenCodeGo, - ProviderKimi, ProviderXiaomiMimoPayg, ProviderXiaomiMimoTokenPlan, ProviderMiniMaxTokenPlan, ProviderMiniMaxPayg, ProviderOllama, + ProviderKimi, ProviderXiaomiMimoPayg, ProviderXiaomiMimoTokenPlan, ProviderMiniMaxTokenPlan, ProviderMiniMaxPayg, ProviderOllama, ProviderStepFun, } // ProviderModelEnvKeys maps each provider to its model env var keys. @@ -246,6 +254,7 @@ var ProviderModelEnvKeys = map[APIProvider][]string{ ProviderXiaomiMimoTokenPlan: XiaomiTokenPlanRuntimeProfile.ModelEnv, ProviderMiniMaxTokenPlan: {"MINIMAX_TOKEN_PLAN_MODEL", "MINIMAX_MODEL", "OPENAI_MODEL"}, ProviderMiniMaxPayg: {"MINIMAX_PAYG_MODEL", "MINIMAX_MODEL", "OPENAI_MODEL"}, + ProviderStepFun: StepFunRuntimeProfile.ModelEnv, } const ( @@ -255,7 +264,7 @@ const ( // OpenAICompatibleRuntimeProfileOrder is the detection order for runtime profiles. var OpenAICompatibleRuntimeProfileOrder = []string{ - "concentrate", "agnes", "longcat", "openrouter", "grok", "gemini", "anthropic", "zai_coding", "zai_payg", "canopywave", "deepseek", "poolside", "groq", "clinepass", "openai", "opencodego", "kimi", "xiaomi_mimo_payg", "xiaomi_mimo_token_plan", "minimax_token_plan", "minimax_payg", + "concentrate", "agnes", "longcat", "openrouter", "grok", "gemini", "anthropic", "zai_coding", "zai_payg", "canopywave", "deepseek", "poolside", "groq", "clinepass", "openai", "opencodego", "kimi", "xiaomi_mimo_payg", "xiaomi_mimo_token_plan", "minimax_token_plan", "minimax_payg", "stepfun", } // OpenAICompatibleRuntimeProfiles maps profile key to its runtime profile. @@ -281,6 +290,7 @@ var OpenAICompatibleRuntimeProfiles = map[string]RuntimeProviderProfile{ "xiaomi_mimo_token_plan": XiaomiTokenPlanRuntimeProfile, "minimax_token_plan": MiniMaxTokenPlanRuntimeProfile, "minimax_payg": MiniMaxPaygRuntimeProfile, + "stepfun": StepFunRuntimeProfile, } // RuntimeProviderProfiles maps provider/profile keys to runtime detection profiles. @@ -308,6 +318,7 @@ var RuntimeProviderProfiles = map[string]RuntimeProviderProfile{ "xiaomi_mimo_token_plan": XiaomiTokenPlanRuntimeProfile, "minimax_token_plan": MiniMaxTokenPlanRuntimeProfile, "minimax_payg": MiniMaxPaygRuntimeProfile, + "stepfun": StepFunRuntimeProfile, "ollama": OllamaRuntimeProfile, } diff --git a/config/provider_env.go b/config/provider_env.go index aaaf9bf7..7e388d87 100644 --- a/config/provider_env.go +++ b/config/provider_env.go @@ -63,6 +63,9 @@ type ProviderConfig struct { ClinePassAPIKey string `json:"clinepass_api_key,omitempty"` ClinePassBaseURL string `json:"clinepass_base_url,omitempty"` ClinePassModel string `json:"clinepass_model,omitempty"` + StepFunAPIKey string `json:"stepfun_api_key,omitempty"` + StepFunBaseURL string `json:"stepfun_base_url,omitempty"` + StepFunModel string `json:"stepfun_model,omitempty"` MiniMaxModel string `json:"minimax_model,omitempty"` AnthropicModel string `json:"anthropic_model,omitempty"` OpenAIModel string `json:"openai_model,omitempty"` @@ -241,6 +244,11 @@ var providerFields = map[string]providerFieldMap{ Models: func(c *ProviderConfig) []string { return []string{c.XiaomiModel} }, BaseURL: func(c *ProviderConfig) string { return c.XiaomiMimoTokenPlanBaseURL }, }, + ProviderStepFun: { + APIKeys: func(c *ProviderConfig) []string { return []string{c.StepFunAPIKey} }, + Models: func(c *ProviderConfig) []string { return []string{c.StepFunModel} }, + BaseURL: func(c *ProviderConfig) string { return c.StepFunBaseURL }, + }, } func firstNonEmpty(ss ...string) string { @@ -587,6 +595,7 @@ func ClearProviderRuntimeEnv() { "POOLSIDE_API_KEY", "POOLSIDE_MODEL", "POOLSIDE_BASE_URL", "CLINE_API_KEY", "CLINE_MODEL", "CLINE_API_BASE", "MOONSHOT_API_KEY", "MOONSHOT_MODEL", "MOONSHOT_BASE_URL", + "STEP_API_KEY", "STEP_MODEL", "STEP_BASE_URL", "XIAOMI_MIMO_PAYG_API_KEY", "XIAOMI_MIMO_TOKEN_PLAN_API_KEY", "XIAOMI_MIMO_TOKEN_PLAN_REGION", "XIAOMI_MODEL", "XIAOMI_BASE_URL", "XIAOMI_MIMO_PAYG_BASE_URL", "XIAOMI_MIMO_TOKEN_PLAN_BASE_URL", @@ -823,6 +832,14 @@ func ApplyProviderEnv(provider string, config *ProviderConfig, activeModel strin if base != "" { collectOpenAICompatibleProvider(env, "XIAOMI", apiKey, m, base, overwrite) } + case ProviderStepFun: + apiKey := AsNonEmptyString(config.StepFunAPIKey) + base := firstNonEmpty(config.StepFunBaseURL, DefaultStepFunOpenAIBaseURL) + m := activeModel + if m == "" { + m = catalog.GetProviderDefaultModel("stepfun", cat) + } + collectOpenAICompatibleProvider(env, "STEP", apiKey, m, base, overwrite) } return env } diff --git a/config/providers.go b/config/providers.go index 3c94714e..f354ccbb 100644 --- a/config/providers.go +++ b/config/providers.go @@ -37,6 +37,7 @@ const ( DefaultGroqOpenAIBaseURL = "https://api.groq.com/openai/v1" DefaultPoolsideOpenAIBaseURL = "https://inference.poolside.ai/v1" DefaultClinePassOpenAIBaseURL = "https://api.cline.bot/api/v1" // #nosec G101 -- public API base URL, not a secret value + DefaultStepFunOpenAIBaseURL = "https://api.stepfun.ai/v1" ) // ProviderTransport is the transport type for provider requests.