v1.20.4 — Browser agent reliability, Anthropic Claude 4.x model-id normalization, robust LLM config on model change
[1.20.4] - 2026-05-12
Fixed — Browser agent: model 404, premature step kill, AX-tree starvation
claude-opus-4.6(andclaude-opus-4.5,claude-sonnet-4.6,claude-haiku-4.5) →404 not_found_error— the LLM catalogue (llm_models,llm_config_overrides) anddomains/llm_config/constants.pystored Anthropic 4.x model ids with a dot (claude-opus-4.6), but the Anthropic API only accepts the dashed form (claude-opus-4-6, …) —core/config/llm.pyalready used the dashed form. Any LLM type pointed at one of these (e.g.browser_agentafter the admin picked "Claude Opus 4.6") crashed at instantiation. Renamed to the dashed form everywhere: pricing/config seeds,mcp_app_react_agentdefault, unit tests, frontend i18n strings (claude-opus-4-5template label), docs. New Alembic migrationrename_anthropic_model_ids_001rewrites the existingllm_models.model_name/llm_config_overrides.modelrows (idempotent regex^(claude-[a-z]+-[0-9]+)\.([0-9]+)$→\1-\2, reversible); DEV migrated, PROD updated via the equivalent in-transaction SQL.browser_task_toolstep killed mid-task — the parallel executor wraps each tool step inasyncio.wait_for(timeout=step.timeout_seconds or DEFAULT_TOOL_TIMEOUT_SECONDS), andbrowser_task_tool(a full nested ReAct loop) was absent from_HIGH_LATENCY_TOOLS, so the planner's 30/60/120 stimeout_secondscancelled the loop before it could finish (observed step durations: exactly 30 009 / 60 020 / 120 009 ms, thenasyncio.CancelledErrorinsidebrowser_task_tool). Addedbrowser_task_toolto_HIGH_LATENCY_TOOLSwith a dedicated floorBROWSER_TOOL_TIMEOUT_SECONDS = 300 sand ceilingMAX_BROWSER_TOOL_TIMEOUT_SECONDS = 600 s(the genericMAX_TOOL_TIMEOUT_SECONDS = 120 sno longer applies to it).- Accessibility-tree truncation hid form controls —
BROWSER_AX_TREE_MAX_TOKENScode default raised5000 → 15000(newBROWSER_AX_TREE_MAX_TOKENS_DEFAULTconstant, referenced fromBrowserSettings);.env.example/.env.prod.exampleupdated.
Added — BROWSER_REACT_MAX_ITERATIONS
New env var (default 15, range 1–50) capping the create_react_agent recursion_limit of the browser agent loop run by browser_task_tool — mirrors REACT_AGENT_MAX_ITERATIONS / MCP_REACT_MAX_ITERATIONS. Constant BROWSER_REACT_MAX_ITERATIONS_DEFAULT, field browser_react_max_iterations on BrowserSettings, wired in browser_tools.py (was a hard-coded 15); .env.example / .env.prod.example and BROWSER_CONTROL.md updated.
Changed — Browser agent prompt (prompts/v1/browser_agent_prompt.txt)
Rewritten from web-research best practices (notably the browser-use agent prompt): explicit read-vs-transactional task modes, no fabricated query-param URLs, observe→act→verify loop with a mandatory snapshot after state-changing actions, stale-[ref] handling, autocomplete → click-the-suggestion (not Enter), fill only on real input controls (textbox/searchbox/combobox/spinbutton), cookie-banner/403/login obstacle handling, stuck-loop detection, stop-before-payment + report-only-facts. browser_agent_prompt added to the PromptName Literal (was missing — pre-existing gap).
Fixed — reasoning_effort ↔ model coherence (robustness on model/provider change)
Switching a model/provider on an LLM type could leave a reasoning_effort whose shape no longer matched the new model's reasoning_widget (e.g. a DeepSeek {"effort": "off"} enum value left on a config whose effective model became the Qwen toggle_budget default — the admin UI rendered it as "thinking disabled", so it looked fine), which then crashed the typed reasoning builder at get_llm() time (RuntimeError: ... must be ReasoningEffortToggleBudget, got ReasoningEffortEnum. Validation upstream is broken.). Three layers now prevent this:
- Frontend — the admin dialog normalizes
reasoning_effortto the newly selected model onmodel/providerchange: kept only if its shape fits the newreasoning_widget(and, forenum, the value is allowed), otherwise reset tonull. NewcoerceReasoningEffortForModel/reasoningEffortMatchesModelincomponents/settings/llm-config/reasoningHelpers.ts. - Write path —
LLMConfigService.update_configvalidatesreasoning_effortagainst the effective model (update.model, orLLM_DEFAULTS[llm_type].modelwhen the model override isnull), rejecting an incompatible combination with422+ structuredctx. - Merge runtime —
core/llm_config_helper.py::merge_config→ new_reconcile_reasoning_effortdrops any still-incompatiblereasoning_effortat merge time (stale override row, outdated seed, manual edit, a past bug), falling back to the model's default and loggingllm_config_reasoning_effort_dropped—get_llm()degrades gracefully regardless of the drift's origin.
Shared non-raising predicate reasoning_effort_matches_widget(caps, value) (twin of validate_reasoning_effort) in domains/llm_config/reasoning_validation.py — one source of truth for "is this value valid for this model?", reused by layers 1 and 3.
Fixed — Production build: stale GeoIP database date
apps/api/Dockerfile.prod hard-coded ARG DBIP_DATE=2026-03 for the DB-IP City Lite GeoIP download; db-ip.com keeps only the last ~2 months online, so the URL started returning 404 ("not in gzip format" after curl -L wrote the error page) and gunzip failed the production image build. The geoip-downloader stage now resolves the latest available month at build time — current month, then the previous one — using curl -fsSL (so a 404 cleanly falls through to the next candidate); still overridable with --build-arg DBIP_DATE=YYYY-MM. Build-only change, no runtime impact.
Tests
tests/unit/domains/llm_config/test_reasoning_validation.py::TestReasoningEffortMatchesWidget— none/enum/budget_int/toggle_budget cases incl. the{"effort": "off"}-on-Qwen regression.tests/unit/domains/llm_config/test_config_helper.py::TestEffectiveConfigReasoningReconciliation— incompatiblereasoning_effortoverride dropped, compatible kept, unknown model left untouched.apps/web/src/components/settings/llm-config/__tests__/reasoningHelpers.test.ts— 10 cases forreasoningEffortShape/reasoningEffortMatchesModel/coerceReasoningEffortForModel.test_reasoning_validation.py/test_constants.py/test_llm_defaults_compliance.py/test_reasoning_builders.pyupdated for theclaude-opus-4-6etc. rename.
Documentation
docs/technical/LLM_CONFIG_ADMIN.md— new "Cohérencereasoning_effort↔ modèle" section (the 3-layer guarantee),reasoning_validation.py/ReasoningWidget.tsx/reasoningHelpers.tsadded to the file map, PUT-semantics note.docs/technical/BROWSER_CONTROL.md—BROWSER_REACT_MAX_ITERATIONS+ browser step-timeout +BROWSER_AX_TREE_MAX_TOKENSnotes; new "Agent prompt" section summarising the prompt's behavioural contract.- In-app FAQ (
apps/web/locales/*/translation.json, 6 languages): newfaq.changelog.versions.v1_20_4entry (3 user/admin items);faq.intro.features.browserControlblurb refined (multi-step flow, stops at login/payment, reports a summary);faq.sections.tool_examples_externalq18/q19 refined (multi-step example, "good to know" note, corrected timing).docs/knowledge/06_external_services.mdpropagated from the updated FAQ.docs/GETTING_STARTED.mdcompatibility line bumped to v1.20.4 (with v1.20.1–v1.20.4 highlights).README.mdtop banner updated to v1.20.4 (v1.20.3 demoted to a<details>block). README.md/docs/technical/LLM_PRICING_TEMPLATES.md/docs/guides/GUIDE_CONFIG_ARCHITECTURE.md/ the 6apps/web/locales/*/translation.json—claude-opus-4.5→claude-opus-4-5in prose/examples.- Version bumped to
1.20.4acrossapps/api/pyproject.toml,apps/web/package.json,package.json, andapps/web/src/lib/version.ts(LAST_UPDATED = 2026-05-12T22:48:00, shown on the landing page).