Skip to content
Merged
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
1 change: 1 addition & 0 deletions catalog/live/fetchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ var Registry = map[string]FetchFunc{
"poolside": FetchPoolside,
"groq": FetchGroq,
"clinepass": FetchClinePass,
"stepfun": FetchStepFun,
}

// Fetch runs a registered live fetcher.
Expand Down
61 changes: 61 additions & 0 deletions catalog/live/fetchers_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions catalog/provider_live_parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 4 additions & 4 deletions catalog/registry/provider_spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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))
}
}

Expand Down
8 changes: 8 additions & 0 deletions catalog/registry/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions catalog/v1_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"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"},
}
}

Expand Down Expand Up @@ -59,6 +60,7 @@
"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),
}
}

Expand Down Expand Up @@ -122,7 +124,7 @@
}

// DefaultOfferingTemplates returns offering templates for Azure deployments (model mappings required).
func DefaultOfferingTemplates(generatedAt time.Time) []ModelOfferingTemplate {

Check failure on line 127 in catalog/v1_defaults.go

View workflow job for this annotation

GitHub Actions / deadcode

unreachable func: DefaultOfferingTemplates
var out []ModelOfferingTemplate
for _, model := range seedOpenAIModels {
modelID := "openai/" + model.ID
Expand Down
4 changes: 2 additions & 2 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
15 changes: 13 additions & 2 deletions config/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 (
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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,
}

Expand Down
17 changes: 17 additions & 0 deletions config/provider_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions config/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading