From ae8b8b625475bf34dd58ae227b93be8bdf356513 Mon Sep 17 00:00:00 2001 From: lucas77778 <3098274296@qq.com> Date: Mon, 17 Aug 2026 11:08:08 +0000 Subject: [PATCH 1/3] feat(providers): add StepFun API support Amp-Thread-ID: https://ampcode.com/threads/T-01a00f1e-ac2f-72fa-9f08-6b425fcd2355 --- .../providers/__tests__/add-flow.test.tsx | 34 ++++++++++++++++ .../providers/src/__tests__/resolve.test.ts | 39 +++++++++++++++++++ packages/foundation/providers/src/catalog.ts | 20 ++++++++++ packages/foundation/providers/src/resolve.ts | 3 ++ packages/presentation/i18n/src/locales/en.ts | 2 + .../presentation/i18n/src/locales/zh-cn.ts | 2 + .../ui/src/shell/service-icon.tsx | 17 +++++++- 7 files changed, 116 insertions(+), 1 deletion(-) diff --git a/packages/client/workbench/src/settings/providers/__tests__/add-flow.test.tsx b/packages/client/workbench/src/settings/providers/__tests__/add-flow.test.tsx index 461322c59..1de5d1268 100644 --- a/packages/client/workbench/src/settings/providers/__tests__/add-flow.test.tsx +++ b/packages/client/workbench/src/settings/providers/__tests__/add-flow.test.tsx @@ -165,6 +165,40 @@ describe('non-subscription account creation', () => { ); }); + it('offers a dedicated StepFun API key entry', async () => { + const onPick = vi.fn(); + render(); + fireEvent.click(screen.getByText('serviceName.stepfun')); + expect(onPick).toHaveBeenCalledWith('stepfun'); + cleanup(); + + const onSubmit = vi.fn(); + render( + , + ); + + const secret = document.querySelector('input[type="password"]'); + if (!secret) throw new Error('credential input missing'); + fireEvent.change(secret, { target: { value: 'stepfun-test-key' } }); + fireEvent.click(screen.getByRole('button', { name: 'form.submit' })); + + await waitFor(() => + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + service: 'stepfun', + credential: { type: 'api-key', key: 'stepfun-test-key' }, + }), + ), + ); + }); + it('asks nothing about protocol for a service serving several shapes', () => { render( { }); }); + it('routes a StepFun key over each supported native protocol', () => { + const stepfun = account({ service: 'stepfun' }); + expect(resolveBinding(stepfun, 'claude-code')).toEqual({ + tier: 'native', + protocol: 'anthropic', + baseUrl: 'https://api.stepfun.com', + }); + expect(resolveBinding(stepfun, 'codex')).toEqual({ + tier: 'native', + protocol: 'openai-responses', + baseUrl: 'https://api.stepfun.com/v1', + }); + expect(resolveBinding(stepfun, 'opencode')).toEqual({ + tier: 'native', + protocol: 'openai-chat', + baseUrl: 'https://api.stepfun.com/v1', + knownProvider: 'stepfun', + }); + for (const kind of ['pi', 'grok-build'] as const) { + expect(resolveBinding(stepfun, kind)).toEqual({ + tier: 'unavailable', + reason: 'protocol-unsupported', + }); + } + expect( + resolveBinding( + account({ + service: 'stepfun', + endpoint: { baseUrl: 'https://proxy.example.com/v1', protocol: 'openai-chat' }, + }), + 'pi', + ), + ).toEqual({ tier: 'unavailable', reason: 'protocol-unsupported' }); + }); + it('reaches codex on every service whose endpoint serves the Responses API', () => { // Verified against vendor docs 2026-08: xAI, OpenRouter and Vercel all serve POST /responses // at the base URL declared here, so codex must have a target on each. @@ -305,6 +340,10 @@ describe('catalog helpers', () => { url: 'https://api.deepseek.com/models', wire: 'openai', }); + expect(modelListSource('stepfun')).toEqual({ + url: 'https://api.stepfun.com/v1/models', + wire: 'openai', + }); expect(modelListSource('anthropic-api')?.wire).toBe('anthropic'); expect(modelListSource('linkcode-gateway')).toEqual({ url: 'https://gateway.linkcode.ai/v1/models', diff --git a/packages/foundation/providers/src/catalog.ts b/packages/foundation/providers/src/catalog.ts index 0dd7653d2..e0a11546e 100644 --- a/packages/foundation/providers/src/catalog.ts +++ b/packages/foundation/providers/src/catalog.ts @@ -50,6 +50,8 @@ export type ServiceDescriptor = /** How the one secret authenticates. Service-level: every variant accepts the same secret. */ credentialType: 'api-key' | 'auth-token'; variants: Partial>; + /** Restrict agents when a provider-routed SDK does not carry this service's model catalog. */ + supportedAgents?: AgentKind[]; models?: ServiceModelList; secretPlaceholder?: string; } @@ -133,6 +135,24 @@ export const SERVICE_CATALOG: ServiceDescriptor[] = [ models: { url: 'https://api.deepseek.com/models', wire: 'openai' }, secretPlaceholder: 'sk-…', }, + { + id: 'stepfun', + label: 'StepFun', + group: 'direct', + kind: 'endpoint', + credentialType: 'api-key', + variants: { + anthropic: { baseUrl: 'https://api.stepfun.com' }, + 'openai-responses': { baseUrl: 'https://api.stepfun.com/v1' }, + 'openai-chat': { + baseUrl: 'https://api.stepfun.com/v1', + knownProvider: { opencode: 'stepfun' }, + }, + }, + // Pi's bundled provider catalog has no StepFun entry, so a key cannot select its wire/models. + supportedAgents: ['claude-code', 'codex', 'opencode'], + models: { url: 'https://api.stepfun.com/v1/models', wire: 'openai' }, + }, { id: LINKCODE_GATEWAY_SERVICE_ID, label: 'LinkCode Gateway', diff --git a/packages/foundation/providers/src/resolve.ts b/packages/foundation/providers/src/resolve.ts index f2a4deb09..e91c606de 100644 --- a/packages/foundation/providers/src/resolve.ts +++ b/packages/foundation/providers/src/resolve.ts @@ -54,6 +54,9 @@ export function resolveBinding(account: Account, kind: AgentKind): ResolvedBindi if (kind === 'grok-build') return resolveGrokBuild(account); const service = endpointServiceById(account.service); + if (service?.supportedAgents !== undefined && !service.supportedAgents.includes(kind)) { + return { tier: 'unavailable', reason: 'protocol-unsupported' }; + } const pinned = pinnedEndpoint(account); if (pinned) { return bind( diff --git a/packages/presentation/i18n/src/locales/en.ts b/packages/presentation/i18n/src/locales/en.ts index 56de73e25..dbbd3cab7 100644 --- a/packages/presentation/i18n/src/locales/en.ts +++ b/packages/presentation/i18n/src/locales/en.ts @@ -1116,6 +1116,7 @@ export const en = { 'openai-api': 'OpenAI API', xai: 'xAI (Grok)', deepseek: 'DeepSeek', + stepfun: 'StepFun', openrouter: 'OpenRouter', 'vercel-gateway': 'Vercel AI Gateway', 'cloudflare-gateway': 'Cloudflare AI Gateway', @@ -1130,6 +1131,7 @@ export const en = { 'openai-api': 'A key from platform.openai.com', xai: 'A key from console.x.ai', deepseek: 'A key from platform.deepseek.com', + stepfun: 'A key from platform.stepfun.ai', 'linkcode-gateway': 'Use LinkCode credits without bringing an API key', openrouter: 'Aggregation gateway', 'vercel-gateway': 'Server-side translating gateway', diff --git a/packages/presentation/i18n/src/locales/zh-cn.ts b/packages/presentation/i18n/src/locales/zh-cn.ts index ea9003899..1131b4f76 100644 --- a/packages/presentation/i18n/src/locales/zh-cn.ts +++ b/packages/presentation/i18n/src/locales/zh-cn.ts @@ -1089,6 +1089,7 @@ export const zhCN = { 'openai-api': 'OpenAI API', xai: 'xAI(Grok)', deepseek: 'DeepSeek', + stepfun: '阶跃星辰 StepFun', openrouter: 'OpenRouter', 'vercel-gateway': 'Vercel AI Gateway', 'cloudflare-gateway': 'Cloudflare AI Gateway', @@ -1103,6 +1104,7 @@ export const zhCN = { 'openai-api': 'platform.openai.com 的密钥', xai: 'console.x.ai 的密钥', deepseek: 'platform.deepseek.com 的密钥', + stepfun: 'platform.stepfun.ai 的密钥', 'linkcode-gateway': '无需自带 API 密钥,直接使用 LinkCode 额度', openrouter: '聚合网关', 'vercel-gateway': '服务端翻译网关', diff --git a/packages/presentation/ui/src/shell/service-icon.tsx b/packages/presentation/ui/src/shell/service-icon.tsx index 6a72d3c50..e5049c850 100644 --- a/packages/presentation/ui/src/shell/service-icon.tsx +++ b/packages/presentation/ui/src/shell/service-icon.tsx @@ -10,6 +10,20 @@ import VercelGlyph from '~icons/lobe-icons/vercel'; import XaiGlyph from '~icons/lobe-icons/xai'; import { cn } from '../lib/cn'; +function StepFunGlyph(props: React.SVGProps): React.ReactNode { + return ( + + + + ); +} + function LinkCodeGlyph(props: React.SVGProps): React.ReactNode { return ( @@ -25,13 +39,14 @@ function LinkCodeGlyph(props: React.SVGProps): React.ReactNode { /** Brand glyphs keyed by `Account.service` (a string join key, like `AgentKind` for `AgentIcon`); * color variants only where lobe-icons ships one, the rest render in currentColor. A missing key * deliberately falls back to label initials, so custom services need no wiring. */ -const SERVICE_GLYPHS: Record = { +const SERVICE_GLYPHS: Record) => React.ReactNode> = { 'claude-sub': ClaudeColorGlyph, 'chatgpt-sub': OpenAiGlyph, 'anthropic-api': AnthropicGlyph, 'openai-api': OpenAiGlyph, xai: XaiGlyph, deepseek: DeepSeekColorGlyph, + stepfun: StepFunGlyph, openrouter: OpenRouterGlyph, 'vercel-gateway': VercelGlyph, 'cloudflare-gateway': CloudflareColorGlyph, From f9530ee5fb9f59f085856af6b845836a2b263c10 Mon Sep 17 00:00:00 2001 From: lucas77778 <3098274296@qq.com> Date: Mon, 17 Aug 2026 11:55:07 +0000 Subject: [PATCH 2/3] fix(providers): address StepFun review feedback Amp-Thread-ID: https://ampcode.com/threads/T-01a00f1e-ac2f-72fa-9f08-6b425fcd2355 --- .../providers/src/__tests__/resolve.test.ts | 24 +++++++------------ packages/foundation/providers/src/catalog.ts | 5 +--- packages/foundation/providers/src/resolve.ts | 3 --- packages/presentation/i18n/src/locales/en.ts | 2 +- .../presentation/i18n/src/locales/zh-cn.ts | 2 +- .../ui/src/shell/service-icon.tsx | 19 +++------------ 6 files changed, 15 insertions(+), 40 deletions(-) diff --git a/packages/foundation/providers/src/__tests__/resolve.test.ts b/packages/foundation/providers/src/__tests__/resolve.test.ts index 6cf17bf9e..717db5133 100644 --- a/packages/foundation/providers/src/__tests__/resolve.test.ts +++ b/packages/foundation/providers/src/__tests__/resolve.test.ts @@ -153,21 +153,15 @@ describe('resolveBinding: variant chosen per agent', () => { baseUrl: 'https://api.stepfun.com/v1', knownProvider: 'stepfun', }); - for (const kind of ['pi', 'grok-build'] as const) { - expect(resolveBinding(stepfun, kind)).toEqual({ - tier: 'unavailable', - reason: 'protocol-unsupported', - }); - } - expect( - resolveBinding( - account({ - service: 'stepfun', - endpoint: { baseUrl: 'https://proxy.example.com/v1', protocol: 'openai-chat' }, - }), - 'pi', - ), - ).toEqual({ tier: 'unavailable', reason: 'protocol-unsupported' }); + expect(resolveBinding(stepfun, 'pi')).toEqual({ + tier: 'native', + protocol: 'anthropic', + baseUrl: 'https://api.stepfun.com', + }); + expect(resolveBinding(stepfun, 'grok-build')).toEqual({ + tier: 'unavailable', + reason: 'protocol-unsupported', + }); }); it('reaches codex on every service whose endpoint serves the Responses API', () => { diff --git a/packages/foundation/providers/src/catalog.ts b/packages/foundation/providers/src/catalog.ts index e0a11546e..2304dec11 100644 --- a/packages/foundation/providers/src/catalog.ts +++ b/packages/foundation/providers/src/catalog.ts @@ -50,8 +50,6 @@ export type ServiceDescriptor = /** How the one secret authenticates. Service-level: every variant accepts the same secret. */ credentialType: 'api-key' | 'auth-token'; variants: Partial>; - /** Restrict agents when a provider-routed SDK does not carry this service's model catalog. */ - supportedAgents?: AgentKind[]; models?: ServiceModelList; secretPlaceholder?: string; } @@ -149,9 +147,8 @@ export const SERVICE_CATALOG: ServiceDescriptor[] = [ knownProvider: { opencode: 'stepfun' }, }, }, - // Pi's bundled provider catalog has no StepFun entry, so a key cannot select its wire/models. - supportedAgents: ['claude-code', 'codex', 'opencode'], models: { url: 'https://api.stepfun.com/v1/models', wire: 'openai' }, + secretPlaceholder: 'sk-…', }, { id: LINKCODE_GATEWAY_SERVICE_ID, diff --git a/packages/foundation/providers/src/resolve.ts b/packages/foundation/providers/src/resolve.ts index e91c606de..f2a4deb09 100644 --- a/packages/foundation/providers/src/resolve.ts +++ b/packages/foundation/providers/src/resolve.ts @@ -54,9 +54,6 @@ export function resolveBinding(account: Account, kind: AgentKind): ResolvedBindi if (kind === 'grok-build') return resolveGrokBuild(account); const service = endpointServiceById(account.service); - if (service?.supportedAgents !== undefined && !service.supportedAgents.includes(kind)) { - return { tier: 'unavailable', reason: 'protocol-unsupported' }; - } const pinned = pinnedEndpoint(account); if (pinned) { return bind( diff --git a/packages/presentation/i18n/src/locales/en.ts b/packages/presentation/i18n/src/locales/en.ts index dbbd3cab7..6afb64469 100644 --- a/packages/presentation/i18n/src/locales/en.ts +++ b/packages/presentation/i18n/src/locales/en.ts @@ -1131,7 +1131,7 @@ export const en = { 'openai-api': 'A key from platform.openai.com', xai: 'A key from console.x.ai', deepseek: 'A key from platform.deepseek.com', - stepfun: 'A key from platform.stepfun.ai', + stepfun: 'A key from platform.stepfun.com', 'linkcode-gateway': 'Use LinkCode credits without bringing an API key', openrouter: 'Aggregation gateway', 'vercel-gateway': 'Server-side translating gateway', diff --git a/packages/presentation/i18n/src/locales/zh-cn.ts b/packages/presentation/i18n/src/locales/zh-cn.ts index 1131b4f76..6dda18272 100644 --- a/packages/presentation/i18n/src/locales/zh-cn.ts +++ b/packages/presentation/i18n/src/locales/zh-cn.ts @@ -1104,7 +1104,7 @@ export const zhCN = { 'openai-api': 'platform.openai.com 的密钥', xai: 'console.x.ai 的密钥', deepseek: 'platform.deepseek.com 的密钥', - stepfun: 'platform.stepfun.ai 的密钥', + stepfun: 'platform.stepfun.com 的密钥', 'linkcode-gateway': '无需自带 API 密钥,直接使用 LinkCode 额度', openrouter: '聚合网关', 'vercel-gateway': '服务端翻译网关', diff --git a/packages/presentation/ui/src/shell/service-icon.tsx b/packages/presentation/ui/src/shell/service-icon.tsx index e5049c850..5c011a894 100644 --- a/packages/presentation/ui/src/shell/service-icon.tsx +++ b/packages/presentation/ui/src/shell/service-icon.tsx @@ -6,24 +6,11 @@ import CloudflareColorGlyph from '~icons/lobe-icons/cloudflare-color'; import DeepSeekColorGlyph from '~icons/lobe-icons/deepseek-color'; import OpenAiGlyph from '~icons/lobe-icons/openai'; import OpenRouterGlyph from '~icons/lobe-icons/openrouter'; +import StepFunColorGlyph from '~icons/lobe-icons/stepfun-color'; import VercelGlyph from '~icons/lobe-icons/vercel'; import XaiGlyph from '~icons/lobe-icons/xai'; import { cn } from '../lib/cn'; -function StepFunGlyph(props: React.SVGProps): React.ReactNode { - return ( - - - - ); -} - function LinkCodeGlyph(props: React.SVGProps): React.ReactNode { return ( @@ -39,14 +26,14 @@ function LinkCodeGlyph(props: React.SVGProps): React.ReactNode { /** Brand glyphs keyed by `Account.service` (a string join key, like `AgentKind` for `AgentIcon`); * color variants only where lobe-icons ships one, the rest render in currentColor. A missing key * deliberately falls back to label initials, so custom services need no wiring. */ -const SERVICE_GLYPHS: Record) => React.ReactNode> = { +const SERVICE_GLYPHS: Record = { 'claude-sub': ClaudeColorGlyph, 'chatgpt-sub': OpenAiGlyph, 'anthropic-api': AnthropicGlyph, 'openai-api': OpenAiGlyph, xai: XaiGlyph, deepseek: DeepSeekColorGlyph, - stepfun: StepFunGlyph, + stepfun: StepFunColorGlyph, openrouter: OpenRouterGlyph, 'vercel-gateway': VercelGlyph, 'cloudflare-gateway': CloudflareColorGlyph, From 0b8105a2979cc3f27d8475ebeb782d95b802660a Mon Sep 17 00:00:00 2001 From: lucas77778 <3098274296@qq.com> Date: Mon, 17 Aug 2026 12:02:21 +0000 Subject: [PATCH 3/3] docs(providers): note StepFun Responses model Amp-Thread-ID: https://ampcode.com/threads/T-01a00f1e-ac2f-72fa-9f08-6b425fcd2355 --- packages/foundation/providers/src/catalog.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/foundation/providers/src/catalog.ts b/packages/foundation/providers/src/catalog.ts index 2304dec11..c375ce20c 100644 --- a/packages/foundation/providers/src/catalog.ts +++ b/packages/foundation/providers/src/catalog.ts @@ -141,6 +141,7 @@ export const SERVICE_CATALOG: ServiceDescriptor[] = [ credentialType: 'api-key', variants: { anthropic: { baseUrl: 'https://api.stepfun.com' }, + // Responses serves `step-3.7-flash` only (2026-08). 'openai-responses': { baseUrl: 'https://api.stepfun.com/v1' }, 'openai-chat': { baseUrl: 'https://api.stepfun.com/v1',