From 487b6942fb487cbc27326b836e945e2c7693b7f4 Mon Sep 17 00:00:00 2001 From: bzp2010 Date: Mon, 4 May 2026 14:10:04 +0800 Subject: [PATCH] feat(provider): add stepfun --- src/config/entities/providers-schema.json | 2 + src/config/entities/providers.rs | 8 +++ src/gateway/providers/mod.rs | 8 ++- src/gateway/providers/stepfun.rs | 75 +++++++++++++++++++++++ src/proxy/provider.rs | 23 ++++++- ui/src/i18n/locales/en.json | 1 + ui/src/i18n/locales/zh-CN.json | 1 + ui/src/lib/api/types.ts | 6 ++ 8 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 src/gateway/providers/stepfun.rs diff --git a/src/config/entities/providers-schema.json b/src/config/entities/providers-schema.json index f3bd2b7..635ef9a 100644 --- a/src/config/entities/providers-schema.json +++ b/src/config/entities/providers-schema.json @@ -20,6 +20,7 @@ "modelscope-cn", "siliconflow", "siliconflow-cn", + "stepfun", "moonshotai", "moonshotai-cn", "openai", @@ -71,6 +72,7 @@ "modelscope-cn", "siliconflow", "siliconflow-cn", + "stepfun", "moonshotai", "moonshotai-cn", "openai", diff --git a/src/config/entities/providers.rs b/src/config/entities/providers.rs index 93342fa..c4cf025 100644 --- a/src/config/entities/providers.rs +++ b/src/config/entities/providers.rs @@ -50,6 +50,8 @@ pub enum ProviderConfig { SiliconFlow(configs::SiliconFlowProviderConfig), #[serde(rename = "siliconflow-cn")] SiliconFlowCn(configs::SiliconFlowCnProviderConfig), + #[serde(rename = "stepfun")] + StepFun(configs::StepFunProviderConfig), #[serde(rename = "moonshotai")] MoonshotAi(configs::MoonshotAiProviderConfig), #[serde(rename = "moonshotai-cn")] @@ -79,6 +81,7 @@ impl ProviderConfig { Self::ModelScopeCn(_) => identifiers::MODELSCOPE_CN, Self::SiliconFlow(_) => identifiers::SILICONFLOW, Self::SiliconFlowCn(_) => identifiers::SILICONFLOW_CN, + Self::StepFun(_) => identifiers::STEPFUN, Self::MoonshotAi(_) => identifiers::MOONSHOT_AI, Self::MoonshotAiCn(_) => identifiers::MOONSHOT_AI_CN, Self::OpenAI(_) => identifiers::OPENAI, @@ -226,6 +229,11 @@ mod tests { "type": "siliconflow-cn", "config": { "api_key": "test_key" } }), true, None)] + #[case::stepfun_ok(json!({ + "name": "stepfun-primary", + "type": "stepfun", + "config": { "api_key": "test_key" } + }), true, None)] #[case::moonshotai_ok(json!({ "name": "moonshot-primary", "type": "moonshotai", diff --git a/src/gateway/providers/mod.rs b/src/gateway/providers/mod.rs index 9f15fdf..98ab66e 100644 --- a/src/gateway/providers/mod.rs +++ b/src/gateway/providers/mod.rs @@ -13,6 +13,7 @@ pub mod moonshot; pub mod openai; pub mod openrouter; pub mod siliconflow; +pub mod stepfun; pub mod xai; pub mod zhipuai; @@ -30,13 +31,14 @@ pub use moonshot::{MoonshotAi, MoonshotAiCn}; pub use openai::OpenAIDef; pub use openrouter::OpenRouter; pub use siliconflow::{SiliconFlow, SiliconFlowCn}; +pub use stepfun::StepFun; pub use xai::Xai; pub use zhipuai::ZhipuAi; pub mod identifiers { use super::{ anthropic, azure, bedrock, cohere, deepseek, fireworks, gemini, groq, mistral, modelscope, - moonshot, openai, openrouter, siliconflow, xai, zhipuai, + moonshot, openai, openrouter, siliconflow, stepfun, xai, zhipuai, }; pub const ANTHROPIC: &str = anthropic::IDENTIFIER; @@ -52,6 +54,7 @@ pub mod identifiers { pub const MODELSCOPE_CN: &str = modelscope::CN_IDENTIFIER; pub const SILICONFLOW: &str = siliconflow::IDENTIFIER; pub const SILICONFLOW_CN: &str = siliconflow::CN_IDENTIFIER; + pub const STEPFUN: &str = stepfun::IDENTIFIER; pub const MOONSHOT_AI: &str = moonshot::IDENTIFIER; pub const MOONSHOT_AI_CN: &str = moonshot::CN_IDENTIFIER; pub const OPENAI: &str = openai::IDENTIFIER; @@ -76,6 +79,7 @@ pub mod configs { openai::OpenAIProviderConfig, openrouter::OpenRouterProviderConfig, siliconflow::{SiliconFlowCnProviderConfig, SiliconFlowProviderConfig}, + stepfun::StepFunProviderConfig, xai::XaiProviderConfig, zhipuai::ZhipuAiProviderConfig, }; @@ -98,6 +102,7 @@ pub fn default_provider_registry() -> Result { .register(ModelScopeCn)? .register(SiliconFlow)? .register(SiliconFlowCn)? + .register(StepFun)? .register(MoonshotAi)? .register(MoonshotAiCn)? .register(OpenAIDef)? @@ -136,6 +141,7 @@ mod tests { registry.get("siliconflow-cn").unwrap().name(), "siliconflow-cn" ); + assert_eq!(registry.get("stepfun").unwrap().name(), "stepfun"); assert_eq!(registry.get("moonshotai").unwrap().name(), "moonshotai"); assert_eq!( registry.get("moonshotai-cn").unwrap().name(), diff --git a/src/gateway/providers/stepfun.rs b/src/gateway/providers/stepfun.rs new file mode 100644 index 0000000..30090c6 --- /dev/null +++ b/src/gateway/providers/stepfun.rs @@ -0,0 +1,75 @@ +//! StepFun documents an OpenAI-compatible chat endpoint. +//! +//! The standard OpenAI-compatible path is /v1/chat/completions, so a single +//! stepfun provider is enough here. +//! +//! Docs: +//! - https://platform.stepfun.com/docs/zh/quickstart/overview +//! - https://platform.stepfun.com/docs/zh/api-reference/chat/chat-completion-create + +use serde::{Deserialize, Serialize}; + +use crate::gateway::providers::macros::provider; + +pub const IDENTIFIER: &str = "stepfun"; + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct StepFunProviderConfig { + pub api_key: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub api_base: Option, +} + +provider!(StepFun { + display_name: "stepfun", + base_url: "https://api.stepfun.com/v1", + auth: bearer, +}); + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use serde_json::json; + + use super::StepFun; + use crate::gateway::{ + provider_instance::ProviderAuth, + traits::{ChatTransform, ProviderMeta}, + types::openai::ChatCompletionRequest, + }; + + #[test] + fn provider_metadata_and_url_are_correct() { + let provider = StepFun; + let headers = provider + .build_auth_headers(&ProviderAuth::ApiKey("stepfun-com-key".into())) + .unwrap(); + + assert_eq!(provider.name(), "stepfun"); + assert_eq!(provider.default_base_url(), "https://api.stepfun.com/v1"); + assert_eq!(headers["authorization"], "Bearer stepfun-com-key"); + assert_eq!( + provider.build_url(provider.default_base_url(), "ignored"), + "https://api.stepfun.com/v1/chat/completions" + ); + } + + #[test] + fn transform_request_passes_openai_compatible_payload_through() { + let provider = StepFun; + let request: ChatCompletionRequest = serde_json::from_value(json!({ + "model": "step-3.5-flash", + "messages": [{"role": "user", "content": "hello"}], + "reasoning_format": {"type": "deepseek-style"}, + "reasoning_effort": "high" + })) + .unwrap(); + + let transformed = provider.transform_request(&request).unwrap(); + + assert_eq!(transformed["model"], "step-3.5-flash"); + assert_eq!(transformed["reasoning_format"]["type"], "deepseek-style"); + assert_eq!(transformed["reasoning_effort"], "high"); + } +} diff --git a/src/proxy/provider.rs b/src/proxy/provider.rs index 40e81f0..a2a4a46 100644 --- a/src/proxy/provider.rs +++ b/src/proxy/provider.rs @@ -85,6 +85,10 @@ fn provider_auth_and_base_url(config: &ProviderConfig) -> Result<(ProviderAuth, ProviderAuth::ApiKey(config.api_key.clone()), parse_base_url(config.api_base.as_deref())?, ), + ProviderConfig::StepFun(config) => ( + ProviderAuth::ApiKey(config.api_key.clone()), + parse_base_url(config.api_base.as_deref())?, + ), ProviderConfig::MoonshotAi(config) => ( ProviderAuth::ApiKey(config.api_key.clone()), parse_base_url(config.api_base.as_deref())?, @@ -197,7 +201,8 @@ mod tests { FireworksAiProviderConfig, GroqProviderConfig, MistralProviderConfig, ModelScopeCnProviderConfig, ModelScopeProviderConfig, MoonshotAiCnProviderConfig, MoonshotAiProviderConfig, OpenRouterProviderConfig, SiliconFlowCnProviderConfig, - SiliconFlowProviderConfig, XaiProviderConfig, ZhipuAiProviderConfig, + SiliconFlowProviderConfig, StepFunProviderConfig, XaiProviderConfig, + ZhipuAiProviderConfig, }, }; @@ -390,6 +395,22 @@ mod tests { ); } + #[test] + fn provider_auth_and_base_url_returns_stepfun_api_key_and_optional_base_url() { + let config = ProviderConfig::StepFun(StepFunProviderConfig { + api_key: "stepfun-key".into(), + api_base: Some("https://api.stepfun.com/v1".into()), + }); + + let (auth, base_url_override) = provider_auth_and_base_url(&config).unwrap(); + + assert_eq!(auth.api_key_for("stepfun").unwrap(), "stepfun-key"); + assert_eq!( + base_url_override.as_ref().map(Url::as_str), + Some("https://api.stepfun.com/v1") + ); + } + #[test] fn provider_auth_and_base_url_returns_moonshot_api_key_and_optional_base_url() { let config = ProviderConfig::MoonshotAi(MoonshotAiProviderConfig { diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 93debe3..dd8eeff 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -52,6 +52,7 @@ "modelscope-cn": "ModelScope (CN)", "siliconflow": "SiliconFlow", "siliconflow-cn": "SiliconFlow (CN)", + "stepfun": "StepFun", "moonshotai": "Moonshot AI", "moonshotai-cn": "Moonshot AI (CN)", "zhipuai": "ZhipuAI", diff --git a/ui/src/i18n/locales/zh-CN.json b/ui/src/i18n/locales/zh-CN.json index 235ef48..ab8e19f 100644 --- a/ui/src/i18n/locales/zh-CN.json +++ b/ui/src/i18n/locales/zh-CN.json @@ -52,6 +52,7 @@ "modelscope-cn": "ModelScope (CN)", "siliconflow": "SiliconFlow", "siliconflow-cn": "硅基流动", + "stepfun": "阶跃星辰", "moonshotai": "Moonshot AI", "moonshotai-cn": "Moonshot AI (CN)", "zhipuai": "智谱", diff --git a/ui/src/lib/api/types.ts b/ui/src/lib/api/types.ts index 0fb71fe..d31fe83 100644 --- a/ui/src/lib/api/types.ts +++ b/ui/src/lib/api/types.ts @@ -49,6 +49,7 @@ export const PROVIDER_TYPE_VARIANTS = [ 'modelscope-cn', 'siliconflow', 'siliconflow-cn', + 'stepfun', 'moonshotai', 'moonshotai-cn', 'zhipuai', @@ -156,6 +157,11 @@ export type Provider = type: 'siliconflow-cn'; config: ApiBaseProviderConfig; } + | { + name: string; + type: 'stepfun'; + config: ApiBaseProviderConfig; + } | { name: string; type: 'moonshotai';