feat(providers): add StepFun API support - #456
Conversation
There was a problem hiding this comment.
Important
The endpoint facts here all hold up, but the setup copy sends users to the wrong StepFun console, the icon isn't StepFun's, and the new supportedAgents axis singles out one service for a condition two existing services also meet.
Reviewed changes — full diff of the single commit ae8b8b62 adding a StepFun service to the provider catalog.
- New
stepfuncatalog entry — three variants (anthropic,openai-responses,openai-chat) on the China hostapi.stepfun.com, withknownProvider: { opencode: 'stepfun' }and a model-list URL. - New
supportedAgentsaxis — an optional per-serviceAgentKind[]allowlist onServiceDescriptor, enforced by an early return inresolveBinding, used to keeppioff StepFun. - Presentation —
serviceName/serviceDescriptionentries in both locales, and a hand-writtenStepFunGlyphinservice-icon.tsxthat widens theSERVICE_GLYPHSvalue type. - Tests — a
resolveBindingcase per agent plus amodelListSourceassertion, and an add-flow test covering the catalog tile and API-key submission. - Vendor claims verified against StepFun's own China API reference (2026-08) —
POST https://api.stepfun.com/v1/messagesis documented and explicitly Anthropic-Messages-compatible,POST /v1/responsesandPOST /v1/chat/completionslikewise, so all three variants are correctly declared. models.dev carries a top-levelstepfunprovider ("StepFun (China)",api: https://api.stepfun.com/v1), so theopencodeid is backed. pi really has no StepFun provider —@earendil-works/pi-ai'sMODELShas 35 providers, none matchingstep, andKnownProvideromits it — so the comment justifying the exclusion is accurate. None of these need re-checking.
⚠️ The providers AGENTS.md still says resolveBinding has no per-service special cases
packages/foundation/providers/AGENTS.md is the file agents are routed to before touching this package, and it documents resolveBinding exhaustively: "One table, no per-service special cases. A service-name check inside a protocol decision is the smell this package exists to remove," followed by a per-agent rule list. supportedAgents adds an orthogonal axis that none of that text accounts for, and the "Not here yet" section still says endpoints without a known provider keep the baseUrl-override behavior — which is now untrue for StepFun only. Whatever you decide on the inline catalog.ts comment, the doc needs to match.
Technical details
# Document the `supportedAgents` axis (or remove it)
## Affected sites
- `packages/foundation/providers/AGENTS.md` — the `## resolveBinding` section claims one table with
no per-service special cases, and enumerates the rule for each of the five agent kinds. A
service-level agent allowlist is a new axis that short-circuits all of them.
- `packages/foundation/providers/AGENTS.md` — the `## Not here yet` section states that endpoints
without a known provider "keep the pre-existing behavior (baseUrl override on a guessed
provider)". After this PR that is true for `linkcode-gateway` and `cloudflare-anthropic` but not
for `stepfun`.
## Required outcome
- The doc and the code agree on when an agent is refused a service, and a future reader can tell
whether a new pi-unknown service should get `supportedAgents` or not.
## Open questions for the human
- Is `supportedAgents` meant to become the general rule for pi-unknown services, or is StepFun a
deliberate one-off? The answer decides whether this is a doc edit or a code change.ℹ️ Nitpicks
stepfunis the only entry in thedirectgroup without asecretPlaceholder—anthropic-api,openai-api,xaianddeepseekall set one.- The new resolve test loops
for (const kind of ['pi', 'grok-build']), butgrok-buildreturns atresolve.ts:54before thesupportedAgentscheck ever runs, so that half of the loop asserts pre-existing behavior rather than anything this PR adds. Thepihalf is a real assertion and does fail without the gate.
<comments">[{"path":"packages/presentation/i18n/src/locales/en.ts","line":1134,"body":"This points at the wrong console:platform.stepfun.aiis StepFun Global, whose keys authenticate againstapi.stepfun.ai, while everybaseUrlin this PR targets the China hostapi.stepfun.com(consoleplatform.stepfun.com). models.dev models them as two separate providers —stepfun"StepFun (China)" andstepfun-ai"StepFun (Global)" — so a user who follows this copy gets a key from the other cloud and every request 401s.","suggestion":" stepfun: 'A key from platform.stepfun.com',"},{"path":"packages/presentation/i18n/src/locales/zh-cn.ts","line":1107,"body":"Same host mismatch as the English string: the catalog targetsapi.stepfun.com, whose console isplatform.stepfun.com, not the Global.aiportal.","suggestion":" stepfun: 'platform.stepfun.com 的密钥',"},{"path":"packages/presentation/ui/src/shell/service-icon.tsx","line":13,"body":"@proj-airi/lobe-iconsalready shipsstepfun,stepfun-colorandstepfun-text, and this hand-written path matches none of them — it draws a disc with knocked-out squares, where the real mark is a stair-step glyph. Importing~icons/lobe-icons/stepfun-colormatches the DeepSeek precedent on the line below, ships the actual brand mark, and lets theSERVICE_GLYPHSvalue type at line 42 stay as it was.\n\n"},{"path":"packages/foundation/providers/src/catalog.ts","line":153,"body":"The condition this encodes — "the agent's own catalog has no entry for this service" — is already expressed by an absentTechnical details
\n\nmarkdown\n# Use the lobe-icons StepFun glyph\n\n## Affected sites\n- `packages/presentation/ui/src/shell/service-icon.tsx:13-25` — hand-written `StepFunGlyph`; its\n path data does not match either glyph the icon set ships.\n- `packages/presentation/ui/src/shell/service-icon.tsx:42` — `SERVICE_GLYPHS` was widened from\n `Record<string, typeof AnthropicGlyph>` to `Record<string, (props: React.SVGProps<SVGSVGElement>)\n => React.ReactNode>` only because the hand-rolled component's return type is not assignable to the\n unplugin-icons component type.\n- `packages/presentation/ui/src/shell/service-icon.tsx:49` — the map entry.\n\n## Required outcome\n- The StepFun tile renders StepFun's actual brand mark, sourced the same way every other brand glyph\n in this file is, and the shared map keeps its original value type.\n\n## Suggested approach\n- Add `import StepFunColorGlyph from '~icons/lobe-icons/stepfun-color';` alongside the other\n `~icons/lobe-icons/*` imports (alphabetical placement puts it after `openrouter`), delete the\n `StepFunGlyph` function, point the map entry at `StepFunColorGlyph`, and revert line 42.\n- `stepfun-color` carries its own blue gradient, matching `deepseek-color` / `claude-color`; use the\n monochrome `stepfun` instead if the tile should follow `currentColor`.\n- Verify the name list with:\n `node -e \"console.log(Object.keys(require('@proj-airi/lobe-icons/dist/icons.json').icons).filter(n=>/step/i.test(n)))\"`\n- No new build wiring is needed — `~icons/lobe-icons/*` is already registered in all three configs.\n\n\nknownProvider, and two existing services meet it for pi:linkcode-gatewayandcloudflare-anthropicdeclare noknownProvider.piand pi'sKnownProviderunion has neither, yet both stay pi-bindable via the baseUrl-override fallback. StepFun alone gets hard-blocked, so the catalog now answers the same question two different ways.\n\n"},{"path":"packages/foundation/providers/src/resolve.ts","line":58,"body":"Technical details
\n\nmarkdown\n# Make the pi-unknown rule consistent\n\n## Affected sites\n- `packages/foundation/providers/src/catalog.ts:153` — `supportedAgents: ['claude-code', 'codex',\n 'opencode']` on `stepfun`.\n- `packages/foundation/providers/src/catalog.ts:157-166` — `linkcode-gateway`: one `openai-chat`\n variant, no `knownProvider` at all. pi resolves it to `tier: 'native'` today.\n- `packages/foundation/providers/src/catalog.ts:219-233` — `cloudflare-anthropic`: one `anthropic`\n variant, no `knownProvider`. Same.\n- `packages/foundation/providers/src/resolve.ts:54-59` — the gate sits *after* the `grok-build`\n early return, so a `supportedAgents` list can never include or exclude `grok-build`. Silent today\n because `resolveGrokBuild` happens to refuse every non-xAI service.\n\n## Required outcome\n- One rule decides whether an agent that does not know a service is refused it or falls back to the\n baseUrl override, and every service in the catalog obeys that rule.\n\n## Suggested approach (pick one)\n- Drop `supportedAgents` and let StepFun behave like the other two: pi binds `openai-chat` with no\n `knownProvider`, which is the behavior `AGENTS.md` \"Not here yet\" already documents.\n- Or keep the hard refusal and derive it rather than hand-listing it — refuse `opencode`/`pi` when no\n variant carries a `knownProvider` for that kind — which then also covers `linkcode-gateway` and\n `cloudflare-anthropic`.\n- If the hand-written list stays, move the check above the `grok-build` return so the field means\n what it says for all five kinds.\n\n## Open questions for the human\n- Is refusing pi actually better UX than binding it to a guessed provider? The guessed-provider path\n gives a working session with the wrong model list; the refusal gives none at all.\n\n\nprotocol-unsupportedis the wrong reason for this gate, and it is user-visible:view.tsmaps every reason butoauth-other-agentandendpoint-incompletetounavailable-protocol, which renders as "The endpoint protocol is incompatible with this agent". That is false here — StepFun servesopenai-chat, which pi speaks natively; the real reason is that pi's bundled catalog has no StepFun entry.\n\n"}]Technical details
\n\nmarkdown\n# Give the agent-allowlist refusal its own reason\n\n## Affected sites\n- `packages/foundation/providers/src/resolve.ts:58` — returns `reason: 'protocol-unsupported'`.\n- `packages/foundation/providers/src/resolve.ts:26-30` — `BindingUnavailableReason`, the union that\n exists specifically so the UI can explain *why*.\n- `packages/client/workbench/src/settings/providers/view.ts:100-109` — collapses any other reason\n into `unavailable-protocol`.\n- `packages/presentation/ui/src/shell/providers/account-detail.tsx:315-316` → `en.ts:1096` /\n `zh-cn.ts:1069` — the rendered copy.\n\n## Required outcome\n- The settings row for pi on a StepFun account states a reason that is true, so a user does not go\n looking for a protocol mismatch that does not exist.\n\n## Suggested approach\n- Add a fourth reason (e.g. `agent-provider-unknown`), a matching `ProviderAgentStatus` kind, and one\n string per locale — the union already carries three cases for exactly this purpose.\n\n## Open questions for the human\n- Moot if the `supportedAgents` gate is dropped per the `catalog.ts` comment; settle that first.\n\n\n
Claude Opus | 𝕏
|
The five findings from my review above should have landed as inline comments, but a tooling error on my side dropped them from the submission and the run can only submit one review. Posting them here with their line references instead — the content is unchanged. 1.
|
PeronGH
left a comment
There was a problem hiding this comment.
Requesting changes — two issues found while verifying the StepFun wire facts against vendor docs:
- Wrong wire for codex. StepFun serves Anthropic-messages (root) and OpenAI chat (
/v1) only — no Responses API. Since codex ≥ 0.122 speaks Responses exclusively, theopenai-responsesvariant resolves codex to a dead endpoint. Drop the variant and removecodexfromsupportedAgents. - Region mismatch. Every endpoint is the China platform (
api.stepfun.com) while both locales' key hints point atplatform.stepfun.ai— separate account systems, so an international key can't authenticate here. Wireapi.stepfun.aivariants or fix the hints.
The claude-code (root Anthropic URL) and opencode (knownProvider: 'stepfun', per models.dev) parts check out.
|
Review feedback addressed in f9530ee:
Verification: @PeronGH please re-review when convenient. |
There was a problem hiding this comment.
ℹ️ All four round-1 findings are addressed, and cleanly. One comment-consistency nit inline, plus a note on the still-open blocking review.
Reviewed changes — the delta since the prior pullfrog review (ae8b8b62 → f9530ee5), a single fixup commit responding to it.
- Pointed the setup copy at the China console —
serviceDescription.stepfunnow readsplatform.stepfun.comin both locales, matching everybaseUrlthe catalog entry declares. - Replaced the hand-rolled glyph with the official mark —
~icons/lobe-icons/stepfun-color, and theSERVICE_GLYPHSvalue type is reverted toRecord<string, typeof AnthropicGlyph>.stepfun-coloris confirmed present in the installeddist/icons.json, andtsc --build --noEmitonpackages/presentation/uipasses with the narrower type restored. - Removed the
supportedAgentsaxis entirely — theServiceDescriptorfield, thestepfunvalue, and theresolveBindingearly return are all gone. This PR no longer touchesresolve.tsat all, which also retires the providersAGENTS.mddoc-drift concern and the wrong-reason-code finding. - Closed both nitpicks —
secretPlaceholder: 'sk-…'added, and the['pi', 'grok-build']loop split into two exacttoEqualassertions.
Checked rather than assumed: with the gate gone, pi falls through preferredProtocols and — since no variant carries knownProvider.pi — takes PROTOCOL_ORDER[0], which is anthropic (schema/src/model/account.ts:13). So the new { tier: 'native', protocol: 'anthropic', baseUrl: 'https://api.stepfun.com' } assertion is what the resolver really returns, it is the baseUrl-override fallthrough the package's AGENTS.md already documents, and it now matches how linkcode-gateway and cloudflare-anthropic behave for pi. Both touched test files pass (31 tests). Grepping every deepseek call site confirms the four surfaces this PR touches are the complete set a new service needs — nothing else enumerates services.
ℹ️ The blocking review's "no Responses API" claim doesn't hold for the China host
PeronGH's CHANGES_REQUESTED is still open and its first point asks for the openai-responses variant to be dropped. That point is contradicted by the doc URL now cited in the PR description: POST https://api.stepfun.com/v1/responses is documented on StepFun's China platform, so the variant is correctly declared and codex has a real target. The review's second point (region mismatch) is genuinely fixed by this commit. Only a human can clear that review, so it stays blocking regardless of this one.
Technical details
# Reconcile the blocking review against the China-host docs
## Affected sites
- `packages/foundation/providers/src/catalog.ts:144` — the `openai-responses` variant the blocking
review asks to remove. Removing it would refuse codex outright, since `preferredProtocols` gives
codex `['openai-responses']` and nothing else.
- PR review 4951165264 — `CHANGES_REQUESTED`, still open against `ae8b8b62`.
## Required outcome
- The blocking review is resolved on the record, either by the reviewer withdrawing point 1 or by a
concrete counter-example showing `/v1/responses` 404s on `api.stepfun.com`.
## Suggested approach
- StepFun runs two separate clouds: `api.stepfun.com` + console `platform.stepfun.com` (China) and
`api.stepfun.ai` + console `platform.stepfun.ai` (Global, which additionally hosts a paid Step Plan
tier at `https://api.stepfun.ai/step_plan`). English-language searches land on the `.ai` pages,
which document a different endpoint set and read as though they contradict the `.com` baseUrls.
Linking the `platform.stepfun.com` Responses reference directly in a reply is the fastest way to
settle it.Claude Opus | 𝕏
PeronGH
left a comment
There was a problem hiding this comment.
LGTM. Verified against StepFun's current API reference: /v1/responses is served for step-3.7-flash, matching the openai-responses variant and its constraint comment; both locale hints now match the China endpoints. The claude-code root URL and opencode knownProvider (stepfun in models.dev) also check out. Could not run the test suite in this environment — CI is the gate.

Summary
api.stepfun.comTesting
pnpm check:cipnpm test(2958 passed, 1 skipped)platform.stepfun.comhint,sk-…key field, protocol list, and official gradient glyphVendor reference
POST https://api.stepfun.com/v1/responses, currently supportingstep-3.7-flashCloses CODE-571