See [docs/prs/2026-10-06-PR#24-billed-provider-calls.md](docs/prs/2026-10-06-PR%2324-billed-provider-calls.md).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
POST /api/billed-calls stores provider-billed calls before the first usage record,
tools/import-openrouter-activity reads the OpenRouter activity CSV;
the dashboard counts them in costs, tokens and the table per model, not in reviews.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; review requested from mi; merge scheduled for when the checks succeed
ORANGE 2 finding(s) that Jev rates as probably real and severe: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:101-102
hint 1 finding(s) of low severity or rated minor by Jev: server/src/main/kotlin/de/hoennig/werkrevisor/server/Statistics.kt:24-27
hint model recommends merge with caution
Findings
[medium] Body limit too small for a full billed-calls batch at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, ORANGE, as a line comment
[medium] Billed-call cut-off with no usage record imports everything at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:101-102, ORANGE, as a line comment
[low] Estimated flag ignores billed cost at server/src/main/kotlin/de/hoennig/werkrevisor/server/Statistics.kt:24-27, hint, as a line comment
Classifier
9 of 9 units passed all questions (1 PR document(s)).
2 answers ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 59779, calls 1, tokens in 59635 / out 850 (reasoning included in out), duration 15.5 s, cost 0.1278 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7666, cost 0.0003 USD, finding judge `jev-1.13.0` calls 2, tokens in 2658, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base ae6c043).
Reviewed head `ab5cfce19fa25c8462b269e1edc2002cb3a481fe` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.
<!-- werkrevisor:ab5cfce19fa25c8462b269e1edc2002cb3a481fe -->
<!-- werkrevisor-verdict:ORANGE -->
<!-- werkrevisor-folded -->
<details><summary>🟠 werkrevisor ORANGE for <code>ab5cfce</code>, superseded by <a href="http://git.javagil.de/mi/werkrevisor/pulls/24#issuecomment-1154">the result for <code>3b82847</code></a></summary>
## 🟠 werkrevisor: ORANGE / FINDINGS
Change kind: `mixed`.
### Reasons
- **ORANGE** 2 finding(s) that Jev rates as probably real and severe: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:101-102
- **YELLOW** risky paths changed: server/build.gradle.kts (server/*.gradle.kts)
- **hint** 1 finding(s) of low severity or rated minor by Jev: server/src/main/kotlin/de/hoennig/werkrevisor/server/Statistics.kt:24-27
- **hint** model recommends merge with caution
### Findings
1. **[medium] Body limit too small for a full billed-calls batch** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29`, ORANGE, as a line comment
2. **[medium] Billed-call cut-off with no usage record imports everything** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:101-102`, ORANGE, as a line comment
3. **[low] Estimated flag ignores billed cost** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/Statistics.kt:24-27`, hint, as a line comment
### Classifier
9 of 9 units passed all questions (1 PR document(s)).
2 answers ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 59779, calls 1, tokens in 59635 / out 850 (reasoning included in out), duration 15.5 s, cost 0.1278 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7666, cost 0.0003 USD, finding judge `jev-1.13.0` calls 2, tokens in 2658, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base ae6c043).<br>
Reviewed head `ab5cfce19fa25c8462b269e1edc2002cb3a481fe` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.
</sub>
</details>
werkrevisor: ORANGE / FINDINGS for ab5cfce19fa25c8462b269e1edc2002cb3a481fe.
The result comment of werkrevisor holds the details.
werkrevisor: ORANGE / FINDINGS for `ab5cfce19fa25c8462b269e1edc2002cb3a481fe`.
The result comment of werkrevisor holds the details.
<!-- werkrevisor-review:ab5cfce19fa25c8462b269e1edc2002cb3a481fe -->
[low] Estimated flag ignores billed cost (werkrevisor)
hint
The estimated flag and costUsd now include billedCostUsd, but the sum is shown with "=" only if the LLM and Jev parts are not estimated. A mix of billed and estimated parts is already marked "≈" for the whole total, which is fine, but the per-period bar and costPerPullRequestUsd use different cost bases. This is intended and documented in a comment, so the impact is low.
**[low] Estimated flag ignores billed cost** (werkrevisor)
hint
The `estimated` flag and `costUsd` now include `billedCostUsd`, but the sum is shown with "=" only if the LLM and Jev parts are not estimated. A mix of billed and estimated parts is already marked "≈" for the whole total, which is fine, but the per-period bar and `costPerPullRequestUsd` use different cost bases. This is intended and documented in a comment, so the impact is low.
[medium] Billed-call cut-off with no usage record imports everything (werkrevisor)
ORANGE; Jev: real 0.80, severity 3.0 of 3
When store.firstUsage() returns null, cutOff is null and every call is stored, including calls from the time the service runs. If the operator imports before the first run reports, or after the usage table is cleared, those calls are later counted twice once usage records arrive. The README says calls are skipped from the first usage record on, but nothing guards this case. The import still works as designed, but it silently stores calls that the first usage record will later duplicate.
**[medium] Billed-call cut-off with no usage record imports everything** (werkrevisor)
ORANGE; Jev: real 0.80, severity 3.0 of 3
When `store.firstUsage()` returns null, `cutOff` is null and every call is stored, including calls from the time the service runs. If the operator imports before the first run reports, or after the usage table is cleared, those calls are later counted twice once usage records arrive. The README says calls are skipped from the first usage record on, but nothing guards this case. The import still works as designed, but it silently stores calls that the first usage record will later duplicate.
[medium] Body limit too small for a full billed-calls batch (werkrevisor)
ORANGE; Jev: real 0.75, severity 3.0 of 3
The limit for /api/billed-calls is MAX_BILLED_CALLS * 512L, which is 256000 bytes. The comment says a billed call has about 250 bytes, which leaves little margin. Real calls carry a 36-character generation id, a model name and a timestamp, and with a longer model name or a pretty-printed body a full 500-call batch could be rejected with 413. The Python client always sends batches of 500, so the import would fail for the whole batch. The margin is thin and the limit is not tested.
**[medium] Body limit too small for a full billed-calls batch** (werkrevisor)
ORANGE; Jev: real 0.75, severity 3.0 of 3
The limit for `/api/billed-calls` is `MAX_BILLED_CALLS * 512L`, which is 256000 bytes. The comment says a billed call has about 250 bytes, which leaves little margin. Real calls carry a 36-character generation id, a model name and a timestamp, and with a longer model name or a pretty-printed body a full 500-call batch could be rejected with 413. The Python client always sends batches of 500, so the import would fail for the whole batch. The margin is thin and the limit is not tested.
ai-reviewer
scheduled this pull request to auto merge when all checks succeed 2026-10-06 12:02:09 +02:00
ai-reviewer
requested review from mi 2026-10-06 12:02:09 +02:00
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; a human review request is pending; merge already scheduled
ORANGE 2 finding(s) that Jev rates as probably real and severe: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:105-107, werkrevisor/openrouter_activity.py:19-28
hint 1 finding(s) of low severity or rated minor by Jev: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29
hint model recommends merge with caution
Findings
[medium] Billed calls with duplicate ids in one request at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:105-107, ORANGE, as a line comment
[medium] Cancelled and malformed CSV rows not handled at werkrevisor/openrouter_activity.py:19-28, ORANGE, as a line comment
[low] Body limit derived from call count at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, hint, still open in an earlier line comment
8 of 9 units passed all questions (1 PR document(s)).
2 answers ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 60925, calls 1, tokens in 60790 / out 762 (reasoning included in out), duration 13.0 s, cost 0.1292 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7683, cost 0.0003 USD, finding judge `jev-1.13.0` calls 2, tokens in 3002, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base ae6c043).
Reviewed head `3b82847c061b8984d0926198d3a3a50555bde93f` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.
<!-- werkrevisor:3b82847c061b8984d0926198d3a3a50555bde93f -->
<!-- werkrevisor-verdict:ORANGE -->
<!-- werkrevisor-folded -->
<details><summary>🟠 werkrevisor ORANGE for <code>3b82847</code>, superseded by <a href="http://git.javagil.de/mi/werkrevisor/pulls/24#issuecomment-1159">the result for <code>0eaede8</code></a></summary>
## 🟠 werkrevisor: ORANGE / FINDINGS
Change kind: `mixed`.
### Reasons
- **ORANGE** 2 finding(s) that Jev rates as probably real and severe: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:105-107, werkrevisor/openrouter_activity.py:19-28
- **YELLOW** risky paths changed: server/build.gradle.kts (server/*.gradle.kts)
- **YELLOW** spec: docs/prs/2026-10-06-PR#24-billed-provider-calls.md Scenario#24.04 vague_then (0.64 > yellow_above = 0.6)
- **hint** 1 finding(s) of low severity or rated minor by Jev: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29
- **hint** model recommends merge with caution
### Findings
1. **[medium] Billed calls with duplicate ids in one request** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:105-107`, ORANGE, as a line comment
2. **[medium] Cancelled and malformed CSV rows not handled** at `werkrevisor/openrouter_activity.py:19-28`, ORANGE, as a line comment
3. **[low] Body limit derived from call count** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29`, hint, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/24#issuecomment-1147)
### Classifier
| Unit | Question | Problem | Confidence | Verdict |
|---|---|---|---|---|
| docs/prs/2026-10-06-PR#24-billed-provider-calls.md Scenario#24.04 | vague_then | 0.64 | – | YELLOW |
8 of 9 units passed all questions (1 PR document(s)).
2 answers ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 60925, calls 1, tokens in 60790 / out 762 (reasoning included in out), duration 13.0 s, cost 0.1292 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7683, cost 0.0003 USD, finding judge `jev-1.13.0` calls 2, tokens in 3002, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base ae6c043).<br>
Reviewed head `3b82847c061b8984d0926198d3a3a50555bde93f` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.
</sub>
</details>
werkrevisor: ORANGE / FINDINGS for 3b82847c061b8984d0926198d3a3a50555bde93f.
The result comment of werkrevisor holds the details.
werkrevisor: ORANGE / FINDINGS for `3b82847c061b8984d0926198d3a3a50555bde93f`.
The result comment of werkrevisor holds the details.
<!-- werkrevisor-review:3b82847c061b8984d0926198d3a3a50555bde93f -->
[medium] Billed calls with duplicate ids in one request (werkrevisor)
ORANGE; Jev: real 0.79, severity 2.6 of 3
The cut-off uses store.firstUsage() only at import time. If a billed call predates the first usage record, a later import that carries calls with timestamps just before it is accepted. That is intended. However, before.count { store.addBilled(it) } is not transactional, so a failure midway stores part of the batch despite the "all or nothing" comment, which only covers validation. A retry is safe because of idempotency, but the comment overstates the guarantee.
**[medium] Billed calls with duplicate ids in one request** (werkrevisor)
ORANGE; Jev: real 0.79, severity 2.6 of 3
The cut-off uses `store.firstUsage()` only at import time. If a billed call predates the first usage record, a later import that carries calls with timestamps just before it is accepted. That is intended. However, `before.count { store.addBilled(it) }` is not transactional, so a failure midway stores part of the batch despite the "all or nothing" comment, which only covers validation. A retry is safe because of idempotency, but the comment overstates the guarantee.
[medium] Cancelled and malformed CSV rows not handled (werkrevisor)
ORANGE; Jev: real 0.91, severity 3.0 of 3
The CSV has a cancelled column, but calls ignores it. Cancelled generations are imported as billed calls. A row with a malformed value (for example an empty generation_id or a non-numeric cost) raises an uncaught ValueError or KeyError. main catches KeyError and reports it as "must be set", a misleading message that is meant for environment variables. A CSV missing a column therefore gives a wrong error.
**[medium] Cancelled and malformed CSV rows not handled** (werkrevisor)
ORANGE; Jev: real 0.91, severity 3.0 of 3
The CSV has a `cancelled` column, but `calls` ignores it. Cancelled generations are imported as billed calls. A row with a malformed value (for example an empty `generation_id` or a non-numeric cost) raises an uncaught `ValueError` or `KeyError`. `main` catches `KeyError` and reports it as "must be set", a misleading message that is meant for environment variables. A CSV missing a column therefore gives a wrong error.
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; a human review request is pending; merge already scheduled
ORANGE 2 finding(s) that Jev rates as probably real and severe: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:102-104
hint 1 finding(s) of low severity or rated minor by Jev: werkrevisor/statsclient.py:46
hint model recommends merge with caution
Findings
[medium] Body limit may reject real full batches at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, ORANGE, still open in an earlier line comment
[medium] Cut-off check misses fractional-second boundary at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:102-104, ORANGE, still open in an earlier line comment
[low] Report now reads the response body at werkrevisor/statsclient.py:46, hint, as a line comment
Classifier
9 of 9 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 62772, calls 1, tokens in 62621 / out 832 (reasoning included in out), duration 17.3 s, cost 0.1336 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7706, cost 0.0003 USD, finding judge `jev-1.13.0` calls 2, tokens in 2716, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base ae6c043).
Reviewed head `0eaede8e11c56c70e0dae5e720f3073fab04d5da` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.
<!-- werkrevisor:0eaede8e11c56c70e0dae5e720f3073fab04d5da -->
<!-- werkrevisor-verdict:ORANGE -->
<!-- werkrevisor-folded -->
<details><summary>🟠 werkrevisor ORANGE for <code>0eaede8</code>, superseded by <a href="http://git.javagil.de/mi/werkrevisor/pulls/24#issuecomment-1164">the result for <code>da58178</code></a></summary>
## 🟠 werkrevisor: ORANGE / FINDINGS
Change kind: `mixed`.
### Reasons
- **ORANGE** 2 finding(s) that Jev rates as probably real and severe: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:102-104
- **YELLOW** risky paths changed: server/build.gradle.kts (server/*.gradle.kts)
- **hint** 1 finding(s) of low severity or rated minor by Jev: werkrevisor/statsclient.py:46
- **hint** model recommends merge with caution
### Findings
1. **[medium] Body limit may reject real full batches** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29`, ORANGE, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/24#issuecomment-1147)
2. **[medium] Cut-off check misses fractional-second boundary** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:102-104`, ORANGE, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/24#issuecomment-1148)
3. **[low] Report now reads the response body** at `werkrevisor/statsclient.py:46`, hint, as a line comment
### Classifier
9 of 9 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 62772, calls 1, tokens in 62621 / out 832 (reasoning included in out), duration 17.3 s, cost 0.1336 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7706, cost 0.0003 USD, finding judge `jev-1.13.0` calls 2, tokens in 2716, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base ae6c043).<br>
Reviewed head `0eaede8e11c56c70e0dae5e720f3073fab04d5da` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.
</sub>
</details>
werkrevisor: ORANGE / FINDINGS for 0eaede8e11c56c70e0dae5e720f3073fab04d5da.
The result comment of werkrevisor holds the details.
werkrevisor: ORANGE / FINDINGS for `0eaede8e11c56c70e0dae5e720f3073fab04d5da`.
The result comment of werkrevisor holds the details.
<!-- werkrevisor-review:0eaede8e11c56c70e0dae5e720f3073fab04d5da -->
[low] Report now reads the response body (werkrevisor)
hint
report() now reads and decodes the whole response body through the shared _post. A non-UTF-8 body raises UnicodeDecodeError, which is not caught as StatsError. The caller only expects StatsError and logs it, so a bad answer could change the review run's behavior.
**[low] Report now reads the response body** (werkrevisor)
hint
`report()` now reads and decodes the whole response body through the shared `_post`. A non-UTF-8 body raises `UnicodeDecodeError`, which is not caught as `StatsError`. The caller only expects `StatsError` and logs it, so a bad answer could change the review run's behavior.
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; a human review request is pending; merge already scheduled
Assessment of the werkrevisor findings (as of da58178):
Fixed:
UsageApiController.kt: no import without a usage record (HTTP 409), since the cut-off is unknown; test added.
UsageApiController.kt / UsageStore.kt: the calls of one import are stored in one transaction (addAllBilled).
UsageBodyLimitFilter.kt:29: limit raised to 1024 bytes per call (512 KB per batch); a test sends 500 calls with real-length generation ids.
openrouter_activity.py: a malformed CSV raises "malformed activity CSV" instead of the misleading "must be set"; tests added.
statsclient.py:46: the answer is decoded with errors="replace", so a non-UTF-8 body never breaks the report of a run.
Scenario#24.04: the Then names the line in the table per model.
Not changed:
UsageBodyLimitFilter.kt:29 (third round): not a defect. The repeated line comment refers to the old limit; the new limit is 4× the real size of a call and is tested with a full batch.
UsageApiController.kt:102-104, fractional seconds at the cut-off: not a defect. The cut-off lies 10 minutes before the first usage record, and the call times are truncated to whole seconds before the comparison, so a sub-second boundary cannot drop or double a call of a real run.
openrouter_activity.py, cancelled calls: intended. OpenRouter bills cancelled calls too, and cost_total holds the billed amount; the PR doc and a test state this.
Statistics.kt:24-27, cost basis of the cost per pull request: intended and commented, since a billed call has no pull request.
YELLOW server/build.gradle.kts: only the version bump to 0.18.3.
Assessment of the werkrevisor findings (as of `da58178`):
Fixed:
- `UsageApiController.kt`: no import without a usage record (HTTP 409), since the cut-off is unknown; test added.
- `UsageApiController.kt` / `UsageStore.kt`: the calls of one import are stored in one transaction (`addAllBilled`).
- `UsageBodyLimitFilter.kt:29`: limit raised to 1024 bytes per call (512 KB per batch); a test sends 500 calls with real-length generation ids.
- `openrouter_activity.py`: a malformed CSV raises "malformed activity CSV" instead of the misleading "must be set"; tests added.
- `statsclient.py:46`: the answer is decoded with `errors="replace"`, so a non-UTF-8 body never breaks the report of a run.
- Scenario#24.04: the Then names the line in the table per model.
Not changed:
- `UsageBodyLimitFilter.kt:29` (third round): not a defect. The repeated line comment refers to the old limit; the new limit is 4× the real size of a call and is tested with a full batch.
- `UsageApiController.kt:102-104`, fractional seconds at the cut-off: not a defect. The cut-off lies 10 minutes before the first usage record, and the call times are truncated to whole seconds before the comparison, so a sub-second boundary cannot drop or double a call of a real run.
- `openrouter_activity.py`, cancelled calls: intended. OpenRouter bills cancelled calls too, and `cost_total` holds the billed amount; the PR doc and a test state this.
- `Statistics.kt:24-27`, cost basis of the cost per pull request: intended and commented, since a billed call has no pull request.
- YELLOW `server/build.gradle.kts`: only the version bump to 0.18.3.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
hint 1 finding(s) of low severity or rated minor by Jev: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29
hint model recommends merge with caution
Findings
[medium] Billed calls from the first-run window can be stored in a partial import at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:101-107, ORANGE, still open in an earlier line comment
[low] Body limit of 500 KB for the billed-calls endpoint is much larger than the comment says at server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, hint, still open in an earlier line comment
Classifier
9 of 9 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 62774, calls 1, tokens in 62626 / out 600 (reasoning included in out), duration 11.7 s, cost 0.1313 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7706, cost 0.0003 USD, finding judge `jev-1.13.0` calls 1, tokens in 1696, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base ae6c043).
Reviewed head `da58178088e610820857312dd5de338aed0cd1df` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.
<!-- werkrevisor:da58178088e610820857312dd5de338aed0cd1df -->
<!-- werkrevisor-verdict:ORANGE -->
## 🟠 werkrevisor: ORANGE / FINDINGS
Change kind: `mixed`.
### Reasons
- **ORANGE** 1 finding(s) that Jev rates as probably real and severe: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:101-107
- **YELLOW** risky paths changed: server/build.gradle.kts (server/*.gradle.kts)
- **hint** 1 finding(s) of low severity or rated minor by Jev: server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29
- **hint** model recommends merge with caution
### Findings
1. **[medium] Billed calls from the first-run window can be stored in a partial import** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:101-107`, ORANGE, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/24#issuecomment-1148)
2. **[low] Body limit of 500 KB for the billed-calls endpoint is much larger than the comment says** at `server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29`, hint, still open in [an earlier line comment](http://git.javagil.de/mi/werkrevisor/pulls/24#issuecomment-1147)
### Classifier
9 of 9 units passed all questions (1 PR document(s)).
1 answer ignored because of a confidence below [classifier] min_confidence.
<sub>
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 62774, calls 1, tokens in 62626 / out 600 (reasoning included in out), duration 11.7 s, cost 0.1313 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7706, cost 0.0003 USD, finding judge `jev-1.13.0` calls 1, tokens in 1696, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base ae6c043).<br>
Reviewed head `da58178088e610820857312dd5de338aed0cd1df` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.
</sub>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
See docs/prs/2026-10-06-PR#24-billed-provider-calls.md.
🤖 Generated with Claude Code
🟠 werkrevisor ORANGE for
ab5cfce, superseded by the result for3b82847🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
mixed.Reasons
Findings
[medium] Body limit too small for a full billed-calls batch at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, ORANGE, as a line comment[medium] Billed-call cut-off with no usage record imports everything at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:101-102, ORANGE, as a line comment[low] Estimated flag ignores billed cost at
server/src/main/kotlin/de/hoennig/werkrevisor/server/Statistics.kt:24-27, hint, as a line commentClassifier
9 of 9 units passed all questions (1 PR document(s)).
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 59779, calls 1, tokens in 59635 / out 850 (reasoning included in out), duration 15.5 s, cost 0.1278 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7666, cost 0.0003 USD, finding judge `jev-1.13.0` calls 2, tokens in 2658, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base2 answers ignored because of a confidence below [classifier] min_confidence.
ae6c043).Reviewed head `ab5cfce19fa25c8462b269e1edc2002cb3a481fe` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.
werkrevisor: ORANGE / FINDINGS for
ab5cfce19fa25c8462b269e1edc2002cb3a481fe.The result comment of werkrevisor holds the details.
@@ -21,2 +22,3 @@val billedCalls: Int = 0,) {val costUsd get() = llmCostUsd + jevCostUsdval costUsd get() = llmCostUsd + jevCostUsd + billedCostUsd[low] Estimated flag ignores billed cost (werkrevisor)
hint
The
estimatedflag andcostUsdnow includebilledCostUsd, but the sum is shown with "=" only if the LLM and Jev parts are not estimated. A mix of billed and estimated parts is already marked "≈" for the whole total, which is fine, but the per-period bar andcostPerPullRequestUsduse different cost bases. This is intended and documented in a comment, so the impact is low.@@ -51,0 +98,4 @@} catch (e: IllegalArgumentException) {return ResponseEntity.badRequest().body(mapOf("error" to (e.message ?: "invalid billed calls")))}val cutOff = store.firstUsage()?.minus(RUN_MARGIN)[medium] Billed-call cut-off with no usage record imports everything (werkrevisor)
ORANGE; Jev: real 0.80, severity 3.0 of 3
When
store.firstUsage()returns null,cutOffis null and every call is stored, including calls from the time the service runs. If the operator imports before the first run reports, or after the usage table is cleared, those calls are later counted twice once usage records arrive. The README says calls are skipped from the first usage record on, but nothing guards this case. The import still works as designed, but it silently stores calls that the first usage record will later duplicate.@@ -26,3 +26,4 @@companion object {const val MAX_BODY_BYTES = 16 * 1024Lprivate val LIMITS = mapOf("/api/usage" to MAX_BODY_BYTES, "/api/billed-calls" to MAX_BILLED_CALLS * 512L)[medium] Body limit too small for a full billed-calls batch (werkrevisor)
ORANGE; Jev: real 0.75, severity 3.0 of 3
The limit for
/api/billed-callsisMAX_BILLED_CALLS * 512L, which is 256000 bytes. The comment says a billed call has about 250 bytes, which leaves little margin. Real calls carry a 36-character generation id, a model name and a timestamp, and with a longer model name or a pretty-printed body a full 500-call batch could be rejected with 413. The Python client always sends batches of 500, so the import would fail for the whole batch. The margin is thin and the limit is not tested.🟠 werkrevisor ORANGE for
3b82847, superseded by the result for0eaede8🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
mixed.Reasons
Findings
[medium] Billed calls with duplicate ids in one request at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:105-107, ORANGE, as a line comment[medium] Cancelled and malformed CSV rows not handled at
werkrevisor/openrouter_activity.py:19-28, ORANGE, as a line comment[low] Body limit derived from call count at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, hint, still open in an earlier line commentClassifier
8 of 9 units passed all questions (1 PR document(s)).
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 60925, calls 1, tokens in 60790 / out 762 (reasoning included in out), duration 13.0 s, cost 0.1292 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7683, cost 0.0003 USD, finding judge `jev-1.13.0` calls 2, tokens in 3002, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base2 answers ignored because of a confidence below [classifier] min_confidence.
ae6c043).Reviewed head `3b82847c061b8984d0926198d3a3a50555bde93f` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.
werkrevisor: ORANGE / FINDINGS for
3b82847c061b8984d0926198d3a3a50555bde93f.The result comment of werkrevisor holds the details.
@@ -51,0 +102,4 @@val cutOff =store.firstUsage()?.minus(RUN_MARGIN)?: return ResponseEntity.status(HttpStatus.CONFLICT).body(mapOf("error" to "no usage record yet"))val (covered, before) = calls.partition { it.timestamp >= cutOff }[medium] Billed calls with duplicate ids in one request (werkrevisor)
ORANGE; Jev: real 0.79, severity 2.6 of 3
The cut-off uses
store.firstUsage()only at import time. If a billed call predates the first usage record, a later import that carries calls with timestamps just before it is accepted. That is intended. However,before.count { store.addBilled(it) }is not transactional, so a failure midway stores part of the batch despite the "all or nothing" comment, which only covers validation. A retry is safe because of idempotency, but the comment overstates the guarantee.@@ -0,0 +16,4 @@return "openrouter/" + DATE_SUFFIX.sub("", permaslug)def calls(rows):[medium] Cancelled and malformed CSV rows not handled (werkrevisor)
ORANGE; Jev: real 0.91, severity 3.0 of 3
The CSV has a
cancelledcolumn, butcallsignores it. Cancelled generations are imported as billed calls. A row with a malformed value (for example an emptygeneration_idor a non-numeric cost) raises an uncaughtValueErrororKeyError.maincatchesKeyErrorand reports it as "must be set", a misleading message that is meant for environment variables. A CSV missing a column therefore gives a wrong error.🟠 werkrevisor ORANGE for
0eaede8, superseded by the result forda58178🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
mixed.Reasons
Findings
[medium] Body limit may reject real full batches at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, ORANGE, still open in an earlier line comment[medium] Cut-off check misses fractional-second boundary at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:102-104, ORANGE, still open in an earlier line comment[low] Report now reads the response body at
werkrevisor/statsclient.py:46, hint, as a line commentClassifier
9 of 9 units passed all questions (1 PR document(s)).
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 62772, calls 1, tokens in 62621 / out 832 (reasoning included in out), duration 17.3 s, cost 0.1336 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7706, cost 0.0003 USD, finding judge `jev-1.13.0` calls 2, tokens in 2716, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base1 answer ignored because of a confidence below [classifier] min_confidence.
ae6c043).Reviewed head `0eaede8e11c56c70e0dae5e720f3073fab04d5da` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.
werkrevisor: ORANGE / FINDINGS for
0eaede8e11c56c70e0dae5e720f3073fab04d5da.The result comment of werkrevisor holds the details.
@@ -36,10 +43,12 @@ class StatsClient:try:with self._opener.open(request, timeout=self._timeout) as response:status = response.statusanswer = response.read().decode("utf-8")[low] Report now reads the response body (werkrevisor)
hint
report()now reads and decodes the whole response body through the shared_post. A non-UTF-8 body raisesUnicodeDecodeError, which is not caught asStatsError. The caller only expectsStatsErrorand logs it, so a bad answer could change the review run's behavior.Assessment of the werkrevisor findings (as of
da58178):Fixed:
UsageApiController.kt: no import without a usage record (HTTP 409), since the cut-off is unknown; test added.UsageApiController.kt/UsageStore.kt: the calls of one import are stored in one transaction (addAllBilled).UsageBodyLimitFilter.kt:29: limit raised to 1024 bytes per call (512 KB per batch); a test sends 500 calls with real-length generation ids.openrouter_activity.py: a malformed CSV raises "malformed activity CSV" instead of the misleading "must be set"; tests added.statsclient.py:46: the answer is decoded witherrors="replace", so a non-UTF-8 body never breaks the report of a run.Not changed:
UsageBodyLimitFilter.kt:29(third round): not a defect. The repeated line comment refers to the old limit; the new limit is 4× the real size of a call and is tested with a full batch.UsageApiController.kt:102-104, fractional seconds at the cut-off: not a defect. The cut-off lies 10 minutes before the first usage record, and the call times are truncated to whole seconds before the comparison, so a sub-second boundary cannot drop or double a call of a real run.openrouter_activity.py, cancelled calls: intended. OpenRouter bills cancelled calls too, andcost_totalholds the billed amount; the PR doc and a test state this.Statistics.kt:24-27, cost basis of the cost per pull request: intended and commented, since a billed call has no pull request.server/build.gradle.kts: only the version bump to 0.18.3.🤖 Generated with Claude Code
🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
mixed.Reasons
Findings
[medium] Billed calls from the first-run window can be stored in a partial import at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageApiController.kt:101-107, ORANGE, still open in an earlier line comment[low] Body limit of 500 KB for the billed-calls endpoint is much larger than the comment says at
server/src/main/kotlin/de/hoennig/werkrevisor/server/UsageBodyLimitFilter.kt:29, hint, still open in an earlier line commentClassifier
9 of 9 units passed all questions (1 PR document(s)).
Usage: model `openrouter/anthropic/claude-sonnet-5.5`, LLM done, tokens counted before the call 62774, calls 1, tokens in 62626 / out 600 (reasoning included in out), duration 11.7 s, cost 0.1313 USD (reported), classifier `jev-1.13.0` calls 9, tokens in 7706, cost 0.0003 USD, finding judge `jev-1.13.0` calls 1, tokens in 1696, cost 0.0001 USD, config: defaults + host + repository (.werkrevisor.toml at base1 answer ignored because of a confidence below [classifier] min_confidence.
ae6c043).Reviewed head `da58178088e610820857312dd5de338aed0cd1df` against base `ae6c04395b2d8f072bc0d9ffe68eb4f7b4d7fec0`, werkrevisor 0.18.2.