AGENTS.md@backend/packages/harness/deerflow/models · git:20261013.e98179a · 2026-10-13 · sha256 b240298fce1af843
AGENTS.md@backend/packages/harness/deerflow/models git:20261013.e98179aA
Immutable. This exact content never changes and is served at /api/v1/blob/b240298fce1af843.
### 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 conversion failures and unsafe Python-literal containers still retain the entire original 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`) 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 - Supports `thinking_enabled` flag with per-model `when_thinking_enabled` / `when_thinking_disabled` overrides; on both the legacy and the contract path every template and every synthesized `extra_body` payload goes through `_merge_thinking_payload` (the native-Anthropic disable assigns the top-level `thinking` mapping outright — a tagged union keyed by `type`): 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's `extra_body` never clobbers sibling keys such as `extra_body.tool_stream`. Corollary: a disable template cannot clear a key the profile sets, so enable-only keys such as `extra_body.thinking.budget_tokens` belong in `when_thinking_enabled`, never in the base `extra_body`. Templates are deep-copied as they are merged, so constructor kwargs never alias the cached `ModelConfig`. The vLLM switch has two spellings (`_VLLM_THINKING_SWITCHES`: legacy `thinking` alias, `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 — `VllmChatModel` collapses the pair to `enable_thinking`, other OpenAI-compatible classes send both, and either way the server reads the template's intent. Do not canonicalize `thinking` → `enable_thinking` in the factory: chat templates differ on which key they honor, and only `VllmChatModel` opts into that remap - Supports vLLM-style thinking toggles via `when_thinking_enabled.extra_body.chat_template_kwargs.enable_thinking` for Qwen reasoning models, while normalizing legacy `thinking` configs for backward compatibility - A per-request `reasoning_effort` kwarg (the regular, non-bootstrap lead-agent build passes it even when `None`) is popped from `kwargs` and layered like `model_overrides`: a non-`None` value replaces the profile's, and the thinking transforms applied afterwards (`when_thinking_enabled`, `when_thinking_disabled`, the `extra_body.thinking` disable path) still decide the final value. Never let a key reach the constructor through both `kwargs` and the profile settings — Python raises `got multiple values for keyword argument` and the lead agent cannot be built for that model. Codex checks the requested level itself. Pinned by `tests/test_model_factory.py` and `tests/test_lead_agent_model_resolution.py` - Supports `supports_vision` flag 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_admission` attaches a process-shared `BaseRateLimiter` at 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 in `finally`. The factory strips the policy from provider kwargs and sets exposed SDK `max_retries=0` so 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.py` and `test_model_request_admission_fifo_atomic.py`. ### 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 `default` outside `values`, an explicit boolean that disagrees, an effort value at `effort.path` in the profile or in the `when_thinking_*` / `thinking` templates that the contract rejects, a `path` that is not a dotted identifier or would overwrite a whole mapping, or a stale `reasoning_effort` key beside a custom effort path). The factory strips a generic effort key from runtime overrides for custom-path contracts. `default` also 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 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`. ### Claude Code Credentials (`packages/harness/deerflow/models/credential_loader.py`) - `ClaudeChatModel.model_post_init` calls `load_claude_code_credential()` for every instance, and `create_chat_model` builds fresh instances per run (lead agent, title, summarization, subagents) - `$CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR` is a one-shot handoff: a pipe returns EOF and a file keeps its advanced offset. `_read_secret_from_file_descriptor` therefore 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 raises `TypeError: Could not resolve authentication method` before sending. Empty reads, `OSError`, and UTF-8 decode failures are not cached; unreadable handoffs return `None` so 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 `--reload` worker) cannot recover a drained descriptor. Pinned by `tests/test_credential_loader.py` and `tests/test_claude_fd_encoding.py`, including two-instance `ClaudeChatModel` tests ### Claude Retry Delays (`claude_provider.py`) `_calc_backoff_ms` uses `utils.retry_after.bounded_retry_after_ms` for integer-second provider hints. Invalid or over-24-hour values retain local exponential backoff; finite negative values clamp to zero. Sync/async 429/500 regressions live in `tests/test_claude_provider_retry_after.py` and use offline provider doubles. ### 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). Writing `cache_control` in 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_caching` must call `_strip_cache_control` first: it then replaces slots in those payload-owned lists with marked copies and writes `msg["content"]` on copied message dicts, placing at most four breakpoints. Pinned by `tests/test_claude_provider_prompt_caching.py` - Exclude `thinking` and `redacted_thinking` blocks before selecting the last four cache candidates: Anthropic forbids direct `cache_control` on 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`, manual `thinking.type=enabled` requests validate integer budgets of at least 1024. Ordinary thinking also requires `budget_tokens < max_tokens` and integer `max_tokens > 1024`; an absent/null budget uses `max(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-14` beta, 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](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#interleaved-thinking-in-manual-mode) when updating this capability gate. - Resolve the beta header as the SDK does: client defaults, then request `betas` (including an empty list), then `extra_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=False` bypasses 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`) - `VllmChatModel` subclasses `langchain_openai:ChatOpenAI` for vLLM 0.19.0 OpenAI-compatible endpoints - Preserves vLLM's non-standard assistant `reasoning` field on full responses, streaming deltas, and follow-up tool-call turns, falling back to the legacy `reasoning_content` wire field when `reasoning` is absent or null (a payload carrying both keeps `reasoning`); `_pick_reasoning` owns this precedence across all three paths and preserves empty-string `reasoning` rather than falling back - Designed for configs that enable thinking through `extra_body.chat_template_kwargs.enable_thinking` on vLLM 0.19.0 Qwen reasoning models, while accepting the older `thinking` alias - Normalize the legacy thinking alias on a request-owned `extra_body` copy. Never modify model defaults or caller-owned mappings: a reused request must be able to switch `thinking` off without inheriting a synthesized `enable_thinking=True`. Preserve explicit canonical-key precedence and unrelated fields. Coverage: `tests/test_vllm_provider.py`. - `cumulative_stream_usage` is an opt-in model setting (default `false`) 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-`choices` frame 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 in `tests/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`. `_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. 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. 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`.