diff --git a/CHANGELOG.md b/CHANGELOG.md index 4316694677..e5484ec85a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index ea09422c7a..0fae47096a 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -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 diff --git a/README.md b/README.md index 055038df9b..6952ad25d8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/README_zh.md b/README_zh.md index dfa4893f78..fcd7419457 100644 --- a/README_zh.md +++ b/README_zh.md @@ -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。 diff --git a/backend/docs/CONFIGURATION.md b/backend/docs/CONFIGURATION.md index d4fb740be6..931e5b9b7f 100644 --- a/backend/docs/CONFIGURATION.md +++ b/backend/docs/CONFIGURATION.md @@ -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, diff --git a/backend/packages/harness/deerflow/config/managed_model_providers.py b/backend/packages/harness/deerflow/config/managed_model_providers.py index 20a448f6f1..27840c1453 100644 --- a/backend/packages/harness/deerflow/config/managed_model_providers.py +++ b/backend/packages/harness/deerflow/config/managed_model_providers.py @@ -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} diff --git a/backend/packages/harness/deerflow/models/AGENTS.md b/backend/packages/harness/deerflow/models/AGENTS.md index 1dba923b81..2807e85241 100644 --- a/backend/packages/harness/deerflow/models/AGENTS.md +++ b/backend/packages/harness/deerflow/models/AGENTS.md @@ -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 diff --git a/backend/packages/harness/deerflow/models/patched_stepfun.py b/backend/packages/harness/deerflow/models/patched_stepfun.py index 281afa4c7a..4c5ffb0e1a 100644 --- a/backend/packages/harness/deerflow/models/patched_stepfun.py +++ b/backend/packages/harness/deerflow/models/patched_stepfun.py @@ -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", []), diff --git a/backend/tests/test_managed_provider_presets.py b/backend/tests/test_managed_provider_presets.py index 605a3a561b..03b46441c2 100644 --- a/backend/tests/test_managed_provider_presets.py +++ b/backend/tests/test_managed_provider_presets.py @@ -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"] diff --git a/backend/tests/test_model_catalog.py b/backend/tests/test_model_catalog.py index b7aa55cd38..418edb1e7f 100644 --- a/backend/tests/test_model_catalog.py +++ b/backend/tests/test_model_catalog.py @@ -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 diff --git a/backend/tests/test_patched_stepfun.py b/backend/tests/test_patched_stepfun.py index cc62216951..485543754a 100644 --- a/backend/tests/test_patched_stepfun.py +++ b/backend/tests/test_patched_stepfun.py @@ -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 # ---------------------------------------------------------------------------