Skip to content

Release 0.7.11: Claude Platform on AWS, usage and cache reporting, extended thinking - #166

Merged
neuromechanist merged 50 commits into
mainfrom
develop
Aug 21, 2026
Merged

Release 0.7.11: Claude Platform on AWS, usage and cache reporting, extended thinking#166
neuromechanist merged 50 commits into
mainfrom
develop

Conversation

@neuromechanist

@neuromechanist neuromechanist commented Aug 21, 2026

Copy link
Copy Markdown
Member

This is the develop to main release PR for the 0.7.11 line. CHANGELOG.md (added in #165)
carries the full notes; the summary below is what a reviewer should look at.

What ships

Provider migration (#155, #156, #158, #162)
All LLM calls go to the Claude Platform on AWS: the Anthropic-operated Messages API billed
through AWS Marketplace, not Bedrock. Offered models are claude-haiku-4-5 (annotation,
evaluation judge, vision) and claude-sonnet-5; Opus is not offered. OpenRouter, LiteLLM,
and Ollama paths are gone; multi-provider support is tracked in #163. Legacy
OpenRouter-style model ids still resolve as aliases, and the X-OpenRouter-* headers are
still accepted alongside the new X-Anthropic-* spellings, so older clients and cached
frontends keep working.

Usage, cost, and cache reporting (#155, #157, #164)
Per-request and per-role accounting in src/utils/llm_usage.py, surfaced in the CLI, the
usage object on annotation responses, the web app, telemetry events, and a new
GET /metrics endpoint (server key required; BYOK callers get 403 and read their own
numbers from their response). Measured: a cache hit costs $0.004670 against $0.024295
uncached, an 81% saving, with break-even on the second request in the window.
HEDIT_PROMPT_CACHE_TTL=1h suits interactive single-user traffic. Details in
docs/prompt-caching.md.

Extended thinking on annotation (#154, #165)
2048-token budget on Haiku 4.5, adaptive on Sonnet 5, off for the support roles and off by
default for any non-Anthropic model added later. Measured over 15 benchmark descriptions:
first-attempt validity 5/15 to 13/15, average attempts 1.87 to 1.13, LLM calls down a
third, cost up 24%, latency roughly doubled (10.3s to 20.9s). docs/reasoning.md has the
six-arm table and HEDIT_ANNOTATION_THINKING_BUDGET=off reverts it.

Validator severity fix (#161)
hedtools reports severity as an ErrorSeverity IntEnum and it was compared against the
string "error", so every error was recorded as a warning, is_valid was always True, and
the refinement loop could never fire on the Python-validator path (CLI standalone runs and
servers without Node). The JavaScript validator path was unaffected. This is the fix that
made the thinking measurement meaningful: with the gate stuck open, every arm scores a
perfect first-attempt rate.

Deployment notes

  • Server needs ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, and ANTHROPIC_WORKSPACE_ID; the
    endpoint rejects requests without the workspace header. First-party BYOK keys go to
    api.anthropic.com without it.
  • Deploy order for the header change: API, then the Cloudflare worker, then the frontend.
    Both header spellings are accepted server-side, so the order is safe either way.
  • Annotation latency roughly doubles with thinking on. Set
    HEDIT_ANNOTATION_THINKING_BUDGET=off on a latency-sensitive deployment; final validity
    is unchanged either way, since the refinement loop converges.
  • Merging this triggers the auto-release workflow, which bumps main to 0.7.11a1, tags it,
    and publishes to PyPI as a prerelease.

Verification

  • 555 unit tests pass locally and on every PR in this line.
  • The Standalone Tests (PR to main) lane ran a live annotation workflow against the real
    endpoint on this PR and passed, which exercises the severity fix and the thinking default
    end to end.
  • The full live integration suite does not run on pull requests (its condition is a push or
    a manual dispatch), so it was triggered manually on develop for this release; it last ran
    green after Complete Phase 2: prompt-cache measurement, usage reporting, and migration cleanup #164 merged to develop (17 passed, 18 skipped), including the prompt-caching
    write and read checks. It runs again automatically on the push to main.
  • Live end-to-end annotation with thinking on returns a valid annotation on the first
    attempt with no reasoning text in the output.

Still open after this

neuromechanist and others added 30 commits May 21, 2026 11:16
Per CLAUDE.md 'Develop Branch Sync Rule': after each alpha release on
main (0.7.10a2 here), develop bumps the patch and resets to .dev0 so
the two branches share a clean version lineage and dev builds publish
to TestPyPI under the next patch series.

Fast-forwarded merge from main (no divergence: develop had nothing
ahead). All #146 (persistent hed-lsp) and #151 (#148+#150 latency)
work is now on develop.
Server mode reads ANTHROPIC_API_KEY/ANTHROPIC_BASE_URL/ANTHROPIC_WORKSPACE_ID
(workspace header required by the AWS endpoint); BYOK keys go to the
first-party API. Offered models: claude-haiku-4-5 (default) and
claude-sonnet-5. Replaces langchain-openai/litellm deps with
langchain-anthropic. Verified live against both models.
- create_anthropic_workflow/create_byok_workflow/create_vision_agent replace
  the OpenRouter factories; provider routing and user-id cache lanes removed
- BYOK accepts Anthropic keys (sk-ant-) via X-Anthropic-Key; legacy
  X-OpenRouter-Key header still accepted as transport
- Unknown models rejected with 400; server mode no longer needs a per-request
  key (credentials come from the environment)
- Ollama path removed; evaluation judge defaults to Claude Haiku 4.5
- Feedback triage and telemetry defaults updated
- Defaults: claude-haiku-4-5 for annotation, evaluation, and vision;
  claude-sonnet-5 offered via --model
- Provider options removed (single provider now); credentials store
  anthropic_api_key with HEDIT_ANTHROPIC_API_KEY env override
- Standalone mode can run on ANTHROPIC_API_KEY env credentials without a
  stored BYOK key; client sends X-Anthropic-Key
…thropic-Key

Provider plumbing removed from the web UI (single provider now); model
values use first-party ids. Worker BYOK detection and header forwarding
accept the new X-Anthropic-Key header alongside the legacy one.
- New test_anthropic_llm.py unit tests (normalization, server/BYOK modes,
  temperature and thinking gating, caching wrapper)
- test_integration_openrouter.py renamed to test_integration_anthropic.py,
  gated on ANTHROPIC_API_KEY
- Security, CLI, and endpoint tests updated for X-Anthropic-Key and
  first-party model ids; removed tests of deleted OpenRouter modules
- 440 non-integration tests pass (LSP tests fail locally for an unrelated
  hed-lsp version issue)
CI now uses the ANTHROPIC_API_KEY secret plus ANTHROPIC_BASE_URL and
ANTHROPIC_WORKSPACE_ID repository variables (must be configured on GitHub).
Factory raises RuntimeError when ANTHROPIC_API_KEY is unset in server mode;
endpoints map it to 503 while model validation errors stay 400.
- New docs/deployment/claude-platform-aws.md replaces openrouter.md
- Ollama/GPU setup removed from deployment guides; Anthropic credential
  setup and model selection (claude-haiku-4-5 default, claude-sonnet-5
  optional) documented throughout
- BYOK manual tests rewritten for X-Anthropic-Key with sk-ant keys
- 400 mapping for rejected models on all four annotate endpoints; exact
  503 with missing server credentials; alias acceptance over HTTP
- BYOK key extraction via both X-Anthropic-Key and legacy header
- Standalone-mode credential gate (env credentials, no credentials,
  API mode still requires key)
- Replace stale pre-migration telemetry test data; pin the BYOK
  format-rejection test to 401
Critical:
- BYOK LLMs now pin base_url to api.anthropic.com explicitly; ChatAnthropic
  otherwise inherits the server's ANTHROPIC_BASE_URL from the process env,
  routing BYOK keys to the AWS endpoint that rejects them (verified live)
- Anthropic exceptions map to specific HTTP statuses via a shared
  classifier across all four annotate endpoints (401 auth, 403 permission,
  413 context overflow, 400 bad request, 502 connection, 504/429 kept)

Robustness:
- Startup validates credentials with a free count_tokens call; failure
  keeps the server up but marks /health degraded instead of booting a
  healthy-looking server that 500s on every request
- Vision-agent init failure no longer takes down text annotation (503 on
  image endpoints only)
- CachingLLMWrapper raises TypeError on unsupported message types instead
  of silently relabeling them as user turns
- CLI warns once when a legacy OpenRouter key is found in credentials;
  client gains explicit 400/429 error branches
- Frontend streaming handlers surface the backend's error detail instead
  of a bare HTTP status

Cleanup: dead LLM_PROVIDER_PREFERENCE read removed, user_id docstrings no
longer claim telemetry recording, obsolete OpenRouter benchmark examples
deleted, worker forwards X-OpenRouter-Vision-Model, stale docstrings and
comments corrected. All 454 unit tests pass; lifespan integration tests
pass against the live endpoint.
…m-aws

Migrate LLM stack to Anthropic Claude via Claude Platform on AWS
CachingLLMWrapper now reports each response's usage metadata to a new
usage ledger, labeled with the agent role that made the call. The ledger
keeps process totals (for server metrics) and per-request totals via a
contextvar scope that covers LangGraph's concurrent nodes.

Cost is computed from Anthropic list prices with the cache multipliers
(read 0.1x, 5-minute write 1.25x), alongside what the same calls would
have cost uncached, which is what makes a savings figure meaningful.
Unpriced models contribute token counts and are counted separately rather
than silently costing zero.

Tested: 50 unit tests covering the token split (generic and per-TTL cache
keys), cost and savings math, role/model breakdown, scope isolation
including concurrent tasks, and extraction from real AIMessage and
ChatResult objects.
Annotation endpoints (streaming included) now run inside a usage scope, so
every LLM call the workflow makes is attributed to the request that caused
it. The figures land in three places:

- a `usage` field on annotation responses and on the stream's result event,
  which is what lets a caller see the cache savings on their own request
- telemetry events, whose input/output token and cost fields were declared
  but never populated, plus new cache-read/write, hit-rate, call-count, and
  uncached-cost fields
- GET /metrics, reporting server-wide totals by role and by model since
  startup; BYOK callers get 403 since their own numbers come back on their
  annotation response

Request-override headers also gained X-Anthropic-* spellings (model,
eval-model, vision-model, temperature, key) behind one helper, with the
legacy X-OpenRouter-* names still accepted as transport.

Tested: header precedence and both spellings reaching model validation,
/metrics auth and breakdowns, telemetry population from a ledger. 108 tests
in the touched files pass.
The CLI now reports, under the annotation, how many tokens the request
used, how many came from the prompt cache, what it cost, and how much the
cache saved. API mode reads the figures from the response's usage field;
standalone mode collects them locally in a usage scope covering the vision
call and the whole workflow.

Two defects in the same output path are fixed while wiring this up:

- Standalone mode returned only `hed_string`/`description`, while the
  shared text renderer reads the API's field names, so `hedit annotate
  --standalone` printed an empty annotation and no status detail. The
  result is now shaped with both spellings.
- The status line built Rich markup and appended it with Text.append,
  which prints markup literally: users saw "[green][x] Valid[/]". It is
  now rendered as markup, with the ASCII checkboxes escaped so they
  survive.

Tested: 16 tests over line formatting (cache hit, first run, singular
call, unpriced model, sub-cent costs), panel rendering, JSON passthrough,
markup escaping, and the standalone result shape.
Measured against the AWS endpoint with count_tokens: the annotation system
prompt is 21,811 tokens, while evaluation (623), assessment (266), feedback
(241), keyword (186), and the vision prompt (51) sit far below Haiku 4.5's
4096-token minimum cacheable prefix. So annotation is the only role that
can cache; the markers on the others are accepted but never create an
entry, and cost nothing extra. Universal caching in the "every role caches"
sense is not reachable by adding markers, and inflating prompts to reach
the minimum would cost more than it saves.

A real two-request run shows the economics: the first request writes the
prefix at the 1.25x premium ($0.0299 vs $0.0245 uncached), the second reads
it back for $0.0047, 81% below uncached. Break-even is two requests inside
the 5-minute window, so interactive use spaced further apart pays the write
premium repeatedly; cache_ttl (or HEDIT_PROMPT_CACHE_TTL) now accepts "1h"
for that traffic shape, with the 5-minute default unchanged.

Tested: live cache write-then-read and short-prefix-does-not-cache tests
against the AWS endpoint (both pass), cached-prefix byte-stability tests
including the no_extend fork, and the TTL marker/validation unit tests.
The CLI client, frontend, and worker now send the X-Anthropic-* spelling of
the per-request overrides. The API accepts both spellings, and the worker
forwards both, so nothing breaks for cached frontends or third-party
clients still sending X-OpenRouter-*.

Deployment order matters for this change: API first (it already accepts
both), then the worker (it must forward the new names before anything sends
them), then the frontend.

The web UI also gained a "Usage and Cache Savings" section fed by the new
usage field on the response, so the saving is visible where users actually
run annotations.

Tested: CLI client header tests updated to assert the new names and the
absence of the old ones; worker JS syntax checked; renderUsage exercised
with real response payloads (cache-hit and first-run shapes) and with
null/zero-call input.
Adds docs/prompt-caching.md with the measured prompt sizes per role (only
annotation's 21.8k-token prefix clears Haiku 4.5's 4096-token minimum), the
two-request cost comparison showing the write premium and the 81% saving,
the cache-lane rules that keep the prefix stable, the four places the
figures are surfaced, and the live test that verifies the claim.

Also documents HEDIT_PROMPT_CACHE_TTL in .env.example and the deployment
guide, records the X-Anthropic-* header names as primary there, and points
README and the docs index at the new page.
neuromechanist and others added 16 commits August 20, 2026 17:38
Sweeps the leftovers from the migration where the text or code still
described the old stack:

- SECURITY.md documented OpenRouter key handling and an Ollama local-GPU
  privacy story that no longer exists; it now covers the Anthropic
  credentials, BYOK keys, and the fact that every annotation leaves the
  machine.
- .context/agent-architecture.md still credited Qwen-VL for vision and
  OpenRouter with an Ollama fallback; .context/api-and-deployment.md still
  documented provider-routing headers as live.
- .rules/testing.md pointed at OPENROUTER_API_KEY_FOR_TESTING.
- debug_workflow.py imported ChatOllama, was referenced by nothing, and
  could not run; deleted.
- examples/test_examples.py had the same dead import plus a hardcoded
  schema path; ported to the Anthropic factory and now prints the cache
  savings per example. Verified live: 3/3 examples annotate successfully.
- plan.md records the epic outcome and a current telemetry event example;
  the manual BYOK test doc uses the X-Anthropic-* header names.

Remaining OpenRouter mentions are deliberate: legacy header acceptance,
the credentials-file migration warning, tests asserting that back-compat,
and historical benchmark data.
The six failing LSP tests were not a HEDit bug: the local hed-lsp checkout
sits on a feature branch at 0.3.3, 32 commits behind origin/main, and
predates the hed/suggest request handler (added in 0.4.0, commit c5a3f85).
Its out/server.js was also built in February. The server answers
"Unhandled method hed/suggest" with JSON-RPC -32601, which surfaced as six
assertion failures instead of naming the real problem.

Verified by building origin/main (0.4.0) in a scratch worktree and pointing
HED_LSP_SERVER_JS at it: all 13 LSP tests pass, and the full suite is
526 passed / 0 failed. So the client is correct as written.

The fixture's docstring already claimed it skipped when the hed/suggest
endpoint was unavailable, but it never checked. It now detects a bundle
without hed/suggest and skips with the rebuild command, so a stale
dependency reads as a stale dependency.

Also documents why extended thinking is disabled everywhere it can be:
with thinking on, the agents emit far more text and tend to circle in
reasoning loops rather than converging. Where a model refuses to disable
thinking, the factory now requests the lowest reasoning effort instead;
_ALWAYS_THINKING_MODELS is empty today, so the rule is in place for
whichever model needs it first.

Addresses the CodeQL unreachable-code alert on tests/test_llm_usage.py by
raising inside a helper, so the control flow is visible to the scanner.
The previous wording claimed every role disables thinking. Annotation and
vision do not: they take the model default, which is no thinking on Haiku
4.5 and adaptive thinking on Sonnet 5. Records the per-provider intent
(on for Anthropic, off for others, where thinking was slow enough to erase
the caching savings) and marks the annotation budget as an open measurement
rather than a settled choice.
…al-caching-observability

Complete Phase 2: prompt-cache measurement, usage reporting, and migration cleanup
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.
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.
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.
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.
…-161

Fix validator severity mapping (#161) and add explicit reasoning control (#154)
Comment thread src/api/main.py Fixed
Comment thread src/api/main.py Fixed
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

@neuromechanist

Copy link
Copy Markdown
Member Author

Live verification before merge

The full integration suite does not run on pull requests (its condition is a push or a manual dispatch), so it was dispatched on develop at this commit: run 32457792491.

17 passed, 18 skipped, 566 deselected in 64.40s

Including against the real endpoint:

  • TestPromptCachingIntegration::test_cache_write_then_read_is_recorded
  • TestPromptCachingIntegration::test_short_prefix_does_not_cache
  • TestAPIEndpointIntegration::test_annotate_endpoint

The Standalone Tests (PR to main) lane on this PR also ran a live annotation workflow (test_simple_annotation_workflow, 23s), which exercises the severity fix and the thinking default end to end.

The 18 skips are the tests gated on credentials this runner does not carry (BYOK first-party keys).

…exposure)

The bad_request branch of _describe_llm_error forwarded str(exc)[:200] to the
client, which CodeQL flagged on both streaming endpoints. The message is now
a fixed string like every other branch. Nothing is lost operationally: all
four call sites already log the exception, so the provider's own wording
stays in the server log.

Adds tests asserting no branch leaks exception text, and that context
overflow still takes precedence over bad_request.

Tested: 559 unit tests pass; ruff clean.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 21, 2026

Copy link
Copy Markdown

Deploying hedit with  Cloudflare Pages  Cloudflare Pages

Latest commit: 162c87f
Status: ✅  Deploy successful!
Preview URL: https://33ec7cf9.hedit.pages.dev
Branch Preview URL: https://develop.hedit.pages.dev

View logs

@neuromechanist
neuromechanist merged commit f25c1b5 into main Aug 21, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants