Declare required thinking, native reasoning effort and image support for both K3.1 Pro IDs. Normalize fixed parameters at the final Moonshot request boundary and cover real SDK serialization with offline regression tests. Fixes #7425
26 KiB
MindIE XML tool arguments (packages/harness/deerflow/models/mindie_provider.py)
Parse XML tool calls from the original model response before applying escaped-newline
compatibility fixes to the remaining prose. Parse JSON and Python-literal arguments
from their original escapes. For non-JSON raw-string parameters, decode literal
\n outside fenced code and trim surrounding whitespace, retaining the gateway's
existing multiline-file/command behavior. Raw strings cannot distinguish intended
literal escapes from over-escaped newlines. Numeric failures and non-finite floats
in list/tuple/set items or dict keys/values preserve the entire argument. Keep the
existing content path when no tool calls can be parsed and preserve native tool
calls. This contract is shared by sync/async generation and tool-enabled simulated
streaming; no-tool native streaming has its own chunk handling. Coverage:
tests/test_mindie_provider.py.
MindIE XML numeric arguments (mindie_provider.py)
The XML parser recognizes JSON numeric syntax, including signed, fractional,
and exponent values. Invalid numeric forms such as 007, 3., and +3 remain
strings. Numeric conversion failures preserve the entire argument, including
lists or objects with overflowing or underflowing floats or oversized integers.
Only JSON syntax errors may use the existing Python-literal fallback; never retry
numeric conversion failures through ast.literal_eval, which can silently turn
nested underflow into zero. Regression coverage is in tests/test_mindie_provider.py.
Model Factory (packages/harness/deerflow/models/factory.py)
selection.py resolves direct user model controls against the visible catalog without model calls. aliases is presentation metadata, stripped from provider kwargs and assembly parameters. Gateway copies subagent_model_name/model_selection_requested into runtime context only. Explicit choices use model:use with no deny-to-default substitution; provider errors retain deployment policy. Bind selected child names through cloned task schemas; never mutate shared tools or agent configs.
Request-admission waits follow the next scheduled admission and configured
interval, capped at 50 ms; the cap must not become a minimum poll interval that
limits high-RPM throughput. Local AdmissionError is structurally non-retriable
in LLM error handling regardless of its message text. Immediate admission and
joining the blocking FIFO are one lock-protected decision: do not split the
fast-path permit check from queue insertion, or an older caller can be overtaken
while handing off to the wait queue.
create_chat_model(name, thinking_enabled)instantiates LLM from config via reflection- Legacy disable-path heuristics treat null
extra_body,thinking, and nestedextra_body.thinkingtemplate values as absent, allowing native/vLLM fallbacks. The enable path still forwards configured values. Regression cases live intests/test_model_factory.py. - Supports
thinking_enabledflag with per-modelwhen_thinking_enabled/when_thinking_disabledoverrides; on both the legacy and the contract path every template and every synthesizedextra_bodypayload goes through_merge_thinking_payload(the native-Anthropic disable assigns the top-levelthinkingmapping outright — a tagged union keyed bytype): keys are never removed (a template can only add or override; nested mappings inherit the profile's other keys), template wins on conflicts, so a template'sextra_bodynever clobbers sibling keys such asextra_body.tool_stream. Corollary: a disable template cannot clear a key the profile sets, so enable-only keys such asextra_body.thinking.budget_tokensbelong inwhen_thinking_enabled, never in the baseextra_body. Templates are deep-copied as they are merged, so constructor kwargs never alias the cachedModelConfig. The vLLM switch has two spellings (_VLLM_THINKING_SWITCHES: legacythinkingalias,enable_thinking); when a profile spells it differently from the payload, the payload's value is mirrored onto the profile's spelling rather than deleting or canonicalizing it —VllmChatModelcollapses the pair toenable_thinking, other OpenAI-compatible classes send both, and either way the server reads the template's intent. Do not canonicalizethinking→enable_thinkingin the factory: chat templates differ on which key they honor, and onlyVllmChatModelopts into that remap - Supports vLLM-style thinking toggles via
when_thinking_enabled.extra_body.chat_template_kwargs.enable_thinkingfor Qwen reasoning models, while normalizing legacythinkingconfigs for backward compatibility - A per-request
reasoning_effortkwarg (the regular, non-bootstrap lead-agent build passes it even whenNone) is popped fromkwargsand layered likemodel_overrides: a non-Nonevalue replaces the profile's, and the thinking transforms applied afterwards (when_thinking_enabled,when_thinking_disabled, theextra_body.thinkingdisable path) still decide the final value. Never let a key reach the constructor through bothkwargsand the profile settings — Python raisesgot multiple values for keyword argumentand the lead agent cannot be built for that model. Codex checks the requested level itself. Pinned bytests/test_model_factory.pyandtests/test_lead_agent_model_resolution.py - Supports
supports_visionflag for image understanding models - Config values starting with
$resolved as environment variables - Missing provider modules surface actionable install hints from reflection resolvers (for example
uv add langchain-google-genai) - Optional
models[].request_admissionattaches a process-sharedBaseRateLimiterat the model factory. Identical explicit groups share one FIFO across model instances, threads and event loops; implicit groups use the configured model name. Policies are immutable once registered and conflicting settings fail construction. A monotonic minimum interval spaces requests without idle-time burst credit; bounded waiters poll without occupying executor threads and unregister infinally. The factory strips the policy from provider kwargs and sets exposed SDKmax_retries=0so middleware retries re-enter admission. This limits model invocations, not tokens or a distributed provider account; custom providers bypassing BaseChatModel hooks are outside the contract. Tests:test_model_request_admission.pyandtest_model_request_admission_fifo_atomic.py. _apply_openai_responses_api_defaultroutes the gpt-6 family through the OpenAI Responses API by default (use_responses_api: true+output_version: responses/v1), because the lockedlangchain-openai==1.2.1lacks a gpt-6 routing branch and OpenAI rejects function tools for gpt-6 on/v1/chat/completions. Gated onissubclass(model_class, BaseChatOpenAI)and agpt-6-prefixedmodel. An explicituse_responses_apialways wins; custom endpoints (base_url/openai_api_baseor a setOPENAI_BASE_URL/OPENAI_API_BASEenvironment variable) andAzureChatOpenAIare skipped (they may lack/responses); an explicitoutput_versionalone is preserved whileuse_responses_api: trueis still injected; injection is logged. Regression cases live intests/test_model_factory.py(issue #6841).
Reasoning Capability Contract (packages/harness/deerflow/models/reasoning.py)
Mapping-valued ModelConfig.reasoning (issue #5073) is an optional declarative contract beside
the legacy supports_thinking / supports_reasoning_effort booleans: thinking
unsupported | optional | required, on_disable_request (keep_enabled or
reject, required-thinking only), the payload dialect (auto infers it from
when_thinking_enabled), the reasoning history requirement, and an effort
vocabulary with default, generic-value aliases, and a serialization path.
A boolean or level-string reasoning (true / false, or low|medium|high for
gpt-oss style models) remains a native ChatOllama provider setting; the factory
forwards it on the legacy path, and the assembly descriptor keeps it — like a
declared contract's dialect / history — inside model_parameters so
request-affecting reasoning settings move the fingerprint. When the block is
present the booleans are derived from it and contradictory
profiles fail at config load (required + when_thinking_disabled, unsupported
- an enable template, a
defaultoutsidevalues, an explicit boolean that disagrees, an effort value ateffort.pathin the profile or in thewhen_thinking_*/thinkingtemplates that the contract rejects, apaththat is not a dotted identifier or would overwrite a whole mapping, or a stalereasoning_effortkey beside a custom effort path). The factory strips a generic effort key from runtime overrides for custom-path contracts.defaultalso governs callers that never choose an effort (summarization, title, subagents), so shipped profiles keep it below the provider's deepest level.
resolve_reasoning_contract turns any profile (legacy or declared) into an
immutable ReasoningContract, resolve_reasoning_request applies a caller's
generic thinking_enabled / reasoning_effort to it, and
reasoning_capabilities_payload projects it for /api/models and
DeerFlowClient (reasoning object, source: legacy|contract).
create_chat_model is the single enforcement point for every caller (lead agent,
subagents, summarization, title, one-shot utilities): a required-thinking model
never enters the disable branch, effort is mapped through aliases or the default
and otherwise dropped, and dialect synthesizes the on/off payload when no
template exists. Legacy profiles (no mapping-valued reasoning: contract) keep the historical path
byte-for-byte, including the synthesized reasoning_effort=minimal on the
OpenAI-compatible disable path. The lead agent and the subagent descriptor resolve
the same policy first so run metadata reports the effective values. Design note:
docs/plans/2026-09-23-reasoning-capability-contract.md; tests:
tests/test_reasoning_contract.py, the contract section of
tests/test_model_factory.py, tests/test_models_router_reasoning.py.
CLI credential files (packages/harness/deerflow/models/credential_loader.py)
CLI credential JSON uses utf-8-sig for locale-independent reads with optional
BOM. Decode failures follow the existing unreadable-source path, preserving
Claude's override-to-default fallback. Doctor's mirrored JSON reader must accept
the same optional BOM. Tests: test_credential_file_encoding.py.
Codex tool-call/result serialization (packages/harness/deerflow/models/openai_codex_provider.py)
_convert_messages uses _is_valid_call_id for both assistant tool calls
(including invalid_tool_calls) and tool results, omitting empty or
whitespace-only call IDs even when direct provider use bypasses middleware repair.
Non-blank IDs remain byte-for-byte unchanged so calls and results keep their
correlation; serialization does not trim IDs or rewrite the input messages.
Each omitted tool result emits a warning with its normalized content length;
the warning never includes the result content.
Coverage: tests/test_codex_provider.py.
Codex function-tool conversion preserves explicit strict=True and False
for wrapped and flat dictionaries; missing or None stays omitted to preserve
the provider default. Wrapped dictionaries and converted BaseTool schemas
share _convert_tools so binding cannot discard the setting. Keep caller
schemas and definitions unchanged. Offline sync/async request and tool-followup
coverage: tests/test_codex_tool_strict.py.
Codex forwards tool_choice from bind_tools and direct sync/async calls.
Normalize any/True to required, named or wrapped function selections to
Responses' flat {type: function, name: ...} shape, and omit None/False.
Validate bound selections at bind_tools; validate invocation overrides again
before HTTP. required needs at least one converted function tool, and a named
selection must match a converted function. auto and none remain valid without
tools. Per-call choices override bound choices without mutating caller definitions
or history. Offline HTTP serialization and tool-followup coverage:
tests/test_codex_tool_choice.py.
Codex SSE termination (packages/harness/deerflow/models/openai_codex_provider.py)
Completed responses with null, omitted, or empty usage retain their text,
reasoning, and tool calls. Normalize unavailable usage to the existing empty
mapping fallback while keeping AIMessage.usage_metadata as None; populated
usage mappings, including zero counts and cached/reasoning token details, remain
unchanged.
response.completed ends stream consumption immediately, before transport EOF;
retain the output-item recovery path for empty completed output. Terminal
response.failed, response.incomplete, and error events raise with their
error code/message or incomplete reason, closing the response and client without
returning partial output. Non-object error details or response containers are
reported as text instead of raising AttributeError. SSE failures do not enter
the HTTP-status retry loop.
Offline HTTP-stream coverage: tests/test_codex_stream_terminal_events.py.
Codex Retry Delays (openai_codex_provider.py)
Prefer numeric retry-after-ms over numeric or HTTP-date Retry-After hints.
Interpret HTTP-date values in UTC, including the obsolete timezone-less asctime
form. Apply utils.retry_after.bounded_retry_after_ms to every parsed delay:
past dates retry immediately; malformed, non-finite or over-24-hour hints retain
local exponential backoff. Keep millisecond hints numeric-only. Offline coverage
in tests/test_codex_retry_after.py includes public sync/async HTTP retries.
Claude Code Credentials (packages/harness/deerflow/models/credential_loader.py)
ClaudeChatModel.model_post_initcallsload_claude_code_credential()for every instance, andcreate_chat_modelbuilds fresh instances per run (lead agent, title, summarization, subagents)$CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTORis a one-shot handoff: a pipe returns EOF and a file keeps its advanced offset._read_secret_from_file_descriptortherefore caches a non-empty secret per(env_var, fd)under a lock held across the read. Do not drop the cache or the lock — later instances would get no credential, and the Anthropic SDK raisesTypeError: Could not resolve authentication methodbefore sending. Empty reads,OSError, and UTF-8 decode failures are not cached; unreadable handoffs returnNoneso the loader can try credential files. Warnings must not include token contents. The key is the descriptor number on purpose — a closed handoff keeps serving its token, and a secret placed on a recycled number in-process is not re-read unless the cache is cleared. The cache is per process, so a new process (e.g. a uvicorn--reloadworker) cannot recover a drained descriptor. Pinned bytests/test_credential_loader.pyandtests/test_claude_fd_encoding.py, including two-instanceClaudeChatModeltests
Claude Retry Delays (claude_provider.py)
_calc_backoff_ms uses utils.retry_after.bounded_retry_after_ms for integer-second
provider hints and HTTP-date hints (including obsolete forms interpreted in UTC).
Invalid or over-24-hour values retain local exponential backoff; finite negative
values and past dates clamp to zero. Sync/async 429/500 regressions live in
tests/test_claude_provider_retry_after.py; tests/test_claude_retry_after_dates.py
exercises public invocation through real SDK clients with offline HTTP transports.
Provider Retry Admission (claude_provider.py, openai_codex_provider.py)
With DeerFlow RequestAdmission, ClaudeChatModel and CodexChatModel disable their own retry loops
(retry_max_attempts=1) even when config/caller kwargs request more attempts.
Log a warning with the previous attempt count when reducing a value above one;
already-single-attempt models should not warn.
The factory also disables SDK retries; middleware retries re-enter admission.
Unpaced models and unrelated custom rate limiters keep their wrapper retry policy.
tests/test_model_request_admission.py covers public sync/async calls through offline HTTP transports.
Claude Prompt Caching (packages/harness/deerflow/models/claude_provider.py)
- The request payload shares objects with the caller: langchain-anthropic forwards Claude-native blocks (an image or document with a
source, search results) and list-form system blocks by reference, and a reused tool binding passes its own tool dicts (the lead agent re-binds per call, so its tool dicts are fresh). Writingcache_controlin place checkpointed the markers with the thread's messages, and the stale ones pushed every later request past the 4-breakpoint limit - Every request goes through
_strip_cache_control, with caching on or off, so markers stored by older checkpoints never reach the API. It copies a marked block without its marker and replaces the system, message, content and tool lists and every message dict with copies; langchain-anthropic already builds fresh message dicts and content lists, so that part is defensive _apply_prompt_cachingmust call_strip_cache_controlfirst: it then replaces slots in those payload-owned lists with marked copies and writesmsg["content"]on copied message dicts, placing at most four breakpoints. Pinned bytests/test_claude_provider_prompt_caching.py- Exclude
thinkingandredacted_thinkingblocks before selecting the last four cache candidates: Anthropic forbids directcache_controlon these blocks. Keep their content, signatures/data and order intact so they remain part of the prefix covered by a later eligible breakpoint. Stripping stale markers must not add them back to thinking blocks or mutate caller-owned history. The same suite exercises real sync/async SDK tool-followup requests through offline transports.
Claude Thinking Budget (packages/harness/deerflow/models/claude_provider.py)
- With
auto_thinking_budget=True, manualthinking.type=enabledrequests validate integer budgets of at least 1024. Ordinary thinking also requiresbudget_tokens < max_tokensand integermax_tokens > 1024; an absent/null budget usesmax(1024, int(max_tokens * 0.8)), defaulting to an 8192 output limit. - Manual interleaving permits a budget equal to or above a positive integer output limit, but only with tools, the effective
interleaved-thinking-2025-05-14beta, and a supported Sonnet 4/4.5/4.6 or Opus 4/4.1/4.5 model (aliases and dated IDs). Haiku 4.5 and Opus 4.6 do not interleave in manual mode even with that header. Follow Anthropic's model-specific rules when updating this capability gate. - Resolve the beta header as the SDK does: client defaults, then request
betas(including an empty list), thenextra_headers. Match comma-separated beta names exactly; a removed or replaced beta must not relax the budget bound. - Automatic allocation replaces the payload's thinking mapping with a copy: LangChain aliases it to
self.thinking, so in-place writes leak across requests and make smaller per-call output limits fail.auto_thinking_budget=Falsebypasses normalization/validation; absent, disabled and adaptive thinking remain untouched. Coverage:tests/test_claude_provider_thinking.py, including native request construction and offline SDK serialization.
vLLM Provider (packages/harness/deerflow/models/vllm_provider.py)
VllmChatModelsubclasseslangchain_openai:ChatOpenAIfor vLLM 0.19.0 OpenAI-compatible endpoints- Preserves vLLM's non-standard assistant
reasoningfield on full responses, streaming deltas, and follow-up tool-call turns, falling back to the legacyreasoning_contentwire field whenreasoningis absent or null (a payload carrying both keepsreasoning);_pick_reasoningowns this precedence across all three paths and preserves empty-stringreasoningrather than falling back - Designed for configs that enable thinking through
extra_body.chat_template_kwargs.enable_thinkingon vLLM 0.19.0 Qwen reasoning models, while accepting the olderthinkingalias - Normalize the legacy thinking alias on a request-owned
extra_bodycopy. Never modify model defaults or caller-owned mappings: a reused request must be able to switchthinkingoff without inheriting a synthesizedenable_thinking=True. Preserve explicit canonical-key precedence and unrelated fields. Coverage:tests/test_vllm_provider.py. cumulative_stream_usageis an opt-in model setting (defaultfalse) for endpoints that repeat cumulative token totals on each streaming chunk. The provider converts snapshots to deltas only when a stable completion id is present, isolates interleaved streams by id, and leaves the original usage untouched otherwise. Per-model tracking is lock-protected and cleared on the trailing empty-choicesframe whether or not that frame carries usage. A soft cap of 1024 ids evicts only entries idle for at least one hour; active streams may temporarily exceed the cap so eviction cannot corrupt their deltas. Regression coverage lives intests/test_vllm_provider.py.
MindIE Provider (packages/harness/deerflow/models/mindie_provider.py)
Public sync and async streams use the same message normalization. No-tool native
streams share the fence-stateful newline decoder; tool-enabled streams explicitly
request stream=False, including with streaming=True model defaults, then use
the shared simulated chunker. Preserve terminal usage exactly once, XML/native
tool calls, invalid calls, and the original messages. Offline SDK boundary tests:
tests/test_mindie_provider.py::test_public_stream_compatibility_through_sdk.
Whole-response and streaming newline decoding protect both triple-backtick and
triple-tilde fences. The earliest opener wins, only the same delimiter closes it,
and an unclosed fence keeps the remainder verbatim. Boundary and mixed-delimiter
regressions live in tests/test_mindie_newline_fences.py.
_fix_messages converts tool results to the XML text format expected by MindIE.
Only tool-message type=json payloads join the text channel; other non-text
blocks remain omitted. Keep the <tool_response> escaping boundary and the
non-serializable/circular JSON fallback. Output limits belong to
ToolOutputBudgetMiddleware, including mixed JSON/media results and configured
exemptions. Enabled PII redaction scans JSON keys and values before serialization.
Regression coverage: test_mindie_provider.py, test_pii_redaction_middleware.py,
and test_tool_output_budget_middleware.py in tests/.
Managed shared models (config/managed_models.py)
ManagedModelStore persists a Fernet-encrypted catalog plus its generated local key
under runtime_home()/managed-models. Files are atomically replaced with temporary
file permissions; complete read/modify/write transactions hold the process lock and
cross-process sidecar lock. Missing keys and invalid catalogs fail closed. Backups
and shared deployments must include both files. SQL storage does not replicate this
catalog. Admin-supplied endpoints can address local providers; only trusted admins
may create or probe them.
get_app_config() and reload_app_config() merge enabled managed models after YAML
profiles, with YAML names winning conflicts. A cached effective snapshot uses the
base config identity and content signatures of both files. Never mutate a previously
returned AppConfig: runtime-scoped and explicitly injected configurations remain
authoritative. _managed_model_names is private source metadata, not provider kwargs.
Direct AppConfig.from_file() continues to read only operator configuration.
config/managed_model_providers.py is the sole registry for provider IDs, labels,
endpoints, protocols, authentication parameter mappings, adapters and request
defaults. Capability rules match exact verified model IDs; unknown preset IDs
have an unsupported-thinking contract, no vision and no inferred context window. Verified DeepSeek presets declare an effort vocabulary of low/medium/high/max/xhigh (default low) and alias the generic minimal preset onto low, so a saved per-agent minimal default serializes as a value the official endpoint accepts (issue #4514).
Preset drafts cannot supply endpoints or adapter paths. Legacy custom
openai-compatible records retain endpoint-based official DeepSeek detection;
never infer that adapter from a model-name substring. Reads do not migrate the
catalog. New names are allocated under the existing catalog lock with bounded
ASCII slugs and random suffixes; edits preserve names and references.
Moonshot's two exact K3.1 Pro IDs use required thinking, dialect=none, preserved
history and native low/high/max effort (default max, no aliases). The shared
DeepSeek adapter gates final request normalization on the official Moonshot
endpoint and effective request model, including an extra_body.model override.
Omit the unsupported thinking switch and fixed sampling keys at both payload
levels without mutating caller mappings; the SDK merges extra_body last.
Keep K2.5, unknown IDs and other endpoints unchanged. Offline SDK coverage:
tests/test_managed_moonshot.py; live acceptance is a separate validation.
The admin-only /api/managed-models/providers exposes form metadata, not routing.
Full updates require the current revision. Omitted keys are retained only for
an unchanged provider/custom endpoint, an empty string clears the key and password
masks are rejected. Presets must have a key while enabled; disable before clearing.
Responses return has_api_key, never credentials, and request validation must
not echo sensitive input dictionaries. Keep the existing storage/lock/signature
mechanisms rather than adding a key store or writing YAML.
The Gateway probe uses create_chat_model with a fresh explicit AppConfig,
tracing off, bounded timeout/output and retries disabled. It uses the same
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.
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
tests/blocking_io/test_managed_models.py.