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 (