feat(models): add StepFun provider preset (#7988)

* feat(models): add StepFun provider preset

* fix(models): standardize the StepFun preset label

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Eilen Shin 2026-11-08 09:49:43 +08:00 committed by GitHub
parent 1dae5eb363
commit 2a8927c5e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 55 additions and 9 deletions

View File

@ -22,6 +22,10 @@ This section accumulates work toward the **2.3.0** milestone
native subagents pause and resume. Unconfirmed messages can be retried
after a Gateway restart. ([#7729])
- **models:** Administrators can select StepFun in **Settings → Models** and
connect an ordinary StepFun API account with an API key and model ID.
([#7988](https://github.com/bytedance/deer-flow/pull/7988))
### Fixed
- **community:** A search or fetch API key that ends in a newline no longer

View File

@ -21,6 +21,9 @@
Queue 等当前任务结束后再执行下一条消息;Steer 让主 Agent 先处理新要求,原生子 Agent 可暂停后恢复。
Gateway 重启后,尚未确认生效的消息可以手动重试。([#7729])
- **模型:** 管理员可在 **设置 → 模型** 中选择 StepFun(阶跃星辰),
填写 API Key 和模型 ID 即可接入普通 API。([#7988](https://github.com/bytedance/deer-flow/pull/7988))
### 修复
- **社区工具:** 以换行结尾的搜索或抓取 API 密钥不再泄露到对话中。GroundRoute

View File

@ -335,7 +335,7 @@ fences, including unclosed fences, in complete and streamed replies.
Administrators can open **Settings → Models** to select a provider and enter
only its **API Key** and **model ID**. Endpoints, authentication and adapters are
backend presets for OpenAI, Anthropic, DeepSeek, Alibaba Cloud Bailian/Qwen,
Volcengine/Doubao, Moonshot/Kimi, Zhipu/Z.AI and MiniMax. Region and plan-specific
Volcengine/Doubao, Moonshot/Kimi, Zhipu/Z.AI, MiniMax and StepFun. Region and plan-specific
entries are explicitly distinguished. Arbitrary model IDs are accepted; internal
names are generated automatically and remain stable when a model ID changes.
Newly saved models are enabled and immediately refresh existing chat and Agent

View File

@ -109,7 +109,7 @@ DeerFlow 新近集成了 BytePlus 自研的智能搜索与抓取工具集——[
管理员可在 **设置 → 模型 → 添加模型** 中选择厂商,仅填写 **API Key** 和
**模型 ID**。后端预置 OpenAI、Anthropic、DeepSeek、百炼/通义千问、火山/豆包、
Moonshot/Kimi、智谱/Z.AI 和 MiniMax 的端点、认证与适配器,地域和 Coding Plan
Moonshot/Kimi、智谱/Z.AI、MiniMax 和阶跃星辰(StepFun)的端点、认证与适配器,地域和 Coding Plan
使用明确区分的选项。模型 ID 不受内置枚举限制;内部名称自动生成且保持稳定。
新模型默认启用,保存后自动刷新聊天及 Agent 的模型选择器,无需重启 Gateway。

View File

@ -334,6 +334,7 @@ are:
| `zai` | Z.AI, international ordinary API | `https://api.z.ai/api/paas/v4` | Chat Completions / Bearer |
| `minimax` | MiniMax, international API | `https://api.minimax.io/v1` | Chat Completions / Bearer |
| `minimax-cn` | MiniMax, China API | `https://api.minimaxi.com/v1` | Chat Completions / Bearer |
| `stepfun` | StepFun, ordinary API | `https://api.stepfun.com/v1` | Chat Completions / Bearer |
| `openai-compatible` | Custom OpenAI-compatible, including unauthenticated local endpoints | Administrator-supplied HTTP(S) base URL | Chat Completions / optional key |
Keys for different regions and Coding Plans are not interchangeable. These
@ -342,6 +343,16 @@ Plan from the model name. Plans/regions without a preset can use the custom
OpenAI-compatible option if their documented protocol is compatible; native
protocols need a verified backend preset or operator YAML configuration.
The StepFun preset uses the existing `PatchedChatStepFun` adapter to capture
`reasoning` / `reasoning_content` in responses and replay reasoning history.
Both connection probes and chat send the documented `max_tokens` output bound.
Model IDs remain free-form; this provider registration does not infer thinking,
vision or context-window capabilities from their names. The preset is for
ordinary API credentials. Step Plan has a separate `/step_plan/v1` endpoint;
use the custom OpenAI-compatible option for that channel. See the official
[OpenAI migration guide](https://platform.stepfun.com/docs/zh/guides/developer/openai)
and [Chat Completions reference](https://platform.stepfun.com/docs/zh/api-reference/chat/chat-completion-create).
Settings → Models → **Fetch and add models** reads the connection's model
catalog without changing configuration. Administrators can reuse an existing
saved connection without exposing its key, search IDs, select visible results,

View File

@ -12,6 +12,7 @@ from urllib.parse import urlsplit
_OPENAI = "langchain_openai:ChatOpenAI"
_DEEPSEEK = "deerflow.models.patched_deepseek:PatchedChatDeepSeek"
_MINIMAX = "deerflow.models.patched_minimax:PatchedChatMiniMax"
_STEPFUN = "deerflow.models.patched_stepfun:PatchedChatStepFun"
MOONSHOT_K31_MODELS = frozenset({"kimi-k3.1-pro", "kimi-k3.1-pro-highspeed"})
OPENAI_GPT6_MODELS = frozenset({"gpt-6-astra", "gpt-6.1-sol", "gpt-6.1-terra", "gpt-6-sol", "gpt-6-luna"})
@ -45,6 +46,7 @@ PROVIDERS = (
ManagedModelProvider("zai", "Z.AI (International, API)", "https://api.z.ai/api/paas/v4", _DEEPSEEK, endpoint_parameter="api_base"),
ManagedModelProvider("minimax", "MiniMax (International, API)", "https://api.minimax.io/v1", _MINIMAX, defaults={"temperature": 1.0}),
ManagedModelProvider("minimax-cn", "MiniMax (China, API)", "https://api.minimaxi.com/v1", _MINIMAX, defaults={"temperature": 1.0}),
ManagedModelProvider("stepfun", "StepFun", "https://api.stepfun.com/v1", _STEPFUN),
ManagedModelProvider("openai-compatible", "Custom OpenAI-compatible", None),
)
_PROVIDER_BY_ID = {provider.id: provider for provider in PROVIDERS}

View File

@ -267,6 +267,8 @@ provider settings/reasoning transforms as chat. Disable optional thinking for
short probes; required thinking and unknown third-party models use automatic
tool choice. MiniMax's existing adapter normalizes the OpenAI SDK's
`max_completion_tokens` into MiniMax's documented `max_tokens` for both paths.
The `stepfun` preset reuses `PatchedChatStepFun` for reasoning capture/replay and
the same token-field normalization; its ordinary API route is distinct from Step Plan.
Tests: `test_managed_models.py`, `test_managed_deepseek.py`,
`test_managed_provider_presets.py` (real vendor SDKs with HTTP doubles), opt-in
`test_managed_deepseek_live.py` / `test_managed_presets_live.py`, and

View File

@ -83,6 +83,10 @@ class PatchedChatStepFun(ChatOpenAI):
"""Restore ``reasoning_content`` on historical assistant messages."""
original_messages = self._convert_input(input_).to_messages()
payload = super()._get_request_payload(input_, stop=stop, **kwargs)
# StepFun documents max_tokens; ChatOpenAI emits the newer OpenAI name.
# Preserve the configured output bound on probes and ordinary chat calls.
if "max_completion_tokens" in payload:
payload["max_tokens"] = payload.pop("max_completion_tokens")
restore_assistant_payloads(
payload.get("messages", []),

View File

@ -35,6 +35,7 @@ def store(tmp_path, monkeypatch):
("moonshot-cn", "https://api.moonshot.cn/v1", "deerflow.models.patched_deepseek:PatchedChatDeepSeek"),
("zai", "https://api.z.ai/api/paas/v4", "deerflow.models.patched_deepseek:PatchedChatDeepSeek"),
("minimax", "https://api.minimax.io/v1", "deerflow.models.patched_minimax:PatchedChatMiniMax"),
("stepfun", "https://api.stepfun.com/v1", "deerflow.models.patched_stepfun:PatchedChatStepFun"),
],
)
def test_preset_routing_and_conservative_unknown_capabilities(provider, endpoint, adapter):
@ -150,6 +151,7 @@ def test_preset_overrides_and_injected_config_remain_authoritative(store):
@pytest.mark.asyncio
async def test_provider_metadata_requires_admin():
catalog = await router.list_model_providers(_ADMIN)
assert {"id": "stepfun", "display_name": "StepFun", "requires_api_key": True, "custom_endpoint": False} in catalog["providers"]
assert {p["id"] for p in catalog["providers"]} == {p.id for p in PROVIDERS}
assert all(set(p) == {"id", "display_name", "requires_api_key", "custom_endpoint"} for p in catalog["providers"])
with pytest.raises(HTTPException) as exc:
@ -333,7 +335,8 @@ def test_generated_name_collision_with_yaml_is_retried(store, monkeypatch):
@pytest.mark.asyncio
async def test_preset_api_save_refresh_yaml_conflict_and_disable(store, tmp_path, monkeypatch):
@pytest.mark.parametrize("provider", ["openai", "stepfun"])
async def test_preset_api_save_refresh_yaml_conflict_and_disable(store, tmp_path, monkeypatch, provider):
from unittest.mock import AsyncMock
from app.gateway.routers import models as models_router
@ -358,7 +361,7 @@ async def test_preset_api_save_refresh_yaml_conflict_and_disable(store, tmp_path
snapshot = get_app_config()
try:
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
create = await client.put("/api/managed-models", json={"config": {"provider": "openai", "model": "org/新 模型", "api_key": "private-test-key"}})
create = await client.put("/api/managed-models", json={"config": {"provider": provider, "model": "org/新 模型", "api_key": "private-test-key"}})
assert create.status_code == 200
saved = create.json()
assert saved["enabled"] and saved["has_api_key"]
@ -370,7 +373,7 @@ async def test_preset_api_save_refresh_yaml_conflict_and_disable(store, tmp_path
assert snapshot.models[0].api_key == "yaml-test-key"
assert len(snapshot.models) == 1
assert len(next_snapshot.models) == 2
edit = {"config": {"name": saved["name"], "provider": "openai", "model": "new-model"}, "expected_revision": saved["revision"]}
edit = {"config": {"name": saved["name"], "provider": provider, "model": "new-model"}, "expected_revision": saved["revision"]}
changed = await client.put("/api/managed-models", json=edit)
assert changed.status_code == 200
assert changed.json()["name"] == saved["name"]

View File

@ -27,17 +27,18 @@ def connection(**kw):
@pytest.mark.asyncio
async def test_discovery_uses_saved_key_and_never_writes(setup, monkeypatch):
saved = setup.save(ManagedModel(provider="openai", model="existing", api_key="catalog-secret"), expected_revision=None)
@pytest.mark.parametrize("provider,endpoint", [("openai", "https://api.openai.com/v1/models"), ("stepfun", "https://api.stepfun.com/v1/models")])
async def test_discovery_uses_saved_key_and_never_writes(setup, monkeypatch, provider, endpoint):
saved = setup.save(ManagedModel(provider=provider, model="existing", api_key="catalog-secret"), expected_revision=None)
before = setup.path.read_bytes()
async def send(client, request, **kwargs):
assert str(request.url) == "https://api.openai.com/v1/models"
assert str(request.url) == endpoint
assert request.headers["authorization"] == "Bearer catalog-secret"
return httpx.Response(200, request=request, json={"data": [{"id": "existing"}, {"id": "new"}, {"id": "new"}, {"id": "bad\n"}, {"id": 5}]})
monkeypatch.setattr(httpx.AsyncClient, "send", send)
body = router.ModelConnection(provider="openai", saved_model_name=saved.name, expected_revision=saved.revision)
body = router.ModelConnection(provider=provider, saved_model_name=saved.name, expected_revision=saved.revision)
result = await router.discover_models(ADMIN, body)
assert result == {"models": [{"id": "existing", "configured": True}, {"id": "new", "configured": False}], "truncated": False}
assert setup.path.read_bytes() == before

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
@ -35,6 +36,21 @@ def test_lc_secrets_contains_stepfun_api_key_mapping():
assert model.lc_secrets["openai_api_key"] == "STEPFUN_API_KEY"
@pytest.mark.parametrize("limit_field", ["max_tokens", "max_completion_tokens"])
def test_request_uses_documented_max_tokens_for_instance_and_call_limits(limit_field):
model = _make_model(max_tokens=128)
messages = [HumanMessage(content="Hello")]
initial = model._get_request_payload(messages)
assert initial["max_tokens"] == 128
assert "max_completion_tokens" not in initial
overrides = {limit_field: 64}
payload = model._get_request_payload(messages, **overrides)
assert payload["max_tokens"] == 64
assert "max_completion_tokens" not in payload
assert overrides == {limit_field: 64}
assert model.max_tokens == 128
# ---------------------------------------------------------------------------
# _extract_reasoning helper
# ---------------------------------------------------------------------------