hedit 0.7.11.dev3__tar.gz → 0.7.11.dev6__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.dev3/hedit.egg-info → hedit-0.7.11.dev6}/PKG-INFO +1 -1
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/README.md +5 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6/hedit.egg-info}/PKG-INFO +1 -1
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/hedit.egg-info/SOURCES.txt +3 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/pyproject.toml +1 -1
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/api/main.py +200 -63
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/api/models.py +67 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/cli/client.py +5 -5
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/cli/config.py +3 -2
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/cli/local_executor.py +83 -35
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/cli/main.py +1 -1
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/cli/output.py +83 -11
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/scripts/process_feedback.py +1 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/telemetry/schema.py +59 -11
- hedit-0.7.11.dev6/src/utils/anthropic_llm.py +475 -0
- hedit-0.7.11.dev6/src/utils/llm_usage.py +345 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/validation/hed_validator.py +9 -2
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/version.py +1 -1
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_annotation_agent.py +55 -0
- hedit-0.7.11.dev6/tests/test_anthropic_llm.py +451 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_api_endpoints.py +153 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_cli_client.py +14 -2
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_cli_integration.py +3 -3
- hedit-0.7.11.dev6/tests/test_cli_usage_report.py +209 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_integration_anthropic.py +102 -0
- hedit-0.7.11.dev6/tests/test_llm_usage.py +294 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_telemetry.py +99 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_validation.py +55 -6
- hedit-0.7.11.dev3/src/utils/anthropic_llm.py +0 -246
- hedit-0.7.11.dev3/tests/test_anthropic_llm.py +0 -170
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/LICENSE +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/PKG_README.md +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/hedit.egg-info/dependency_links.txt +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/hedit.egg-info/entry_points.txt +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/hedit.egg-info/requires.txt +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/hedit.egg-info/top_level.txt +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/setup.cfg +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/__init__.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/agents/__init__.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/agents/annotation_agent.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/agents/assessment_agent.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/agents/evaluation_agent.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/agents/feedback_summarizer.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/agents/feedback_triage_agent.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/agents/state.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/agents/validation_agent.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/agents/vision_agent.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/agents/workflow.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/api/__init__.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/api/security.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/cli/__init__.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/cli/api_executor.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/cli/commands/__init__.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/cli/commands/lsp.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/cli/executor.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/data/__init__.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/data/hed-docs/02_Terminology.md +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/data/hed-docs/HedAnnotationSemantics.md +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/data/hed-docs/manifest.json +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/lsp/__init__.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/lsp/client.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/lsp/daemon.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/lsp/protocol.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/scripts/__init__.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/telemetry/__init__.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/telemetry/collector.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/telemetry/storage.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/utils/__init__.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/utils/error_remediation.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/utils/github_client.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/utils/hed_comprehensive_guide.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/utils/hed_docs_loader.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/utils/image_processing.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/utils/json_schema_loader.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/utils/schema_loader.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/validation/__init__.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/src/validation/hed_lsp.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_cli_config.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_cli_main.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_comprehensive_guide.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_error_remediation.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_feedback_integration.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_feedback_triage.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_fetch_hed_docs.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_github_client.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_hed_docs_loader.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_json_schema_loader.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_no_extend_propagation.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_schema_loader.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_security.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_state.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_validation_agent.py +0 -0
- {hedit-0.7.11.dev3 → hedit-0.7.11.dev6}/tests/test_version.py +0 -0
|
@@ -67,6 +67,7 @@ Config files are stored in `~/.config/hedit/`:
|
|
|
67
67
|
- **Image Annotation**: Annotate visual stimuli directly from image files
|
|
68
68
|
- **Multi-Stage Validation**: AI agents generate, validate, evaluate, and refine annotations
|
|
69
69
|
- **Claude-Powered**: Anthropic Claude models (Haiku 4.5 default, Sonnet 5 optional); bring your own Anthropic key if you prefer your own billing
|
|
70
|
+
- **Cost Transparency**: Every annotation reports its token use and how much prompt caching saved (typically ~80% of input cost after the first request)
|
|
70
71
|
- **JSON Output**: Easy integration with scripts and pipelines
|
|
71
72
|
- **HED Schema Support**: Works with official HED schemas (8.x)
|
|
72
73
|
|
|
@@ -83,6 +84,9 @@ The agents work in feedback loops, automatically refining the annotation until i
|
|
|
83
84
|
|
|
84
85
|
## Documentation
|
|
85
86
|
|
|
87
|
+
- [Changelog](CHANGELOG.md) - What changed in each release
|
|
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
|
|
86
90
|
- [HED Standard](https://hedtags.org) - Learn about HED annotations
|
|
87
91
|
- [GitHub Issues](https://github.com/Annotation-Garden/HEDit/issues) - Report bugs or request features
|
|
88
92
|
|
|
@@ -234,6 +238,7 @@ uvicorn src.api.main:app --reload --host 0.0.0.0 --port 38427
|
|
|
234
238
|
- `POST /annotate`: Generate HED annotation from natural language
|
|
235
239
|
- `POST /validate`: Validate HED annotation
|
|
236
240
|
- `GET /health`: Health check
|
|
241
|
+
- `GET /metrics`: Token use, cost, and prompt-cache savings since startup (server API key required)
|
|
237
242
|
- API URL: `http://localhost:38427`
|
|
238
243
|
|
|
239
244
|
## Development
|
|
@@ -56,6 +56,7 @@ src/utils/hed_comprehensive_guide.py
|
|
|
56
56
|
src/utils/hed_docs_loader.py
|
|
57
57
|
src/utils/image_processing.py
|
|
58
58
|
src/utils/json_schema_loader.py
|
|
59
|
+
src/utils/llm_usage.py
|
|
59
60
|
src/utils/schema_loader.py
|
|
60
61
|
src/validation/__init__.py
|
|
61
62
|
src/validation/hed_lsp.py
|
|
@@ -67,6 +68,7 @@ tests/test_cli_client.py
|
|
|
67
68
|
tests/test_cli_config.py
|
|
68
69
|
tests/test_cli_integration.py
|
|
69
70
|
tests/test_cli_main.py
|
|
71
|
+
tests/test_cli_usage_report.py
|
|
70
72
|
tests/test_comprehensive_guide.py
|
|
71
73
|
tests/test_error_remediation.py
|
|
72
74
|
tests/test_feedback_integration.py
|
|
@@ -76,6 +78,7 @@ tests/test_github_client.py
|
|
|
76
78
|
tests/test_hed_docs_loader.py
|
|
77
79
|
tests/test_integration_anthropic.py
|
|
78
80
|
tests/test_json_schema_loader.py
|
|
81
|
+
tests/test_llm_usage.py
|
|
79
82
|
tests/test_no_extend_propagation.py
|
|
80
83
|
tests/test_schema_loader.py
|
|
81
84
|
tests/test_security.py
|
|
@@ -10,6 +10,7 @@ import logging
|
|
|
10
10
|
import os
|
|
11
11
|
import time
|
|
12
12
|
from contextlib import asynccontextmanager
|
|
13
|
+
from datetime import UTC, datetime
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
import anthropic
|
|
@@ -31,13 +32,21 @@ from src.api.models import (
|
|
|
31
32
|
HealthResponse,
|
|
32
33
|
ImageAnnotationRequest,
|
|
33
34
|
ImageAnnotationResponse,
|
|
35
|
+
MetricsResponse,
|
|
36
|
+
UsageSummary,
|
|
34
37
|
ValidationRequest,
|
|
35
38
|
ValidationResponse,
|
|
36
39
|
)
|
|
37
40
|
from src.api.security import api_key_auth, audit_logger
|
|
38
41
|
from src.lsp import HedLspClient
|
|
39
42
|
from src.telemetry import LocalFileStorage, TelemetryCollector, TelemetryEvent
|
|
40
|
-
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
|
+
)
|
|
49
|
+
from src.utils.llm_usage import UsageLedger, process_ledger, usage_scope
|
|
41
50
|
from src.utils.schema_loader import HedSchemaLoader
|
|
42
51
|
from src.validation.hed_validator import HedPythonValidator
|
|
43
52
|
|
|
@@ -56,6 +65,7 @@ lsp_client: HedLspClient | None = None
|
|
|
56
65
|
|
|
57
66
|
# Telemetry collector (initialized in lifespan)
|
|
58
67
|
telemetry_collector: TelemetryCollector | None = None
|
|
68
|
+
_startup_time: str = datetime.now(UTC).isoformat()
|
|
59
69
|
|
|
60
70
|
# Cache for BYOK configuration
|
|
61
71
|
_byok_config: dict = {}
|
|
@@ -100,6 +110,41 @@ def _describe_llm_error(exc: Exception) -> tuple[int, str, str]:
|
|
|
100
110
|
_USE_GLOBAL_LSP: HedLspClient = object() # type: ignore[assignment]
|
|
101
111
|
|
|
102
112
|
|
|
113
|
+
def _usage_summary(ledger: UsageLedger) -> UsageSummary | None:
|
|
114
|
+
"""Build the response's usage figures from a request's usage ledger.
|
|
115
|
+
|
|
116
|
+
Returns None when no LLM call was recorded (a fully cached or failed
|
|
117
|
+
request), so clients can tell "no calls" from "zero cost".
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
ledger: Ledger collected for one request
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
UsageSummary, or None when nothing was recorded
|
|
124
|
+
"""
|
|
125
|
+
totals = ledger.total()
|
|
126
|
+
if totals.calls == 0:
|
|
127
|
+
return None
|
|
128
|
+
return UsageSummary(**totals.as_dict())
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _override_header(req: Request, name: str) -> str | None:
|
|
132
|
+
"""Read a per-request override header, preferring the X-Anthropic-* spelling.
|
|
133
|
+
|
|
134
|
+
The X-OpenRouter-* names are the wire spelling from before the Anthropic
|
|
135
|
+
migration. They remain accepted indefinitely so that cached frontends and
|
|
136
|
+
third-party clients keep working; current clients send X-Anthropic-*.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
req: Incoming request
|
|
140
|
+
name: Header suffix, e.g. "model" for X-Anthropic-Model
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Header value, or None when neither spelling is present
|
|
144
|
+
"""
|
|
145
|
+
return req.headers.get(f"x-anthropic-{name}") or req.headers.get(f"x-openrouter-{name}")
|
|
146
|
+
|
|
147
|
+
|
|
103
148
|
def _resolve_lsp_client(
|
|
104
149
|
explicit: HedLspClient | None,
|
|
105
150
|
) -> HedLspClient | None:
|
|
@@ -160,13 +205,16 @@ def create_anthropic_workflow(
|
|
|
160
205
|
normalize_model(actual_eval_model)
|
|
161
206
|
|
|
162
207
|
# Create LLMs.
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
#
|
|
208
|
+
# Annotation thinks: measured over the benchmark descriptions, a 2048-token
|
|
209
|
+
# budget took first-attempt validity from 5/15 to 13/15 and cut total LLM
|
|
210
|
+
# calls by a third, for 24% more cost and about twice the latency. See
|
|
211
|
+
# annotation_thinking() and docs/prompt-caching.md.
|
|
166
212
|
annotation_llm = create_anthropic_llm(
|
|
167
213
|
model=actual_annotation_model,
|
|
168
214
|
api_key=api_key,
|
|
169
215
|
temperature=actual_temperature,
|
|
216
|
+
thinking=annotation_thinking(actual_annotation_model),
|
|
217
|
+
role="annotation",
|
|
170
218
|
)
|
|
171
219
|
# Evaluation / assessment / feedback / keyword extraction are short
|
|
172
220
|
# structured tasks; reasoning adds 5-10 s per call without
|
|
@@ -176,18 +224,21 @@ def create_anthropic_workflow(
|
|
|
176
224
|
api_key=api_key,
|
|
177
225
|
temperature=actual_temperature,
|
|
178
226
|
disable_reasoning=True,
|
|
227
|
+
role="evaluation",
|
|
179
228
|
)
|
|
180
229
|
assessment_llm = create_anthropic_llm(
|
|
181
230
|
model=actual_eval_model,
|
|
182
231
|
api_key=api_key,
|
|
183
232
|
temperature=actual_temperature,
|
|
184
233
|
disable_reasoning=True,
|
|
234
|
+
role="assessment",
|
|
185
235
|
)
|
|
186
236
|
feedback_llm = create_anthropic_llm(
|
|
187
237
|
model=actual_eval_model,
|
|
188
238
|
api_key=api_key,
|
|
189
239
|
temperature=actual_temperature,
|
|
190
240
|
disable_reasoning=True,
|
|
241
|
+
role="feedback",
|
|
191
242
|
)
|
|
192
243
|
# Keyword extraction (#148): use the fast annotation model with
|
|
193
244
|
# reasoning explicitly disabled and a small token cap. The
|
|
@@ -199,6 +250,7 @@ def create_anthropic_workflow(
|
|
|
199
250
|
temperature=actual_temperature,
|
|
200
251
|
max_tokens=200,
|
|
201
252
|
disable_reasoning=True,
|
|
253
|
+
role="keyword",
|
|
202
254
|
)
|
|
203
255
|
|
|
204
256
|
# Create and return workflow
|
|
@@ -283,6 +335,7 @@ def create_vision_agent(
|
|
|
283
335
|
model=actual_model,
|
|
284
336
|
api_key=api_key,
|
|
285
337
|
temperature=actual_temperature,
|
|
338
|
+
role="vision",
|
|
286
339
|
)
|
|
287
340
|
|
|
288
341
|
return VisionAgent(llm=vision_llm)
|
|
@@ -477,6 +530,9 @@ async def lifespan(app: FastAPI):
|
|
|
477
530
|
exc_info=True,
|
|
478
531
|
)
|
|
479
532
|
|
|
533
|
+
global _startup_time
|
|
534
|
+
_startup_time = datetime.now(UTC).isoformat()
|
|
535
|
+
|
|
480
536
|
# Initialize telemetry collector
|
|
481
537
|
global telemetry_collector
|
|
482
538
|
# Use /app/telemetry in Docker, otherwise use local .hedit/telemetry
|
|
@@ -555,13 +611,18 @@ app.add_middleware(
|
|
|
555
611
|
"X-Requested-With",
|
|
556
612
|
"X-API-Key",
|
|
557
613
|
"X-Anthropic-Key", # BYOK mode (Anthropic API key)
|
|
558
|
-
"X-
|
|
559
|
-
"X-
|
|
560
|
-
"X-
|
|
614
|
+
"X-Anthropic-Model", # Model override
|
|
615
|
+
"X-Anthropic-Eval-Model", # Eval model override
|
|
616
|
+
"X-Anthropic-Vision-Model", # Vision model override
|
|
617
|
+
"X-Anthropic-Temperature", # Temperature override
|
|
618
|
+
# Legacy X-OpenRouter-* spellings, still accepted as transport
|
|
619
|
+
"X-OpenRouter-Key",
|
|
620
|
+
"X-OpenRouter-Model",
|
|
621
|
+
"X-OpenRouter-Vision-Model",
|
|
561
622
|
"X-OpenRouter-Vision-Provider", # Legacy, ignored
|
|
562
623
|
"X-OpenRouter-Provider", # Legacy, ignored
|
|
563
|
-
"X-OpenRouter-Temperature",
|
|
564
|
-
"X-OpenRouter-Eval-Model",
|
|
624
|
+
"X-OpenRouter-Temperature",
|
|
625
|
+
"X-OpenRouter-Eval-Model",
|
|
565
626
|
"X-OpenRouter-Eval-Provider", # Legacy, ignored
|
|
566
627
|
"X-User-Id", # Legacy, ignored
|
|
567
628
|
],
|
|
@@ -646,9 +707,9 @@ async def annotate(
|
|
|
646
707
|
"""
|
|
647
708
|
# Determine which workflow to use
|
|
648
709
|
# Check for model override headers (from frontend dropdown or CLI)
|
|
649
|
-
model_override = request.model or req
|
|
650
|
-
eval_model_override = req
|
|
651
|
-
temp_header = req
|
|
710
|
+
model_override = request.model or _override_header(req, "model")
|
|
711
|
+
eval_model_override = _override_header(req, "eval-model")
|
|
712
|
+
temp_header = _override_header(req, "temperature")
|
|
652
713
|
temperature = request.temperature
|
|
653
714
|
if temperature is None and temp_header:
|
|
654
715
|
try:
|
|
@@ -658,7 +719,7 @@ async def annotate(
|
|
|
658
719
|
|
|
659
720
|
if api_key == "byok":
|
|
660
721
|
# BYOK mode: Create workflow with user's Anthropic key
|
|
661
|
-
byok_key = req
|
|
722
|
+
byok_key = _override_header(req, "key")
|
|
662
723
|
if not byok_key:
|
|
663
724
|
raise HTTPException(status_code=401, detail="Missing X-Anthropic-Key header")
|
|
664
725
|
|
|
@@ -707,14 +768,15 @@ async def annotate(
|
|
|
707
768
|
config = {"recursion_limit": 50}
|
|
708
769
|
|
|
709
770
|
start_time = time.time()
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
771
|
+
with usage_scope() as usage:
|
|
772
|
+
final_state = await active_workflow.run(
|
|
773
|
+
input_description=request.description,
|
|
774
|
+
schema_version=request.schema_version,
|
|
775
|
+
max_validation_attempts=request.max_validation_attempts,
|
|
776
|
+
run_assessment=request.run_assessment,
|
|
777
|
+
no_extend=request.no_extend,
|
|
778
|
+
config=config,
|
|
779
|
+
)
|
|
718
780
|
latency_ms = int((time.time() - start_time) * 1000)
|
|
719
781
|
|
|
720
782
|
# Determine overall status
|
|
@@ -728,12 +790,12 @@ async def annotate(
|
|
|
728
790
|
# Get model info from request body, BYOK headers, or server config
|
|
729
791
|
model_name = (
|
|
730
792
|
request.model
|
|
731
|
-
or req
|
|
793
|
+
or _override_header(req, "model")
|
|
732
794
|
or os.getenv("ANNOTATION_MODEL", DEFAULT_MODEL)
|
|
733
795
|
)
|
|
734
796
|
temperature = request.temperature
|
|
735
797
|
if temperature is None:
|
|
736
|
-
temp_header = req
|
|
798
|
+
temp_header = _override_header(req, "temperature")
|
|
737
799
|
if temp_header is not None:
|
|
738
800
|
try:
|
|
739
801
|
temperature = float(temp_header)
|
|
@@ -753,6 +815,7 @@ async def annotate(
|
|
|
753
815
|
temperature=temperature,
|
|
754
816
|
latency_ms=latency_ms,
|
|
755
817
|
source="api",
|
|
818
|
+
usage=usage.total(),
|
|
756
819
|
)
|
|
757
820
|
await telemetry_collector.collect(event)
|
|
758
821
|
|
|
@@ -767,6 +830,7 @@ async def annotate(
|
|
|
767
830
|
evaluation_feedback=final_state["evaluation_feedback"],
|
|
768
831
|
assessment_feedback=final_state["assessment_feedback"],
|
|
769
832
|
status=status,
|
|
833
|
+
usage=_usage_summary(usage),
|
|
770
834
|
)
|
|
771
835
|
|
|
772
836
|
except Exception as e:
|
|
@@ -803,10 +867,10 @@ async def annotate_from_image(
|
|
|
803
867
|
"""
|
|
804
868
|
# Determine which workflow and vision agent to use
|
|
805
869
|
# Check for model override headers (from frontend dropdown or CLI)
|
|
806
|
-
model_override = request.model or req
|
|
807
|
-
vision_model_override = request.vision_model or req
|
|
808
|
-
eval_model_override = req
|
|
809
|
-
temp_header = req
|
|
870
|
+
model_override = request.model or _override_header(req, "model")
|
|
871
|
+
vision_model_override = request.vision_model or _override_header(req, "vision-model")
|
|
872
|
+
eval_model_override = _override_header(req, "eval-model")
|
|
873
|
+
temp_header = _override_header(req, "temperature")
|
|
810
874
|
temperature = request.temperature
|
|
811
875
|
if temperature is None and temp_header:
|
|
812
876
|
try:
|
|
@@ -816,7 +880,7 @@ async def annotate_from_image(
|
|
|
816
880
|
|
|
817
881
|
if api_key == "byok":
|
|
818
882
|
# BYOK mode: Create workflow and vision agent with user's Anthropic key
|
|
819
|
-
byok_key = req
|
|
883
|
+
byok_key = _override_header(req, "key")
|
|
820
884
|
if not byok_key:
|
|
821
885
|
raise HTTPException(status_code=401, detail="Missing X-Anthropic-Key header")
|
|
822
886
|
|
|
@@ -878,26 +942,29 @@ async def annotate_from_image(
|
|
|
878
942
|
try:
|
|
879
943
|
start_time = time.time()
|
|
880
944
|
|
|
881
|
-
#
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
945
|
+
# The vision call and the annotation workflow share one usage scope
|
|
946
|
+
# so the reported figures cover the whole request.
|
|
947
|
+
with usage_scope() as usage:
|
|
948
|
+
# Step 1: Generate image description using vision model
|
|
949
|
+
vision_result = await active_vision_agent.describe_image(
|
|
950
|
+
image_data=request.image,
|
|
951
|
+
custom_prompt=request.prompt,
|
|
952
|
+
)
|
|
886
953
|
|
|
887
|
-
|
|
888
|
-
|
|
954
|
+
image_description = vision_result["description"]
|
|
955
|
+
image_metadata = vision_result["metadata"]
|
|
889
956
|
|
|
890
|
-
|
|
891
|
-
|
|
957
|
+
# Step 2: Pass description through HED annotation workflow
|
|
958
|
+
config = {"recursion_limit": 50}
|
|
892
959
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
960
|
+
final_state = await active_workflow.run(
|
|
961
|
+
input_description=image_description,
|
|
962
|
+
schema_version=request.schema_version,
|
|
963
|
+
max_validation_attempts=request.max_validation_attempts,
|
|
964
|
+
run_assessment=request.run_assessment,
|
|
965
|
+
no_extend=request.no_extend,
|
|
966
|
+
config=config,
|
|
967
|
+
)
|
|
901
968
|
latency_ms = int((time.time() - start_time) * 1000)
|
|
902
969
|
|
|
903
970
|
# Determine overall status
|
|
@@ -909,12 +976,12 @@ async def annotate_from_image(
|
|
|
909
976
|
# Get model info from request body, BYOK headers, or server config
|
|
910
977
|
model_name = (
|
|
911
978
|
request.model
|
|
912
|
-
or req
|
|
979
|
+
or _override_header(req, "model")
|
|
913
980
|
or os.getenv("ANNOTATION_MODEL", DEFAULT_MODEL)
|
|
914
981
|
)
|
|
915
982
|
temperature = request.temperature
|
|
916
983
|
if temperature is None:
|
|
917
|
-
temp_header = req
|
|
984
|
+
temp_header = _override_header(req, "temperature")
|
|
918
985
|
if temp_header is not None:
|
|
919
986
|
try:
|
|
920
987
|
temperature = float(temp_header)
|
|
@@ -934,6 +1001,7 @@ async def annotate_from_image(
|
|
|
934
1001
|
temperature=temperature,
|
|
935
1002
|
latency_ms=latency_ms,
|
|
936
1003
|
source="api-image", # Distinguish from text-based annotation
|
|
1004
|
+
usage=usage.total(),
|
|
937
1005
|
)
|
|
938
1006
|
await telemetry_collector.collect(event)
|
|
939
1007
|
|
|
@@ -950,6 +1018,7 @@ async def annotate_from_image(
|
|
|
950
1018
|
assessment_feedback=final_state["assessment_feedback"],
|
|
951
1019
|
status=status,
|
|
952
1020
|
image_metadata=image_metadata,
|
|
1021
|
+
usage=_usage_summary(usage),
|
|
953
1022
|
)
|
|
954
1023
|
|
|
955
1024
|
except Exception as e:
|
|
@@ -965,6 +1034,7 @@ async def _collect_stream_telemetry(
|
|
|
965
1034
|
start_time: float,
|
|
966
1035
|
source: str,
|
|
967
1036
|
description: str,
|
|
1037
|
+
usage: UsageLedger | None = None,
|
|
968
1038
|
) -> None:
|
|
969
1039
|
"""Collect telemetry for streaming endpoints.
|
|
970
1040
|
|
|
@@ -978,6 +1048,7 @@ async def _collect_stream_telemetry(
|
|
|
978
1048
|
start_time: Workflow start time (from time.time())
|
|
979
1049
|
source: Telemetry source identifier (e.g., "api-stream", "api-image-stream")
|
|
980
1050
|
description: Input description text (or image description for image endpoints)
|
|
1051
|
+
usage: Usage ledger for this request, when one was collected
|
|
981
1052
|
"""
|
|
982
1053
|
if not request.telemetry_enabled or not telemetry_collector:
|
|
983
1054
|
return
|
|
@@ -987,12 +1058,12 @@ async def _collect_stream_telemetry(
|
|
|
987
1058
|
# Get model info from request body, BYOK headers, or server config
|
|
988
1059
|
model_name = (
|
|
989
1060
|
request.model
|
|
990
|
-
or req
|
|
1061
|
+
or _override_header(req, "model")
|
|
991
1062
|
or os.getenv("ANNOTATION_MODEL", DEFAULT_MODEL)
|
|
992
1063
|
)
|
|
993
1064
|
temperature = request.temperature
|
|
994
1065
|
if temperature is None:
|
|
995
|
-
temp_header = req
|
|
1066
|
+
temp_header = _override_header(req, "temperature")
|
|
996
1067
|
if temp_header is not None:
|
|
997
1068
|
try:
|
|
998
1069
|
temperature = float(temp_header)
|
|
@@ -1012,6 +1083,7 @@ async def _collect_stream_telemetry(
|
|
|
1012
1083
|
temperature=temperature,
|
|
1013
1084
|
latency_ms=latency_ms,
|
|
1014
1085
|
source=source,
|
|
1086
|
+
usage=usage.total() if usage is not None else None,
|
|
1015
1087
|
)
|
|
1016
1088
|
await telemetry_collector.collect(event)
|
|
1017
1089
|
|
|
@@ -1044,9 +1116,9 @@ async def annotate_stream(
|
|
|
1044
1116
|
from src.agents.state import create_initial_state
|
|
1045
1117
|
|
|
1046
1118
|
# Determine which workflow to use (same logic as /annotate)
|
|
1047
|
-
model_override = request.model or req
|
|
1048
|
-
eval_model_override = req
|
|
1049
|
-
temp_header = req
|
|
1119
|
+
model_override = request.model or _override_header(req, "model")
|
|
1120
|
+
eval_model_override = _override_header(req, "eval-model")
|
|
1121
|
+
temp_header = _override_header(req, "temperature")
|
|
1050
1122
|
temperature = request.temperature
|
|
1051
1123
|
if temperature is None and temp_header:
|
|
1052
1124
|
try:
|
|
@@ -1055,7 +1127,7 @@ async def annotate_stream(
|
|
|
1055
1127
|
pass # Invalid header value, use default temperature
|
|
1056
1128
|
|
|
1057
1129
|
if api_key == "byok":
|
|
1058
|
-
byok_key = req
|
|
1130
|
+
byok_key = _override_header(req, "key")
|
|
1059
1131
|
if not byok_key:
|
|
1060
1132
|
raise HTTPException(status_code=401, detail="Missing X-Anthropic-Key header")
|
|
1061
1133
|
try:
|
|
@@ -1112,7 +1184,7 @@ async def annotate_stream(
|
|
|
1112
1184
|
"assess": ("assessing", "Running final assessment..."),
|
|
1113
1185
|
}
|
|
1114
1186
|
|
|
1115
|
-
async def event_generator():
|
|
1187
|
+
async def event_generator(usage: UsageLedger):
|
|
1116
1188
|
"""Generate SSE events for workflow progress using LangGraph streaming."""
|
|
1117
1189
|
|
|
1118
1190
|
def send_event(event_type: str, data: dict) -> str:
|
|
@@ -1211,6 +1283,9 @@ async def annotate_stream(
|
|
|
1211
1283
|
"assessment_feedback": current_state.get("assessment_feedback", ""),
|
|
1212
1284
|
"status": status,
|
|
1213
1285
|
}
|
|
1286
|
+
usage_summary = _usage_summary(usage)
|
|
1287
|
+
if usage_summary is not None:
|
|
1288
|
+
result["usage"] = usage_summary.model_dump()
|
|
1214
1289
|
|
|
1215
1290
|
yield send_event("result", result)
|
|
1216
1291
|
|
|
@@ -1223,6 +1298,7 @@ async def annotate_stream(
|
|
|
1223
1298
|
start_time=start_time,
|
|
1224
1299
|
source="api-stream",
|
|
1225
1300
|
description=request.description,
|
|
1301
|
+
usage=usage,
|
|
1226
1302
|
)
|
|
1227
1303
|
except Exception:
|
|
1228
1304
|
logging.warning("Telemetry collection failed for streaming request", exc_info=True)
|
|
@@ -1244,13 +1320,25 @@ async def annotate_stream(
|
|
|
1244
1320
|
start_time=start_time,
|
|
1245
1321
|
source="api-stream",
|
|
1246
1322
|
description=request.description,
|
|
1323
|
+
usage=usage,
|
|
1247
1324
|
)
|
|
1248
1325
|
except Exception:
|
|
1249
1326
|
logging.warning("Telemetry collection failed on error", exc_info=True)
|
|
1250
1327
|
yield send_event("done", {"message": "Workflow ended with error"})
|
|
1251
1328
|
|
|
1329
|
+
async def streamed_events():
|
|
1330
|
+
"""Hold one usage scope open for the whole stream.
|
|
1331
|
+
|
|
1332
|
+
The scope lives outside the generator that runs the workflow so that
|
|
1333
|
+
every LLM call made while the response streams is attributed to this
|
|
1334
|
+
request.
|
|
1335
|
+
"""
|
|
1336
|
+
with usage_scope() as usage:
|
|
1337
|
+
async for chunk in event_generator(usage):
|
|
1338
|
+
yield chunk
|
|
1339
|
+
|
|
1252
1340
|
return StreamingResponse(
|
|
1253
|
-
|
|
1341
|
+
streamed_events(),
|
|
1254
1342
|
media_type="text/event-stream",
|
|
1255
1343
|
headers={
|
|
1256
1344
|
"Cache-Control": "no-cache",
|
|
@@ -1289,10 +1377,10 @@ async def annotate_from_image_stream(
|
|
|
1289
1377
|
from src.agents.state import create_initial_state
|
|
1290
1378
|
|
|
1291
1379
|
# Determine which workflow and vision agent to use (same logic as /annotate-from-image)
|
|
1292
|
-
model_override = request.model or req
|
|
1293
|
-
vision_model_override = request.vision_model or req
|
|
1294
|
-
eval_model_override = req
|
|
1295
|
-
temp_header = req
|
|
1380
|
+
model_override = request.model or _override_header(req, "model")
|
|
1381
|
+
vision_model_override = request.vision_model or _override_header(req, "vision-model")
|
|
1382
|
+
eval_model_override = _override_header(req, "eval-model")
|
|
1383
|
+
temp_header = _override_header(req, "temperature")
|
|
1296
1384
|
temperature = request.temperature
|
|
1297
1385
|
if temperature is None and temp_header:
|
|
1298
1386
|
try:
|
|
@@ -1301,7 +1389,7 @@ async def annotate_from_image_stream(
|
|
|
1301
1389
|
pass
|
|
1302
1390
|
|
|
1303
1391
|
if api_key == "byok":
|
|
1304
|
-
byok_key = req
|
|
1392
|
+
byok_key = _override_header(req, "key")
|
|
1305
1393
|
if not byok_key:
|
|
1306
1394
|
raise HTTPException(status_code=401, detail="Missing X-Anthropic-Key header")
|
|
1307
1395
|
try:
|
|
@@ -1365,7 +1453,7 @@ async def annotate_from_image_stream(
|
|
|
1365
1453
|
"assess": ("assessing", "Running final assessment..."),
|
|
1366
1454
|
}
|
|
1367
1455
|
|
|
1368
|
-
async def event_generator():
|
|
1456
|
+
async def event_generator(usage: UsageLedger):
|
|
1369
1457
|
"""Generate SSE events for image annotation workflow progress."""
|
|
1370
1458
|
|
|
1371
1459
|
def send_event(event_type: str, data: dict) -> str:
|
|
@@ -1496,6 +1584,9 @@ async def annotate_from_image_stream(
|
|
|
1496
1584
|
"status": status,
|
|
1497
1585
|
"image_metadata": image_metadata,
|
|
1498
1586
|
}
|
|
1587
|
+
usage_summary = _usage_summary(usage)
|
|
1588
|
+
if usage_summary is not None:
|
|
1589
|
+
result["usage"] = usage_summary.model_dump()
|
|
1499
1590
|
|
|
1500
1591
|
yield send_event("result", result)
|
|
1501
1592
|
|
|
@@ -1508,6 +1599,7 @@ async def annotate_from_image_stream(
|
|
|
1508
1599
|
start_time=start_time,
|
|
1509
1600
|
source="api-image-stream",
|
|
1510
1601
|
description=image_description,
|
|
1602
|
+
usage=usage,
|
|
1511
1603
|
)
|
|
1512
1604
|
except Exception:
|
|
1513
1605
|
logging.debug(
|
|
@@ -1531,13 +1623,25 @@ async def annotate_from_image_stream(
|
|
|
1531
1623
|
start_time=start_time,
|
|
1532
1624
|
source="api-image-stream",
|
|
1533
1625
|
description=image_description or "image-annotation-failed",
|
|
1626
|
+
usage=usage,
|
|
1534
1627
|
)
|
|
1535
1628
|
except Exception:
|
|
1536
1629
|
logging.warning("Telemetry collection failed on image error", exc_info=True)
|
|
1537
1630
|
yield send_event("done", {"message": "Workflow ended with error"})
|
|
1538
1631
|
|
|
1632
|
+
async def streamed_events():
|
|
1633
|
+
"""Hold one usage scope open for the whole stream.
|
|
1634
|
+
|
|
1635
|
+
The scope lives outside the generator that runs the workflow so that
|
|
1636
|
+
every LLM call made while the response streams is attributed to this
|
|
1637
|
+
request.
|
|
1638
|
+
"""
|
|
1639
|
+
with usage_scope() as usage:
|
|
1640
|
+
async for chunk in event_generator(usage):
|
|
1641
|
+
yield chunk
|
|
1642
|
+
|
|
1539
1643
|
return StreamingResponse(
|
|
1540
|
-
|
|
1644
|
+
streamed_events(),
|
|
1541
1645
|
media_type="text/event-stream",
|
|
1542
1646
|
headers={
|
|
1543
1647
|
"Cache-Control": "no-cache",
|
|
@@ -1608,7 +1712,6 @@ async def submit_feedback(request: FeedbackRequest) -> FeedbackResponse:
|
|
|
1608
1712
|
Returns:
|
|
1609
1713
|
FeedbackResponse with feedback ID and status
|
|
1610
1714
|
"""
|
|
1611
|
-
from datetime import datetime
|
|
1612
1715
|
from uuid import uuid4
|
|
1613
1716
|
|
|
1614
1717
|
try:
|
|
@@ -1683,6 +1786,7 @@ async def submit_feedback(request: FeedbackRequest) -> FeedbackResponse:
|
|
|
1683
1786
|
model=model,
|
|
1684
1787
|
temperature=0.1,
|
|
1685
1788
|
max_tokens=1000,
|
|
1789
|
+
role="triage",
|
|
1686
1790
|
)
|
|
1687
1791
|
|
|
1688
1792
|
# Create and run triage agent
|
|
@@ -1737,6 +1841,39 @@ async def submit_feedback(request: FeedbackRequest) -> FeedbackResponse:
|
|
|
1737
1841
|
) from e
|
|
1738
1842
|
|
|
1739
1843
|
|
|
1844
|
+
@app.get("/metrics", response_model=MetricsResponse)
|
|
1845
|
+
async def metrics(api_key: str = Depends(api_key_auth)) -> MetricsResponse:
|
|
1846
|
+
"""Report LLM token usage, cost, and prompt-cache savings since startup.
|
|
1847
|
+
|
|
1848
|
+
These are server-wide operator figures, so BYOK callers are refused: a
|
|
1849
|
+
BYOK request gets its own numbers in the ``usage`` field of its
|
|
1850
|
+
annotation response instead.
|
|
1851
|
+
|
|
1852
|
+
Args:
|
|
1853
|
+
api_key: Authentication result (injected by dependency)
|
|
1854
|
+
|
|
1855
|
+
Returns:
|
|
1856
|
+
Totals since startup, broken down by agent role and by model
|
|
1857
|
+
|
|
1858
|
+
Raises:
|
|
1859
|
+
HTTPException: 403 when authenticated via BYOK
|
|
1860
|
+
"""
|
|
1861
|
+
if api_key == "byok":
|
|
1862
|
+
raise HTTPException(
|
|
1863
|
+
status_code=403,
|
|
1864
|
+
detail="Server metrics require a server API key; "
|
|
1865
|
+
"per-request usage is returned in the annotation response.",
|
|
1866
|
+
)
|
|
1867
|
+
|
|
1868
|
+
snapshot = process_ledger().snapshot()
|
|
1869
|
+
return MetricsResponse(
|
|
1870
|
+
since=_startup_time,
|
|
1871
|
+
total=UsageSummary(**snapshot["total"]),
|
|
1872
|
+
by_role={role: UsageSummary(**totals) for role, totals in snapshot["by_role"].items()},
|
|
1873
|
+
by_model={model: UsageSummary(**totals) for model, totals in snapshot["by_model"].items()},
|
|
1874
|
+
)
|
|
1875
|
+
|
|
1876
|
+
|
|
1740
1877
|
@app.get("/version")
|
|
1741
1878
|
async def get_version():
|
|
1742
1879
|
"""Get API version information.
|