Fix validator severity mapping (#161) and add explicit reasoning control (#154) - #165
Conversation
hedtools reports issue severity as an ErrorSeverity enum (ERROR=1, WARNING=10), not the string "error". The Python validator compared it to "error", which is never true, so every error was filed as a warning and is_valid came back True for any input: "NotARealTag/Foo" validated clean, and the workflow's refinement loop could not fire because validation never failed. The JavaScript validator path was unaffected (the JS validator returns separate error and warning arrays). Anything not explicitly a warning now counts as an error, so an unrecognized severity fails closed rather than passing silently. Note this makes the refine loop live on the Python-validator path, so requests with genuinely invalid tags will now iterate where they previously returned on the first attempt. Tested: severity mapping for unknown tags, non-base tags, repeated expressions (all errors, is_valid False) and tag extensions (warning, is_valid True). test_validate_invalid_tag previously asserted "is_valid is False or warnings > 0" with a note claiming HED 8.3.0+ reports invalid tags as warnings; that note described this bug, and the test now asserts the error. 533 tests pass.
Adds a `thinking` parameter that overrides disable_reasoning, with
validation matching what the API actually enforces (verified against the
AWS endpoint):
- Haiku 4.5 has no adaptive mode and needs
{"type": "enabled", "budget_tokens": N}, N >= 1024 and below max_tokens.
- Sonnet 5 rejects thinking.type "enabled" ("Use thinking.type adaptive")
and takes {"type": "adaptive"} or {"type": "disabled"}.
- Enabling thinking drops `temperature`: the API returns 400
("temperature may only be set to 1 when thinking is enabled") otherwise.
- {"type": "disabled"} is accepted on both, so a caller can express "off"
uniformly instead of special-casing per model.
Needed to measure whether thinking earns its cost on the annotation agent;
the answer decides whether any default changes.
Tested: each accepted and rejected shape per model, the temperature drop
and its absence when thinking is disabled, budget bounds, and precedence
over disable_reasoning.
Deploying hedit with
|
| Latest commit: |
6b8a950
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://97603363.hedit.pages.dev |
| Branch Preview URL: | https://fix-validator-severity-161.hedit.pages.dev |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Measured six configurations over the 15 benchmark descriptions, varying only the annotation LLM (evaluation and keyword stayed on Haiku with thinking off): | arm | 1st-attempt valid | attempts | latency | cost/req | |----------------------------|-------------------|----------|---------|----------| | Haiku, no thinking | 5/15 | 1.87 | 10.3s | $0.0092 | | Haiku, no thinking, temp 1 | 6/15 | 1.73 | 10.0s | $0.0086 | | Haiku, 1024 budget | 11/15 | 1.27 | 22.8s | $0.0129 | | Haiku, 2048 budget | 13/15 | 1.13 | 20.9s | $0.0114 | | Sonnet 5, thinking off | 10/15 | 1.40 | 11.8s | $0.0261 | | Sonnet 5, adaptive | 12/15 | 1.20 | 12.9s | $0.0267 | Annotation now runs with a 2048-token budget on Haiku and adaptive on Sonnet 5, which has no budget mode. Notable results: 2048 costs less per request than 1024 because it removes more refinement rounds than it adds in thinking tokens; total LLM calls drop 71 -> 49; no reasoning loops appear (attempts fall); and Sonnet 5 reaches no higher first-attempt validity than Haiku-with-thinking at 2.3x the cost. A no-thinking arm at temperature 1.0 rules out the forced temperature change as the cause. The price is latency, roughly doubled, so HEDIT_ANNOTATION_THINKING_BUDGET=off turns it back off for latency-sensitive deployments. Support roles keep thinking off (#150). Non-Anthropic models, if HEDit gains them (#163), default to off: that is where thinking was slow enough to erase the caching savings. Adds examples/thinking_experiment.py so the measurement can be repeated when the default model changes (#64), the raw results, and docs/reasoning.md. Tested: policy resolution per model, env override and disable values, budget bounds, and one live end-to-end annotation confirming the workflow runs with thinking on and no reasoning text reaches the annotation string.
Experiment results: annotation now thinksSix arms, same 15 benchmark descriptions, only the annotation LLM varying (evaluation and
Findings:
Caveat: one run per arm at n=15, so a case or two of noise, and the 1024/2048 ordering These numbers only exist because the severity fix in this PR came first: with the Added in this PR: the policy ( |
The thinking experiment showed Haiku 4.5 with a 2048-token budget matches Sonnet 5 on first-attempt validity at 2.3x less cost, so calling Sonnet "highest quality" in the UI, CLI, and docs is no longer accurate. Sonnet stays selectable everywhere it was, with the cost difference stated and the web app linking to the measurement. Tested: 555 unit tests pass; ruff clean.
docs/reasoning.md is not on main yet, so a blob/main link 404s until the next release. Switch to blob/develop when it lands on main.
Model presentation follows the measurementDefault stays Claude Haiku 4.5 with a 2048-token thinking budget. Sonnet 5 remains selectable in the web app, the API (
Model ids and request handling are unchanged, so nothing about BYOK or the model allowlist moves. 555 unit tests pass, ruff clean. |
Releases have been described only by the auto-generated commit list. This records 0.7.11 in prose: the Claude Platform on AWS migration, per-role token and prompt-cache accounting on every surface, the 1-hour cache TTL, extended thinking on annotation with its measured effect, the X-Anthropic-* headers, and the validator severity fix.
Summary
Fixes #161 (critical) and adds the reasoning knob from #154 that measuring the thinking
question required.
The validator bug
src/validation/hed_validator.pycompared hedtools' severity against the string"error", but hedtools reports anErrorSeverityenum (ERROR=1,WARNING=10). Thatcomparison is false for every issue ever produced, so every error was filed as a warning,
errorsstayed empty, andis_valid = len(errors) == 0was always True:Beyond the wrong
is_valid, the refinement loop could never fire on this path:validation never failed, so the workflow always returned after the first annotation
attempt. The multi-iteration architecture was inert wherever the Python validator runs -
CLI standalone mode (
use_js_validator=False) and any server without Node orHED_VALIDATOR_PATH. The JavaScript path was unaffected (it reads separate error andwarning arrays).
Now:
An unrecognized severity now counts as an error, so the validator fails closed.
Expect a behavior change: requests whose annotations really are invalid now iterate
instead of returning immediately, so those requests make more LLM calls. That is the loop
doing the job it was built for - visible immediately in a benchmark run, where cases that
previously reported "valid" on attempt 1 now take 2-3 attempts.
max_validation_attemptsstill bounds it.
test_validate_invalid_taghad encoded the bug as expected behavior, assertingis_valid is False or len(warnings) > 0under a note claiming "HED 8.3.0+ reports invalidtags as warnings, not errors". That note described this bug; the test now asserts the
error.
The reasoning knob
A
thinkingparameter oncreate_anthropic_llm, overridingdisable_reasoning, withvalidation matching what the API enforces (each shape verified against the AWS endpoint):
{"type": "enabled", "budget_tokens": N}(N >= 1024, < max_tokens),{"type": "disabled"}{"type": "adaptive"}- no adaptive mode{"type": "adaptive"},{"type": "disabled"}"enabled"- "Use thinking.type adaptive"Enabling thinking also drops
temperature, since the API returns 400 ("temperature mayonly be set to 1 when thinking is enabled").
No default changes here. Whether annotation should think is being measured across six
arms (Haiku with no thinking at temp 0.1 and temp 1.0, 1024- and 2048-token budgets,
Sonnet 5 with thinking off and adaptive) over the project's own 15 benchmark
descriptions, recovered from
examples/model_benchmark.py. Results will be posted beforeany default moves.
Testing
533 tests pass, including new coverage for the severity split (unknown tag, non-base tag,
repeated expression -> errors; tag extension -> warning) and for every accepted and
rejected thinking shape, the temperature drop, and budget bounds.