mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 05:06:59 +00:00
fix(tools): ignore unusable output budget limits (#6516)
* fix(tools): ignore non-finite output budget limits * fix(tools): skip overflowing budget estimates
This commit is contained in:
parent
c58d04b6b6
commit
286bb5fe1c
@ -315,6 +315,8 @@ streams simulate chunks from a non-streaming response, while no-tool streams sta
|
||||
|
||||
OpenRouter and similar OpenAI-compatible gateways should be configured with `langchain_openai:ChatOpenAI` plus `base_url`. If you prefer a provider-specific environment variable name, point `api_key` at that variable explicitly (for example `api_key: $OPENROUTER_API_KEY`).
|
||||
|
||||
The `write_file` tool's output-budget hint uses the active model's effective `max_tokens`. Missing or unusable limits, including YAML `.inf`, `-.inf`, `.nan`, and values too large for the character estimate, omit this hint without failing tool assembly; provider-specific model validation still applies.
|
||||
|
||||
To route OpenAI models through `/v1/responses`, keep using `langchain_openai:ChatOpenAI` and set `use_responses_api: true` with `output_version: responses/v1`.
|
||||
|
||||
Models whose provider contract differs from DeerFlow's generic thinking/effort assumptions can declare a per-model mapping-valued `reasoning:` block (thinking `unsupported`/`optional`/`required`, the accepted effort values with aliases and a default, the payload dialect, and the reasoning-history requirement). The setup wizard's Z.AI GLM-5.3-Flash profile uses it: thinking stays on for every foreground and background call, and the effort selector offers the model's own `low`/`high`/`max` levels. Ollama's existing boolean `reasoning: true` remains a native provider setting and is forwarded to ChatOllama. When migrating a profile to a custom effort `path`, remove any old `reasoning_effort` setting from the profile and thinking templates; configuration validation rejects the leftover key. The chat UI drops a remembered provider-specific effort when switching to a legacy model that does not advertise it. Profiles without the block keep their existing provider behavior. See `config.example.yaml` for the shape and the equivalent manual configuration.
|
||||
|
||||
@ -23,7 +23,10 @@ the same host reader serves; keep reading guidance separate from permission enfo
|
||||
|
||||
Lead and bootstrap assembly pass the constructed `chat_model` to tool assembly.
|
||||
The cloned `write_file` budget hint uses that instance's effective `max_tokens`,
|
||||
including custom-agent and thinking-mode overrides; an absent cap omits the hint.
|
||||
including custom-agent and thinking-mode overrides; an absent or unusable cap
|
||||
(including non-finite values or caps that overflow the character estimate)
|
||||
omits the hint without aborting tool assembly. Guard estimate arithmetic as
|
||||
well as integer conversion; a huge finite cap can pass the latter.
|
||||
Only standalone tool discovery without a model falls back to the base profile.
|
||||
|
||||
`get_available_tools(groups, include_mcp, model_name, subagent_enabled)` assembles:
|
||||
|
||||
@ -76,7 +76,7 @@ def _extract_max_tokens(model_config: object | None) -> int | None:
|
||||
|
||||
Handles ModelConfig (where max_tokens may be stored as an extra dynamic field),
|
||||
dicts, SimpleNamespace, or test stubs. Rejects booleans, mocks, non-numeric
|
||||
values, negative numbers, zero, and None.
|
||||
values, non-finite numbers, negative numbers, zero, and None.
|
||||
"""
|
||||
if model_config is None:
|
||||
return None
|
||||
@ -86,7 +86,7 @@ def _extract_max_tokens(model_config: object | None) -> int | None:
|
||||
try:
|
||||
val = int(raw)
|
||||
return val if val > 0 else None
|
||||
except (ValueError, TypeError):
|
||||
except (ValueError, TypeError, OverflowError):
|
||||
return None
|
||||
|
||||
|
||||
@ -219,22 +219,27 @@ def get_available_tools(
|
||||
# models configured without max_tokens.
|
||||
max_tokens = _extract_max_tokens(chat_model if chat_model is not None else model_config)
|
||||
if max_tokens is not None:
|
||||
safe_chars = int(max_tokens * 3 * 0.7)
|
||||
budget_note = (
|
||||
f"\n\nPER-RESPONSE BUDGET: your output limit is {max_tokens} tokens "
|
||||
f"(≈{safe_chars} chars). Single non-append writes above this will be truncated. "
|
||||
"For larger documents, write the first section now, "
|
||||
"then use append=True for subsequent sections."
|
||||
)
|
||||
loaded_tools = [
|
||||
_clone_tool_with_description(
|
||||
tool,
|
||||
f"{getattr(tool, 'description', '') or ''}{budget_note}",
|
||||
try:
|
||||
safe_chars = int(max_tokens * 3 * 0.7)
|
||||
except OverflowError:
|
||||
# Even finite caps can be too large for this optional estimate.
|
||||
pass
|
||||
else:
|
||||
budget_note = (
|
||||
f"\n\nPER-RESPONSE BUDGET: your output limit is {max_tokens} tokens "
|
||||
f"(≈{safe_chars} chars). Single non-append writes above this will be truncated. "
|
||||
"For larger documents, write the first section now, "
|
||||
"then use append=True for subsequent sections."
|
||||
)
|
||||
if tool.name == "write_file" and hasattr(tool, "description") and "PER-RESPONSE BUDGET:" not in (getattr(tool, "description", "") or "")
|
||||
else tool
|
||||
for tool in loaded_tools
|
||||
]
|
||||
loaded_tools = [
|
||||
_clone_tool_with_description(
|
||||
tool,
|
||||
f"{getattr(tool, 'description', '') or ''}{budget_note}",
|
||||
)
|
||||
if tool.name == "write_file" and hasattr(tool, "description") and "PER-RESPONSE BUDGET:" not in (getattr(tool, "description", "") or "")
|
||||
else tool
|
||||
for tool in loaded_tools
|
||||
]
|
||||
|
||||
# Get cached MCP tools if enabled
|
||||
# NOTE: We use ExtensionsConfig.from_file() instead of config.extensions
|
||||
|
||||
@ -10,6 +10,7 @@ from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from pydantic import BaseModel
|
||||
|
||||
from deerflow.config.app_config import AppConfig, ModelConfig, SandboxConfig, ToolConfig
|
||||
@ -70,6 +71,14 @@ def test_extract_max_tokens_various_inputs():
|
||||
assert _extract_max_tokens(mock_with) == 8000
|
||||
|
||||
|
||||
@pytest.mark.parametrize("max_tokens", [float("inf"), float("-inf"), float("nan")], ids=["inf", "negative-inf", "nan"])
|
||||
def test_extract_max_tokens_rejects_non_finite_values(max_tokens):
|
||||
"""Non-finite caps are unusable for every supported config representation."""
|
||||
assert _extract_max_tokens({"max_tokens": max_tokens}) is None
|
||||
assert _extract_max_tokens(SimpleNamespace(max_tokens=max_tokens)) is None
|
||||
assert _extract_max_tokens(ModelConfig(name="test", model="m", use="u", max_tokens=max_tokens)) is None
|
||||
|
||||
|
||||
def test_clone_tool_with_description_preserves_singleton():
|
||||
"""Verify _clone_tool_with_description returns an isolated copy and keeps original unchanged."""
|
||||
original_desc = write_file_tool.description
|
||||
@ -107,6 +116,34 @@ def test_get_available_tools_with_model_config_lacking_max_tokens():
|
||||
assert "PER-RESPONSE BUDGET:" not in write_tool.description
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"yaml_max_tokens",
|
||||
[
|
||||
".inf",
|
||||
"-.inf",
|
||||
".nan",
|
||||
pytest.param("1.0e+308", id="huge-finite-float"),
|
||||
pytest.param("1" + "0" * 400, id="huge-integer"),
|
||||
pytest.param('"' + "1" + "0" * 400 + '"', id="huge-integer-string"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("use_chat_model", [False, True], ids=["profile", "constructed-model"])
|
||||
def test_get_available_tools_omits_unusable_budget(yaml_max_tokens, use_chat_model):
|
||||
"""Non-finite or overflowing caps must not abort assembly or produce a hint."""
|
||||
baseline_desc = write_file_tool.description
|
||||
max_tokens = yaml.safe_load(f"max_tokens: {yaml_max_tokens}")["max_tokens"]
|
||||
model = ModelConfig(name="budget-model", model="m", use="u", max_tokens=4096 if use_chat_model else max_tokens)
|
||||
config = _build_minimal_app_config([model])
|
||||
chat_model = SimpleNamespace(max_tokens=max_tokens) if use_chat_model else None
|
||||
|
||||
tools = get_available_tools(model_name="budget-model", app_config=config, include_mcp=False, chat_model=chat_model)
|
||||
write_tool = next(t for t in tools if t.name == "write_file")
|
||||
|
||||
assert "PER-RESPONSE BUDGET:" not in write_tool.description
|
||||
assert write_tool.description == baseline_desc
|
||||
assert write_file_tool.description == baseline_desc
|
||||
|
||||
|
||||
def test_write_file_singleton_remains_unmutated_across_assemblies():
|
||||
"""Verify write_file_tool process singleton is never mutated during tool assembly."""
|
||||
baseline_desc = write_file_tool.description
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user