hedit 0.7.11.dev5__tar.gz → 0.7.11.dev7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hedit-0.7.11.dev5/hedit.egg-info → hedit-0.7.11.dev7}/PKG-INFO +1 -1
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/README.md +2 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7/hedit.egg-info}/PKG-INFO +1 -1
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/pyproject.toml +1 -1
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/api/main.py +21 -5
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/cli/config.py +3 -2
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/cli/local_executor.py +5 -2
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/cli/main.py +1 -1
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/utils/anthropic_llm.py +135 -6
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/validation/hed_validator.py +9 -2
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/version.py +1 -1
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_anthropic_llm.py +144 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_api_endpoints.py +81 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_validation.py +55 -6
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/LICENSE +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/PKG_README.md +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/hedit.egg-info/SOURCES.txt +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/hedit.egg-info/dependency_links.txt +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/hedit.egg-info/entry_points.txt +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/hedit.egg-info/requires.txt +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/hedit.egg-info/top_level.txt +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/setup.cfg +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/__init__.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/agents/__init__.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/agents/annotation_agent.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/agents/assessment_agent.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/agents/evaluation_agent.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/agents/feedback_summarizer.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/agents/feedback_triage_agent.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/agents/state.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/agents/validation_agent.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/agents/vision_agent.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/agents/workflow.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/api/__init__.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/api/models.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/api/security.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/cli/__init__.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/cli/api_executor.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/cli/client.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/cli/commands/__init__.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/cli/commands/lsp.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/cli/executor.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/cli/output.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/data/__init__.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/data/hed-docs/02_Terminology.md +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/data/hed-docs/HedAnnotationSemantics.md +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/data/hed-docs/manifest.json +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/lsp/__init__.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/lsp/client.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/lsp/daemon.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/lsp/protocol.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/scripts/__init__.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/scripts/process_feedback.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/telemetry/__init__.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/telemetry/collector.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/telemetry/schema.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/telemetry/storage.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/utils/__init__.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/utils/error_remediation.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/utils/github_client.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/utils/hed_comprehensive_guide.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/utils/hed_docs_loader.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/utils/image_processing.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/utils/json_schema_loader.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/utils/llm_usage.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/utils/schema_loader.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/validation/__init__.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/src/validation/hed_lsp.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_annotation_agent.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_cli_client.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_cli_config.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_cli_integration.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_cli_main.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_cli_usage_report.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_comprehensive_guide.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_error_remediation.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_feedback_integration.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_feedback_triage.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_fetch_hed_docs.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_github_client.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_hed_docs_loader.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_integration_anthropic.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_json_schema_loader.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_llm_usage.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_no_extend_propagation.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_schema_loader.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_security.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_state.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_telemetry.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_validation_agent.py +0 -0
- {hedit-0.7.11.dev5 → hedit-0.7.11.dev7}/tests/test_version.py +0 -0
|
@@ -84,7 +84,9 @@ The agents work in feedback loops, automatically refining the annotation until i
|
|
|
84
84
|
|
|
85
85
|
## Documentation
|
|
86
86
|
|
|
87
|
+
- [Changelog](CHANGELOG.md) - What changed in each release
|
|
87
88
|
- [Prompt Caching and Usage Reporting](docs/prompt-caching.md) - What HEDit caches, what it saves, and where to see the numbers
|
|
89
|
+
- [Extended Thinking](docs/reasoning.md) - Measured effect of reasoning per agent role, and how to tune it
|
|
88
90
|
- [HED Standard](https://hedtags.org) - Learn about HED annotations
|
|
89
91
|
- [GitHub Issues](https://github.com/Annotation-Garden/HEDit/issues) - Report bugs or request features
|
|
90
92
|
|
|
@@ -40,7 +40,12 @@ from src.api.models import (
|
|
|
40
40
|
from src.api.security import api_key_auth, audit_logger
|
|
41
41
|
from src.lsp import HedLspClient
|
|
42
42
|
from src.telemetry import LocalFileStorage, TelemetryCollector, TelemetryEvent
|
|
43
|
-
from src.utils.anthropic_llm import
|
|
43
|
+
from src.utils.anthropic_llm import (
|
|
44
|
+
DEFAULT_MODEL,
|
|
45
|
+
annotation_thinking,
|
|
46
|
+
create_anthropic_llm,
|
|
47
|
+
normalize_model,
|
|
48
|
+
)
|
|
44
49
|
from src.utils.llm_usage import UsageLedger, process_ledger, usage_scope
|
|
45
50
|
from src.utils.schema_loader import HedSchemaLoader
|
|
46
51
|
from src.validation.hed_validator import HedPythonValidator
|
|
@@ -77,6 +82,11 @@ def _describe_llm_error(exc: Exception) -> tuple[int, str, str]:
|
|
|
77
82
|
key gets a 401 pointing at their key, not a generic 500. Order matters --
|
|
78
83
|
AnthropicContextOverflowError subclasses BadRequestError, and the timeout
|
|
79
84
|
and rate-limit types must be checked before their parent classes.
|
|
85
|
+
|
|
86
|
+
Every message here is a fixed string. Exception text never reaches the
|
|
87
|
+
client, because a provider error can carry request details and the web app
|
|
88
|
+
renders the message as HTML. Callers log the exception, so the provider's
|
|
89
|
+
own wording stays available in the server log for whoever debugs it.
|
|
80
90
|
"""
|
|
81
91
|
if isinstance(exc, APITimeoutError):
|
|
82
92
|
return 504, "timeout", "LLM request timed out. Try again or use a faster model."
|
|
@@ -93,7 +103,11 @@ def _describe_llm_error(exc: Exception) -> tuple[int, str, str]:
|
|
|
93
103
|
if isinstance(exc, AnthropicContextOverflowError):
|
|
94
104
|
return 413, "context_overflow", "The input is too long for this model. Try shortening it."
|
|
95
105
|
if isinstance(exc, anthropic.BadRequestError):
|
|
96
|
-
return
|
|
106
|
+
return (
|
|
107
|
+
400,
|
|
108
|
+
"bad_request",
|
|
109
|
+
"The LLM rejected the request. Check the model and request parameters, then try again.",
|
|
110
|
+
)
|
|
97
111
|
if isinstance(exc, anthropic.APIConnectionError):
|
|
98
112
|
return 502, "upstream_unreachable", "Could not reach the LLM service. Please try again."
|
|
99
113
|
return 500, "internal", "An error occurred during annotation processing."
|
|
@@ -200,13 +214,15 @@ def create_anthropic_workflow(
|
|
|
200
214
|
normalize_model(actual_eval_model)
|
|
201
215
|
|
|
202
216
|
# Create LLMs.
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
217
|
+
# Annotation thinks: measured over the benchmark descriptions, a 2048-token
|
|
218
|
+
# budget took first-attempt validity from 5/15 to 13/15 and cut total LLM
|
|
219
|
+
# calls by a third, for 24% more cost and about twice the latency. See
|
|
220
|
+
# annotation_thinking() and docs/prompt-caching.md.
|
|
206
221
|
annotation_llm = create_anthropic_llm(
|
|
207
222
|
model=actual_annotation_model,
|
|
208
223
|
api_key=api_key,
|
|
209
224
|
temperature=actual_temperature,
|
|
225
|
+
thinking=annotation_thinking(actual_annotation_model),
|
|
210
226
|
role="annotation",
|
|
211
227
|
)
|
|
212
228
|
# Evaluation / assessment / feedback / keyword extraction are short
|
|
@@ -34,8 +34,9 @@ DEFAULT_API_URL = "https://api.annotation.garden/hedit"
|
|
|
34
34
|
DEFAULT_DEV_API_URL = "https://api.annotation.garden/hedit-dev"
|
|
35
35
|
|
|
36
36
|
# Default models (Anthropic Claude only since the 2026-08-18 migration).
|
|
37
|
-
# Annotation model: Claude Haiku 4.5
|
|
38
|
-
#
|
|
37
|
+
# Annotation model: Claude Haiku 4.5, which runs with extended thinking and
|
|
38
|
+
# matched Sonnet 5's first-attempt validity for 2.3x less cost (docs/reasoning.md).
|
|
39
|
+
# Claude Sonnet 5 ("claude-sonnet-5") stays selectable for comparison.
|
|
39
40
|
DEFAULT_MODEL = "claude-haiku-4-5"
|
|
40
41
|
|
|
41
42
|
# Evaluation judge stays on Haiku regardless of the annotation model.
|
|
@@ -150,13 +150,16 @@ class LocalExecutionBackend(ExecutionBackend):
|
|
|
150
150
|
self._ensure_api_key()
|
|
151
151
|
|
|
152
152
|
from src.agents.workflow import HedAnnotationWorkflow
|
|
153
|
-
from src.utils.anthropic_llm import create_anthropic_llm
|
|
153
|
+
from src.utils.anthropic_llm import annotation_thinking, create_anthropic_llm
|
|
154
154
|
|
|
155
|
-
# Annotation
|
|
155
|
+
# Annotation thinks; see annotation_thinking() for the measurement
|
|
156
|
+
# behind the budget and HEDIT_ANNOTATION_THINKING_BUDGET to change
|
|
157
|
+
# or disable it.
|
|
156
158
|
annotation_llm = create_anthropic_llm(
|
|
157
159
|
model=self._model,
|
|
158
160
|
api_key=self._api_key,
|
|
159
161
|
temperature=self._temperature,
|
|
162
|
+
thinking=annotation_thinking(self._model),
|
|
160
163
|
role="annotation",
|
|
161
164
|
)
|
|
162
165
|
|
|
@@ -41,8 +41,8 @@ DEFAULT_MODEL = "claude-haiku-4-5"
|
|
|
41
41
|
# Models offered to users (web UI dropdown / CLI). Opus is deliberately
|
|
42
42
|
# not offered.
|
|
43
43
|
ALLOWED_MODELS = {
|
|
44
|
-
"claude-haiku-4-5": "Claude Haiku 4.5 (
|
|
45
|
-
"claude-sonnet-5": "Claude Sonnet 5 (
|
|
44
|
+
"claude-haiku-4-5": "Claude Haiku 4.5 (default, recommended)",
|
|
45
|
+
"claude-sonnet-5": "Claude Sonnet 5 (larger, 2.3x the cost)",
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
# Legacy identifiers from older clients, saved CLI configs, and cached
|
|
@@ -78,6 +78,20 @@ _ADAPTIVE_THINKING_MODELS = {"claude-sonnet-5"}
|
|
|
78
78
|
_ALWAYS_THINKING_MODELS: set[str] = set()
|
|
79
79
|
LOWEST_REASONING_EFFORT = "low"
|
|
80
80
|
|
|
81
|
+
# Smallest thinking budget the API accepts on budget-style models.
|
|
82
|
+
MIN_THINKING_BUDGET_TOKENS = 1024
|
|
83
|
+
|
|
84
|
+
# Thinking budget for the annotation role on budget-style models. Measured
|
|
85
|
+
# over the 15 benchmark descriptions (2026-08-20, Haiku 4.5): first-attempt
|
|
86
|
+
# validity went 5/15 without thinking to 11/15 at 1024 tokens and 13/15 at
|
|
87
|
+
# 2048, average attempts 1.87 -> 1.13, and total LLM calls 71 -> 49. Cost per
|
|
88
|
+
# request rose 24% ($0.0092 -> $0.0114) while 2048 came out cheaper than 1024
|
|
89
|
+
# ($0.0129), because the larger budget removed more refinement rounds than it
|
|
90
|
+
# added in thinking tokens. Latency roughly doubled (10.3s -> 20.9s), which is
|
|
91
|
+
# the real price; HEDIT_ANNOTATION_THINKING_BUDGET=0 turns it off for
|
|
92
|
+
# latency-sensitive deployments.
|
|
93
|
+
DEFAULT_ANNOTATION_THINKING_BUDGET = 2048
|
|
94
|
+
|
|
81
95
|
# Models that still accept sampling parameters. Sonnet 5 rejects
|
|
82
96
|
# `temperature` with a 400 (sampling params are removed on Claude 5 models).
|
|
83
97
|
_SAMPLING_MODELS = {"claude-haiku-4-5"}
|
|
@@ -114,6 +128,101 @@ def normalize_model(model: str | None) -> str:
|
|
|
114
128
|
return resolved
|
|
115
129
|
|
|
116
130
|
|
|
131
|
+
def _validate_thinking(thinking: dict[str, Any], model: str, max_tokens: int) -> None:
|
|
132
|
+
"""Check a thinking configuration against what the model accepts.
|
|
133
|
+
|
|
134
|
+
The API enforces different shapes per generation, and a mismatch is a
|
|
135
|
+
400 at request time: Sonnet 5 rejects thinking.type "enabled" ("Use
|
|
136
|
+
thinking.type adaptive"), while Haiku 4.5 has no adaptive mode and needs
|
|
137
|
+
an explicit token budget.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
thinking: Thinking configuration to check
|
|
141
|
+
model: Resolved first-party model id
|
|
142
|
+
max_tokens: Maximum tokens for the request
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: If the configuration is not valid for this model
|
|
146
|
+
"""
|
|
147
|
+
kind = thinking.get("type")
|
|
148
|
+
|
|
149
|
+
if model in _ADAPTIVE_THINKING_MODELS:
|
|
150
|
+
if kind not in ("adaptive", "disabled"):
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"{model} accepts thinking type 'adaptive' or 'disabled', not {kind!r}; "
|
|
153
|
+
"budget_tokens was removed on this model generation"
|
|
154
|
+
)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
if kind == "disabled":
|
|
158
|
+
# Accepted (verified against the endpoint) and redundant on these
|
|
159
|
+
# models, but it lets a caller express "off" the same way for every
|
|
160
|
+
# model instead of special-casing.
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
if kind != "enabled":
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f"{model} has no adaptive thinking mode; enable it with "
|
|
166
|
+
'{"type": "enabled", "budget_tokens": N}, disable it with '
|
|
167
|
+
'{"type": "disabled"}, or leave thinking unset'
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
budget = thinking.get("budget_tokens")
|
|
171
|
+
if not isinstance(budget, int) or budget < MIN_THINKING_BUDGET_TOKENS:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"budget_tokens must be an integer of at least "
|
|
174
|
+
f"{MIN_THINKING_BUDGET_TOKENS}, got {budget!r}"
|
|
175
|
+
)
|
|
176
|
+
if budget >= max_tokens:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"budget_tokens ({budget}) must be below max_tokens ({max_tokens}); "
|
|
179
|
+
"thinking tokens are drawn from the same budget as the response"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def annotation_thinking(model: str | None = None) -> dict[str, Any] | None:
|
|
184
|
+
"""Thinking configuration for the annotation role.
|
|
185
|
+
|
|
186
|
+
Reasoning measurably improves first-attempt validity on the annotation
|
|
187
|
+
agent, which is worth paying for because every failed attempt costs
|
|
188
|
+
another full annotate/validate/evaluate round. The support roles are
|
|
189
|
+
unaffected: reasoning there added latency without quality (#150).
|
|
190
|
+
|
|
191
|
+
Set HEDIT_ANNOTATION_THINKING_BUDGET to a token count to change the
|
|
192
|
+
budget, or to 0 (or "off") to disable thinking entirely.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
model: Model the annotation LLM will use (default model if None)
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
A thinking configuration for the model, or None when disabled
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
ValueError: If the environment override is not an integer or "off"
|
|
202
|
+
"""
|
|
203
|
+
resolved_model = normalize_model(model)
|
|
204
|
+
|
|
205
|
+
raw = os.getenv("HEDIT_ANNOTATION_THINKING_BUDGET")
|
|
206
|
+
if raw is not None and raw.strip().lower() in ("0", "off", "false", "none"):
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
if resolved_model in _ADAPTIVE_THINKING_MODELS:
|
|
210
|
+
# Adaptive is the only on-mode on this generation; the model decides
|
|
211
|
+
# how much to think, so the budget value does not apply.
|
|
212
|
+
return {"type": "adaptive"}
|
|
213
|
+
|
|
214
|
+
budget = DEFAULT_ANNOTATION_THINKING_BUDGET
|
|
215
|
+
if raw is not None:
|
|
216
|
+
try:
|
|
217
|
+
budget = int(raw)
|
|
218
|
+
except ValueError as exc:
|
|
219
|
+
raise ValueError(
|
|
220
|
+
f"HEDIT_ANNOTATION_THINKING_BUDGET must be an integer or 'off', got {raw!r}"
|
|
221
|
+
) from exc
|
|
222
|
+
|
|
223
|
+
return {"type": "enabled", "budget_tokens": budget}
|
|
224
|
+
|
|
225
|
+
|
|
117
226
|
def create_anthropic_llm(
|
|
118
227
|
model: str | None = None,
|
|
119
228
|
api_key: str | None = None,
|
|
@@ -124,6 +233,7 @@ def create_anthropic_llm(
|
|
|
124
233
|
timeout: float = 60.0,
|
|
125
234
|
role: str = "unspecified",
|
|
126
235
|
cache_ttl: str | None = None,
|
|
236
|
+
thinking: dict[str, Any] | None = None,
|
|
127
237
|
) -> BaseChatModel:
|
|
128
238
|
"""Create a Claude LLM instance with prompt caching.
|
|
129
239
|
|
|
@@ -151,12 +261,21 @@ def create_anthropic_llm(
|
|
|
151
261
|
token and cache usage; has no effect on the request itself.
|
|
152
262
|
cache_ttl: Prompt-cache lifetime, "5m" (default) or "1h". Falls back
|
|
153
263
|
to the HEDIT_PROMPT_CACHE_TTL environment variable when None.
|
|
264
|
+
thinking: Explicit extended-thinking configuration, overriding
|
|
265
|
+
disable_reasoning. The accepted shape depends on the model, and
|
|
266
|
+
the API is strict about it: Sonnet 5 takes
|
|
267
|
+
{"type": "adaptive"} or {"type": "disabled"} and rejects
|
|
268
|
+
"enabled"; Haiku 4.5 takes
|
|
269
|
+
{"type": "enabled", "budget_tokens": N} with N at least 1024 and
|
|
270
|
+
below max_tokens. Enabling thinking also drops `temperature`,
|
|
271
|
+
since the API only allows temperature 1 alongside thinking.
|
|
154
272
|
|
|
155
273
|
Returns:
|
|
156
274
|
LLM instance configured for the Claude Messages API
|
|
157
275
|
|
|
158
276
|
Raises:
|
|
159
|
-
ValueError: If the model is not offered
|
|
277
|
+
ValueError: If the model is not offered, the TTL is not supported, or
|
|
278
|
+
the thinking configuration is not valid for the model
|
|
160
279
|
RuntimeError: If server mode is used without ANTHROPIC_API_KEY set
|
|
161
280
|
"""
|
|
162
281
|
from langchain_anthropic import ChatAnthropic
|
|
@@ -188,10 +307,20 @@ def create_anthropic_llm(
|
|
|
188
307
|
if workspace_id:
|
|
189
308
|
kwargs["default_headers"] = {"anthropic-workspace-id": workspace_id}
|
|
190
309
|
|
|
191
|
-
|
|
310
|
+
resolved_max_tokens = max_tokens or 8000
|
|
311
|
+
|
|
312
|
+
if thinking is not None:
|
|
313
|
+
_validate_thinking(thinking, resolved_model, resolved_max_tokens)
|
|
314
|
+
kwargs["thinking"] = thinking
|
|
315
|
+
|
|
316
|
+
thinking_on = thinking is not None and thinking.get("type") != "disabled"
|
|
317
|
+
|
|
318
|
+
# The API rejects any temperature but 1 when thinking is enabled, so the
|
|
319
|
+
# sampling setting is dropped rather than silently causing a 400.
|
|
320
|
+
if resolved_model in _SAMPLING_MODELS and not thinking_on:
|
|
192
321
|
kwargs["temperature"] = temperature
|
|
193
322
|
|
|
194
|
-
if disable_reasoning:
|
|
323
|
+
if disable_reasoning and thinking is None:
|
|
195
324
|
if resolved_model in _ADAPTIVE_THINKING_MODELS:
|
|
196
325
|
kwargs["thinking"] = {"type": "disabled"}
|
|
197
326
|
elif resolved_model in _ALWAYS_THINKING_MODELS:
|
|
@@ -199,7 +328,7 @@ def create_anthropic_llm(
|
|
|
199
328
|
|
|
200
329
|
llm = ChatAnthropic(
|
|
201
330
|
model=resolved_model,
|
|
202
|
-
max_tokens=
|
|
331
|
+
max_tokens=resolved_max_tokens,
|
|
203
332
|
timeout=timeout,
|
|
204
333
|
**kwargs,
|
|
205
334
|
)
|
|
@@ -16,7 +16,7 @@ from pathlib import Path
|
|
|
16
16
|
from typing import Literal
|
|
17
17
|
|
|
18
18
|
from hed import HedString
|
|
19
|
-
from hed.errors import get_printable_issue_string
|
|
19
|
+
from hed.errors import ErrorSeverity, get_printable_issue_string
|
|
20
20
|
from hed.schema import HedSchema, load_schema_version
|
|
21
21
|
from hed.validator import HedValidator
|
|
22
22
|
|
|
@@ -167,8 +167,15 @@ class HedPythonValidator:
|
|
|
167
167
|
# Process issues
|
|
168
168
|
for issue in issues:
|
|
169
169
|
issue_str = get_printable_issue_string([issue])
|
|
170
|
+
# hedtools reports severity as an ErrorSeverity enum
|
|
171
|
+
# (ERROR=1, WARNING=10), never the string "error". Comparing
|
|
172
|
+
# it to "error" was always False, so every error was filed as
|
|
173
|
+
# a warning and is_valid was always True: "NotARealTag/Foo"
|
|
174
|
+
# validated clean and the workflow's refinement loop could
|
|
175
|
+
# never fire (#161). Anything not explicitly a warning counts
|
|
176
|
+
# as an error, so an unrecognized severity fails closed.
|
|
170
177
|
severity: Literal["error", "warning"] = (
|
|
171
|
-
"
|
|
178
|
+
"warning" if issue.get("severity") == ErrorSeverity.WARNING else "error"
|
|
172
179
|
)
|
|
173
180
|
|
|
174
181
|
validation_issue = ValidationIssue(
|
|
@@ -7,8 +7,10 @@ from langchain_core.outputs import ChatGeneration, ChatResult
|
|
|
7
7
|
from src.utils import anthropic_llm
|
|
8
8
|
from src.utils.anthropic_llm import (
|
|
9
9
|
ALLOWED_MODELS,
|
|
10
|
+
DEFAULT_ANNOTATION_THINKING_BUDGET,
|
|
10
11
|
DEFAULT_MODEL,
|
|
11
12
|
CachingLLMWrapper,
|
|
13
|
+
annotation_thinking,
|
|
12
14
|
create_anthropic_llm,
|
|
13
15
|
normalize_model,
|
|
14
16
|
)
|
|
@@ -305,3 +307,145 @@ class TestCacheTtl:
|
|
|
305
307
|
monkeypatch.setenv("HEDIT_PROMPT_CACHE_TTL", "forever")
|
|
306
308
|
with pytest.raises(ValueError, match="Unsupported prompt cache TTL"):
|
|
307
309
|
create_anthropic_llm()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TestThinkingConfiguration:
|
|
313
|
+
"""Tests for the explicit thinking knob.
|
|
314
|
+
|
|
315
|
+
The shapes asserted here are the ones the API actually enforces, checked
|
|
316
|
+
against the live endpoint: Haiku 4.5 needs an explicit budget and refuses
|
|
317
|
+
any temperature but 1 alongside thinking; Sonnet 5 rejects
|
|
318
|
+
thinking.type "enabled" and wants "adaptive".
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
@pytest.fixture(autouse=True)
|
|
322
|
+
def server_key(self, monkeypatch):
|
|
323
|
+
monkeypatch.setenv("ANTHROPIC_API_KEY", "server-key")
|
|
324
|
+
|
|
325
|
+
def test_haiku_takes_a_budget(self):
|
|
326
|
+
llm = create_anthropic_llm(
|
|
327
|
+
model="claude-haiku-4-5",
|
|
328
|
+
thinking={"type": "enabled", "budget_tokens": 1024},
|
|
329
|
+
enable_caching=False,
|
|
330
|
+
)
|
|
331
|
+
assert llm.thinking == {"type": "enabled", "budget_tokens": 1024}
|
|
332
|
+
|
|
333
|
+
def test_temperature_is_dropped_when_thinking_is_on(self):
|
|
334
|
+
"""The API allows only temperature 1 with thinking, so it is omitted."""
|
|
335
|
+
llm = create_anthropic_llm(
|
|
336
|
+
model="claude-haiku-4-5",
|
|
337
|
+
temperature=0.1,
|
|
338
|
+
thinking={"type": "enabled", "budget_tokens": 1024},
|
|
339
|
+
enable_caching=False,
|
|
340
|
+
)
|
|
341
|
+
assert llm.temperature is None
|
|
342
|
+
|
|
343
|
+
def test_temperature_survives_thinking_disabled(self):
|
|
344
|
+
llm = create_anthropic_llm(
|
|
345
|
+
model="claude-haiku-4-5",
|
|
346
|
+
temperature=0.1,
|
|
347
|
+
thinking={"type": "disabled"},
|
|
348
|
+
enable_caching=False,
|
|
349
|
+
)
|
|
350
|
+
assert llm.temperature == 0.1
|
|
351
|
+
|
|
352
|
+
def test_sonnet_takes_adaptive(self):
|
|
353
|
+
llm = create_anthropic_llm(
|
|
354
|
+
model="claude-sonnet-5", thinking={"type": "adaptive"}, enable_caching=False
|
|
355
|
+
)
|
|
356
|
+
assert llm.thinking == {"type": "adaptive"}
|
|
357
|
+
|
|
358
|
+
def test_sonnet_rejects_budget_style_thinking(self):
|
|
359
|
+
with pytest.raises(ValueError, match="adaptive"):
|
|
360
|
+
create_anthropic_llm(
|
|
361
|
+
model="claude-sonnet-5",
|
|
362
|
+
thinking={"type": "enabled", "budget_tokens": 1024},
|
|
363
|
+
enable_caching=False,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def test_haiku_rejects_adaptive(self):
|
|
367
|
+
with pytest.raises(ValueError, match="no adaptive thinking mode"):
|
|
368
|
+
create_anthropic_llm(
|
|
369
|
+
model="claude-haiku-4-5", thinking={"type": "adaptive"}, enable_caching=False
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def test_budget_below_minimum_rejected(self):
|
|
373
|
+
with pytest.raises(ValueError, match="at least 1024"):
|
|
374
|
+
create_anthropic_llm(
|
|
375
|
+
model="claude-haiku-4-5",
|
|
376
|
+
thinking={"type": "enabled", "budget_tokens": 512},
|
|
377
|
+
enable_caching=False,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def test_budget_must_fit_under_max_tokens(self):
|
|
381
|
+
with pytest.raises(ValueError, match="must be below max_tokens"):
|
|
382
|
+
create_anthropic_llm(
|
|
383
|
+
model="claude-haiku-4-5",
|
|
384
|
+
max_tokens=2000,
|
|
385
|
+
thinking={"type": "enabled", "budget_tokens": 2000},
|
|
386
|
+
enable_caching=False,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def test_explicit_thinking_overrides_disable_reasoning(self):
|
|
390
|
+
llm = create_anthropic_llm(
|
|
391
|
+
model="claude-sonnet-5",
|
|
392
|
+
disable_reasoning=True,
|
|
393
|
+
thinking={"type": "adaptive"},
|
|
394
|
+
enable_caching=False,
|
|
395
|
+
)
|
|
396
|
+
assert llm.thinking == {"type": "adaptive"}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class TestAnnotationThinking:
|
|
400
|
+
"""Tests for the annotation role's thinking policy."""
|
|
401
|
+
|
|
402
|
+
@pytest.fixture(autouse=True)
|
|
403
|
+
def clean_env(self, monkeypatch):
|
|
404
|
+
monkeypatch.setenv("ANTHROPIC_API_KEY", "server-key")
|
|
405
|
+
monkeypatch.delenv("HEDIT_ANNOTATION_THINKING_BUDGET", raising=False)
|
|
406
|
+
|
|
407
|
+
def test_default_budget_on_haiku(self):
|
|
408
|
+
assert annotation_thinking("claude-haiku-4-5") == {
|
|
409
|
+
"type": "enabled",
|
|
410
|
+
"budget_tokens": DEFAULT_ANNOTATION_THINKING_BUDGET,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
def test_default_model_gets_a_budget(self):
|
|
414
|
+
assert annotation_thinking()["type"] == "enabled"
|
|
415
|
+
|
|
416
|
+
def test_adaptive_model_ignores_the_budget(self):
|
|
417
|
+
"""Sonnet 5 decides its own depth; budget_tokens is rejected there."""
|
|
418
|
+
assert annotation_thinking("claude-sonnet-5") == {"type": "adaptive"}
|
|
419
|
+
|
|
420
|
+
def test_env_override_sets_the_budget(self, monkeypatch):
|
|
421
|
+
monkeypatch.setenv("HEDIT_ANNOTATION_THINKING_BUDGET", "4096")
|
|
422
|
+
assert annotation_thinking("claude-haiku-4-5")["budget_tokens"] == 4096
|
|
423
|
+
|
|
424
|
+
def test_env_can_disable_thinking(self, monkeypatch):
|
|
425
|
+
for value in ("0", "off", "false", "none", "OFF"):
|
|
426
|
+
monkeypatch.setenv("HEDIT_ANNOTATION_THINKING_BUDGET", value)
|
|
427
|
+
assert annotation_thinking("claude-haiku-4-5") is None
|
|
428
|
+
assert annotation_thinking("claude-sonnet-5") is None
|
|
429
|
+
|
|
430
|
+
def test_bad_env_value_is_rejected(self, monkeypatch):
|
|
431
|
+
monkeypatch.setenv("HEDIT_ANNOTATION_THINKING_BUDGET", "lots")
|
|
432
|
+
with pytest.raises(ValueError, match="must be an integer or 'off'"):
|
|
433
|
+
annotation_thinking("claude-haiku-4-5")
|
|
434
|
+
|
|
435
|
+
def test_legacy_model_id_is_normalized(self):
|
|
436
|
+
assert annotation_thinking("anthropic/claude-haiku-4.5")["type"] == "enabled"
|
|
437
|
+
|
|
438
|
+
def test_policy_produces_a_usable_llm(self):
|
|
439
|
+
"""The default policy must satisfy the factory's own validation."""
|
|
440
|
+
llm = create_anthropic_llm(
|
|
441
|
+
model="claude-haiku-4-5",
|
|
442
|
+
thinking=annotation_thinking("claude-haiku-4-5"),
|
|
443
|
+
enable_caching=False,
|
|
444
|
+
)
|
|
445
|
+
assert llm.thinking["budget_tokens"] == DEFAULT_ANNOTATION_THINKING_BUDGET
|
|
446
|
+
# Thinking forces temperature off, which the API requires.
|
|
447
|
+
assert llm.temperature is None
|
|
448
|
+
|
|
449
|
+
def test_budget_fits_under_the_default_max_tokens(self):
|
|
450
|
+
"""A budget above max_tokens would 400 at request time."""
|
|
451
|
+
assert DEFAULT_ANNOTATION_THINKING_BUDGET < 8000
|
|
@@ -1469,3 +1469,84 @@ class TestMetricsEndpoint:
|
|
|
1469
1469
|
)
|
|
1470
1470
|
assert response.status_code == 403
|
|
1471
1471
|
assert "server API key" in response.json()["detail"]
|
|
1472
|
+
|
|
1473
|
+
|
|
1474
|
+
class TestLLMErrorMessages:
|
|
1475
|
+
"""Error responses classify the failure without quoting the provider.
|
|
1476
|
+
|
|
1477
|
+
CodeQL flagged the streaming error path (py/stack-trace-exposure) because
|
|
1478
|
+
exception text reached the client. The web app renders these messages as
|
|
1479
|
+
HTML, and a provider error can echo request details, so every branch has
|
|
1480
|
+
to return a fixed string.
|
|
1481
|
+
"""
|
|
1482
|
+
|
|
1483
|
+
@staticmethod
|
|
1484
|
+
def _response(status: int):
|
|
1485
|
+
import httpx
|
|
1486
|
+
|
|
1487
|
+
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
|
|
1488
|
+
return httpx.Response(status, request=request)
|
|
1489
|
+
|
|
1490
|
+
def test_bad_request_does_not_quote_the_provider(self):
|
|
1491
|
+
import anthropic
|
|
1492
|
+
|
|
1493
|
+
from src.api.main import _describe_llm_error
|
|
1494
|
+
|
|
1495
|
+
detail = (
|
|
1496
|
+
"Error code: 400 - thinking.type.enabled is not supported for this model; "
|
|
1497
|
+
"workspace wrkspc_01Rayuq1JvjaMYusRvUsn6vk"
|
|
1498
|
+
)
|
|
1499
|
+
exc = anthropic.BadRequestError(detail, response=self._response(400), body=None)
|
|
1500
|
+
|
|
1501
|
+
status, error_type, message = _describe_llm_error(exc)
|
|
1502
|
+
|
|
1503
|
+
assert status == 400
|
|
1504
|
+
assert error_type == "bad_request"
|
|
1505
|
+
assert "wrkspc_01Rayuq1JvjaMYusRvUsn6vk" not in message
|
|
1506
|
+
assert "thinking.type.enabled" not in message
|
|
1507
|
+
assert "Error code" not in message
|
|
1508
|
+
|
|
1509
|
+
def test_context_overflow_still_precedes_bad_request(self):
|
|
1510
|
+
from langchain_anthropic.chat_models import AnthropicContextOverflowError
|
|
1511
|
+
|
|
1512
|
+
from src.api.main import _describe_llm_error
|
|
1513
|
+
|
|
1514
|
+
exc = AnthropicContextOverflowError(
|
|
1515
|
+
"prompt is too long: 250000 tokens > 200000 maximum",
|
|
1516
|
+
response=self._response(400),
|
|
1517
|
+
body=None,
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
status, error_type, _message = _describe_llm_error(exc)
|
|
1521
|
+
|
|
1522
|
+
assert status == 413
|
|
1523
|
+
assert error_type == "context_overflow"
|
|
1524
|
+
|
|
1525
|
+
def test_auth_failure_points_at_the_key(self):
|
|
1526
|
+
import anthropic
|
|
1527
|
+
|
|
1528
|
+
from src.api.main import _describe_llm_error
|
|
1529
|
+
|
|
1530
|
+
exc = anthropic.AuthenticationError(
|
|
1531
|
+
"invalid x-api-key sk-ant-api03-secretmaterial",
|
|
1532
|
+
response=self._response(401),
|
|
1533
|
+
body=None,
|
|
1534
|
+
)
|
|
1535
|
+
|
|
1536
|
+
status, error_type, message = _describe_llm_error(exc)
|
|
1537
|
+
|
|
1538
|
+
assert status == 401
|
|
1539
|
+
assert error_type == "auth"
|
|
1540
|
+
assert "sk-ant-api03-secretmaterial" not in message
|
|
1541
|
+
|
|
1542
|
+
def test_unknown_failure_is_generic(self):
|
|
1543
|
+
from src.api.main import _describe_llm_error
|
|
1544
|
+
|
|
1545
|
+
status, error_type, message = _describe_llm_error(
|
|
1546
|
+
RuntimeError("Traceback (most recent call last): File /srv/hedit/main.py")
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
assert status == 500
|
|
1550
|
+
assert error_type == "internal"
|
|
1551
|
+
assert "Traceback" not in message
|
|
1552
|
+
assert "/srv/hedit" not in message
|
|
@@ -118,6 +118,57 @@ class TestHedPythonValidator:
|
|
|
118
118
|
assert isinstance(result.errors, list)
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
class TestSeverityMapping:
|
|
122
|
+
"""Tests that hedtools severities map to the right bucket.
|
|
123
|
+
|
|
124
|
+
hedtools reports severity as an ErrorSeverity enum (ERROR=1,
|
|
125
|
+
WARNING=10). Comparing it to the string "error" is always False, which
|
|
126
|
+
filed every error as a warning and left is_valid True for any input --
|
|
127
|
+
"NotARealTag/Foo" validated clean and the workflow's refinement loop
|
|
128
|
+
could never fire (#161).
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def test_unknown_tag_is_an_error(self, validator):
|
|
132
|
+
result = validator.validate("Sensory-event, NotARealTag/Foo")
|
|
133
|
+
|
|
134
|
+
assert result.is_valid is False
|
|
135
|
+
assert [issue.code for issue in result.errors] == ["TAG_INVALID"]
|
|
136
|
+
assert result.warnings == []
|
|
137
|
+
|
|
138
|
+
def test_non_base_tag_is_an_error(self, validator):
|
|
139
|
+
"""A plausible-looking tag that is not in the schema still fails."""
|
|
140
|
+
result = validator.validate("Agent-action, (Press, Button)")
|
|
141
|
+
|
|
142
|
+
assert result.is_valid is False
|
|
143
|
+
assert any(issue.code == "TAG_INVALID" for issue in result.errors)
|
|
144
|
+
|
|
145
|
+
def test_repeated_tag_is_an_error(self, validator):
|
|
146
|
+
result = validator.validate("Sensory-event, Sensory-event")
|
|
147
|
+
|
|
148
|
+
assert result.is_valid is False
|
|
149
|
+
assert any(issue.code == "TAG_EXPRESSION_REPEATED" for issue in result.errors)
|
|
150
|
+
|
|
151
|
+
def test_tag_extension_is_a_warning(self, validator):
|
|
152
|
+
"""Extensions are allowed, so they must not invalidate the string."""
|
|
153
|
+
result = validator.validate("Sensory-event, Animal/Dog")
|
|
154
|
+
|
|
155
|
+
assert result.is_valid is True
|
|
156
|
+
assert result.errors == []
|
|
157
|
+
assert [issue.code for issue in result.warnings] == ["TAG_EXTENDED"]
|
|
158
|
+
|
|
159
|
+
def test_clean_string_has_no_issues(self, validator):
|
|
160
|
+
result = validator.validate("Sensory-event, Visual-presentation")
|
|
161
|
+
|
|
162
|
+
assert result.is_valid is True
|
|
163
|
+
assert result.errors == []
|
|
164
|
+
assert result.warnings == []
|
|
165
|
+
|
|
166
|
+
def test_errors_carry_their_level(self, validator):
|
|
167
|
+
result = validator.validate("Sensory-event, NotARealTag/Foo")
|
|
168
|
+
|
|
169
|
+
assert all(issue.level == "error" for issue in result.errors)
|
|
170
|
+
|
|
171
|
+
|
|
121
172
|
class TestHedJavaScriptValidator:
|
|
122
173
|
"""Tests for HedJavaScriptValidator class."""
|
|
123
174
|
|
|
@@ -189,14 +240,12 @@ def test_validate_valid_string(validator):
|
|
|
189
240
|
|
|
190
241
|
|
|
191
242
|
def test_validate_invalid_tag(validator):
|
|
192
|
-
"""
|
|
193
|
-
|
|
194
|
-
Note: HED 8.3.0+ reports invalid tags as warnings, not errors.
|
|
195
|
-
"""
|
|
243
|
+
"""An unknown tag is an error, so the string is invalid."""
|
|
196
244
|
result = validator.validate("Invalid-nonexistent-tag")
|
|
197
245
|
|
|
198
|
-
|
|
199
|
-
assert
|
|
246
|
+
assert result.is_valid is False
|
|
247
|
+
assert len(result.errors) >= 1
|
|
248
|
+
assert result.errors[0].code == "TAG_INVALID"
|
|
200
249
|
|
|
201
250
|
|
|
202
251
|
def test_validate_with_grouping(validator):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|