claude-dev-env 8.14.0 → 8.15.0
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.
- package/.agents/skills/everything-search/SKILL.md +1 -1
- package/.agents/skills/second-account-workers/.claude/CLAUDE.md +1 -0
- package/.agents/skills/second-account-workers/AGENTS.md +1 -0
- package/.agents/skills/second-account-workers/SKILL.md +65 -0
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install.prompt-context.test.mjs +42 -0
- package/docs/DJANGO_PATTERNS.md +2 -2
- package/docs/TEST_QUALITY.md +1 -13
- package/docs/second-claude-account.md +4 -0
- package/hooks/hooks.json +1 -6
- package/hooks/session/style_reminder_prompt.py +18 -4
- package/hooks/session/test_style_reminder_prompt.py +57 -3
- package/package.json +1 -1
- package/rules/research-mode.md +1 -1
- package/scripts/claude_account_worker.py +347 -0
- package/scripts/claude_account_worker_process.py +169 -0
- package/scripts/claude_account_worker_report.py +170 -0
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +139 -0
- package/scripts/grok_headless_runner.py +2 -5
- package/scripts/subprocess_stream_text.py +28 -0
- package/scripts/task_usage_events.py +387 -0
- package/scripts/task_usage_report.py +391 -0
- package/scripts/test_claude_account_worker.py +331 -0
- package/scripts/test_task_usage_events.py +47 -0
- package/scripts/test_task_usage_report.py +603 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"""Check task usage reporting through its command line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from decimal import Decimal
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
SCRIPT_PATH = Path(__file__).with_name("task_usage_report.py")
|
|
14
|
+
CODEX_STREAM_FIXTURE = (
|
|
15
|
+
Path(__file__).resolve().parent.parent
|
|
16
|
+
/ "_shared"
|
|
17
|
+
/ "pr-loop"
|
|
18
|
+
/ "scripts"
|
|
19
|
+
/ "fixtures"
|
|
20
|
+
/ "success_stream_v0.144.3.jsonl"
|
|
21
|
+
)
|
|
22
|
+
RUN_ARGUMENTS = [
|
|
23
|
+
"run",
|
|
24
|
+
"--task-id",
|
|
25
|
+
"task-7",
|
|
26
|
+
"--run-id",
|
|
27
|
+
"run-1",
|
|
28
|
+
"--repository",
|
|
29
|
+
"team/project",
|
|
30
|
+
"--outcome",
|
|
31
|
+
"pass",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def invoke_cli(
|
|
36
|
+
arguments: list[str], stdin_text: str
|
|
37
|
+
) -> subprocess.CompletedProcess[str]:
|
|
38
|
+
return subprocess.run(
|
|
39
|
+
[sys.executable, str(SCRIPT_PATH), *arguments],
|
|
40
|
+
input=stdin_text,
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
check=False,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def report_for(source_format: str, source_payload: object) -> dict[str, object]:
|
|
48
|
+
if source_format == "codex-json":
|
|
49
|
+
stdin_text = "\n".join(
|
|
50
|
+
map(
|
|
51
|
+
json.dumps,
|
|
52
|
+
[{"type": "thread.started", "thread_id": "thread-1"}, source_payload],
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
stdin_text = json.dumps(source_payload)
|
|
57
|
+
completed = invoke_cli(
|
|
58
|
+
[*RUN_ARGUMENTS, "--source-format", source_format], stdin_text
|
|
59
|
+
)
|
|
60
|
+
assert completed.returncode == 0, completed.stderr
|
|
61
|
+
return json.loads(completed.stdout)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_claude_json_emits_only_allowlisted_fields_and_uses_model_usage_once() -> None:
|
|
65
|
+
source_payload = {
|
|
66
|
+
"type": "result",
|
|
67
|
+
"result": "SECRET_PROMPT_AND_ANSWER",
|
|
68
|
+
"session_id": "SECRET_SESSION",
|
|
69
|
+
"total_cost_usd": 0.125,
|
|
70
|
+
"usage": {
|
|
71
|
+
"input_tokens": 17,
|
|
72
|
+
"cache_creation_input_tokens": 11,
|
|
73
|
+
"cache_read_input_tokens": 31,
|
|
74
|
+
"output_tokens": 7,
|
|
75
|
+
},
|
|
76
|
+
"modelUsage": {
|
|
77
|
+
"claude-opus": {
|
|
78
|
+
"inputTokens": 17,
|
|
79
|
+
"cacheCreationInputTokens": 11,
|
|
80
|
+
"cacheReadInputTokens": 31,
|
|
81
|
+
"outputTokens": 7,
|
|
82
|
+
"costUSD": 0.125,
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
report = report_for("claude-json", source_payload)
|
|
87
|
+
serialized_report = json.dumps(report)
|
|
88
|
+
assert "SECRET_PROMPT_AND_ANSWER" not in serialized_report
|
|
89
|
+
assert "SECRET_SESSION" not in serialized_report
|
|
90
|
+
assert report["uncached_input_tokens"] == 17
|
|
91
|
+
assert report["cache_write_tokens"] == 11
|
|
92
|
+
assert report["cache_read_tokens"] == 31
|
|
93
|
+
assert report["output_tokens"] == 7
|
|
94
|
+
assert report["cost_usd"] == "0.125"
|
|
95
|
+
assert report["cost_basis"] == "provider_estimate"
|
|
96
|
+
assert report["models"] == ["claude-opus"]
|
|
97
|
+
assert report["event_count"] == 1
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_claude_json_keeps_missing_usage_unknown() -> None:
|
|
101
|
+
report = report_for(
|
|
102
|
+
"claude-json", {"type": "result", "total_cost_usd": 0.03, "result": "secret"}
|
|
103
|
+
)
|
|
104
|
+
assert report["uncached_input_tokens"] is None
|
|
105
|
+
assert report["cache_write_tokens"] is None
|
|
106
|
+
assert report["cost_usd"] == "0.03"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_claude_otel_deduplicates_request_ids_and_keeps_models_separate() -> None:
|
|
110
|
+
first_event = {
|
|
111
|
+
"body": "claude_code.api_request",
|
|
112
|
+
"attributes": {
|
|
113
|
+
"request_id": "request-1",
|
|
114
|
+
"session.id": "session-1",
|
|
115
|
+
"model": "claude-opus",
|
|
116
|
+
"input_tokens": 2,
|
|
117
|
+
"cache_creation_tokens": 3,
|
|
118
|
+
"cache_read_tokens": 5,
|
|
119
|
+
"output_tokens": 7,
|
|
120
|
+
"cost_usd_micros": 12_500,
|
|
121
|
+
"prompt": "SECRET_PROMPT",
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
second_event = {
|
|
125
|
+
"body": "claude_code.api_request",
|
|
126
|
+
"attributes": {
|
|
127
|
+
"request_id": "request-2",
|
|
128
|
+
"session.id": "session-1",
|
|
129
|
+
"model": "claude-sonnet",
|
|
130
|
+
"input_tokens": 11,
|
|
131
|
+
"cache_creation_tokens": 13,
|
|
132
|
+
"cache_read_tokens": 17,
|
|
133
|
+
"output_tokens": 19,
|
|
134
|
+
"cost_usd": 0.0375,
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
stdin_text = "\n".join(map(json.dumps, [first_event, first_event, second_event]))
|
|
138
|
+
completed = invoke_cli(
|
|
139
|
+
[*RUN_ARGUMENTS, "--source-format", "claude-otel"], stdin_text
|
|
140
|
+
)
|
|
141
|
+
assert completed.returncode == 0, completed.stderr
|
|
142
|
+
report = json.loads(completed.stdout)
|
|
143
|
+
assert report["event_count"] == 2
|
|
144
|
+
assert report["uncached_input_tokens"] == 13
|
|
145
|
+
assert report["cache_write_tokens"] == 16
|
|
146
|
+
assert report["cache_read_tokens"] == 22
|
|
147
|
+
assert report["output_tokens"] == 26
|
|
148
|
+
assert Decimal(report["cost_usd"]) == Decimal("0.0500")
|
|
149
|
+
assert report["models"] == ["claude-opus", "claude-sonnet"]
|
|
150
|
+
assert "SECRET_PROMPT" not in completed.stdout
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_claude_otel_accepts_typed_attributes_and_ignores_other_events() -> None:
|
|
154
|
+
source_payload = {
|
|
155
|
+
"resourceLogs": [
|
|
156
|
+
{
|
|
157
|
+
"scopeLogs": [
|
|
158
|
+
{
|
|
159
|
+
"logRecords": [
|
|
160
|
+
{
|
|
161
|
+
"body": {"stringValue": "claude_code.user_prompt"},
|
|
162
|
+
"attributes": [
|
|
163
|
+
{
|
|
164
|
+
"key": "session.id",
|
|
165
|
+
"value": {"stringValue": "session-1"},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"key": "prompt",
|
|
169
|
+
"value": {"stringValue": "SECRET_PROMPT"},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"body": {"stringValue": "claude_code.api_request"},
|
|
175
|
+
"attributes": [
|
|
176
|
+
{
|
|
177
|
+
"key": "request_id",
|
|
178
|
+
"value": {"stringValue": "r1"},
|
|
179
|
+
},
|
|
180
|
+
{"key": "model", "value": {"stringValue": "opus"}},
|
|
181
|
+
{
|
|
182
|
+
"key": "session.id",
|
|
183
|
+
"value": {"stringValue": "session-1"},
|
|
184
|
+
},
|
|
185
|
+
{"key": "input_tokens", "value": {"intValue": "4"}},
|
|
186
|
+
{
|
|
187
|
+
"key": "cache_creation_tokens",
|
|
188
|
+
"value": {"intValue": "0"},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"key": "cache_read_tokens",
|
|
192
|
+
"value": {"intValue": "8"},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"key": "output_tokens",
|
|
196
|
+
"value": {"intValue": "2"},
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
report = report_for("claude-otel", source_payload)
|
|
207
|
+
assert report["event_count"] == 1
|
|
208
|
+
assert report["uncached_input_tokens"] == 4
|
|
209
|
+
assert report["cache_read_tokens"] == 8
|
|
210
|
+
assert report["cost_usd"] is None
|
|
211
|
+
assert "SECRET_PROMPT" not in json.dumps(report)
|
|
212
|
+
assert "session-1" not in json.dumps(report)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_codex_uses_cached_and_reasoning_counts_as_subsets() -> None:
|
|
216
|
+
source_payload = [
|
|
217
|
+
{"type": "thread.started", "thread_id": "SECRET_THREAD"},
|
|
218
|
+
{
|
|
219
|
+
"type": "turn.completed",
|
|
220
|
+
"turn_id": "turn-1",
|
|
221
|
+
"model": "gpt-x",
|
|
222
|
+
"usage": {
|
|
223
|
+
"input_tokens": 100,
|
|
224
|
+
"cached_input_tokens": 20,
|
|
225
|
+
"cache_write_input_tokens": 5,
|
|
226
|
+
"output_tokens": 30,
|
|
227
|
+
"reasoning_output_tokens": 10,
|
|
228
|
+
},
|
|
229
|
+
"text": "SECRET_ANSWER",
|
|
230
|
+
},
|
|
231
|
+
]
|
|
232
|
+
stdin_text = "\n".join(map(json.dumps, source_payload))
|
|
233
|
+
completed = invoke_cli(
|
|
234
|
+
[
|
|
235
|
+
*RUN_ARGUMENTS,
|
|
236
|
+
"--source-format",
|
|
237
|
+
"codex-json",
|
|
238
|
+
"--rate",
|
|
239
|
+
"gpt-x=1,2,0.1,4",
|
|
240
|
+
"--rate-source",
|
|
241
|
+
"vendor-price-card",
|
|
242
|
+
"--rate-effective-date",
|
|
243
|
+
"2026-09-24",
|
|
244
|
+
],
|
|
245
|
+
stdin_text,
|
|
246
|
+
)
|
|
247
|
+
assert completed.returncode == 0, completed.stderr
|
|
248
|
+
report = json.loads(completed.stdout)
|
|
249
|
+
assert report["uncached_input_tokens"] == 75
|
|
250
|
+
assert report["cache_write_tokens"] == 5
|
|
251
|
+
assert report["cache_read_tokens"] == 20
|
|
252
|
+
assert report["output_tokens"] == 30
|
|
253
|
+
assert report["reasoning_output_tokens"] == 10
|
|
254
|
+
assert report["cost_usd"] == "0.000207"
|
|
255
|
+
assert report["cost_basis"] == "caller_rates"
|
|
256
|
+
assert report["rate_source"] == "vendor-price-card"
|
|
257
|
+
assert report["rate_effective_date"] == "2026-09-24"
|
|
258
|
+
assert "SECRET_THREAD" not in completed.stdout
|
|
259
|
+
assert "SECRET_ANSWER" not in completed.stdout
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_codex_deduplicates_turn_ids_and_requires_rates_for_cost() -> None:
|
|
263
|
+
turn_event = {
|
|
264
|
+
"type": "turn.completed",
|
|
265
|
+
"turn_id": "turn-1",
|
|
266
|
+
"usage": {
|
|
267
|
+
"input_tokens": 20,
|
|
268
|
+
"cached_input_tokens": 5,
|
|
269
|
+
"cache_write_input_tokens": 0,
|
|
270
|
+
"output_tokens": 3,
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
stdin_text = "\n".join(
|
|
274
|
+
map(
|
|
275
|
+
json.dumps,
|
|
276
|
+
[
|
|
277
|
+
{"type": "thread.started", "thread_id": "thread-1"},
|
|
278
|
+
turn_event,
|
|
279
|
+
turn_event,
|
|
280
|
+
],
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
completed = invoke_cli(
|
|
284
|
+
[*RUN_ARGUMENTS, "--source-format", "codex-json"], stdin_text
|
|
285
|
+
)
|
|
286
|
+
assert completed.returncode == 0, completed.stderr
|
|
287
|
+
report = json.loads(completed.stdout)
|
|
288
|
+
assert report["event_count"] == 1
|
|
289
|
+
assert report["uncached_input_tokens"] == 15
|
|
290
|
+
assert report["cost_usd"] is None
|
|
291
|
+
assert report["cost_basis"] == "unknown"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_codex_missing_usage_does_not_become_zero() -> None:
|
|
295
|
+
report = report_for("codex-json", {"type": "turn.completed"})
|
|
296
|
+
assert report["event_count"] == 1
|
|
297
|
+
assert report["uncached_input_tokens"] is None
|
|
298
|
+
assert report["output_tokens"] is None
|
|
299
|
+
assert report["cost_usd"] is None
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@pytest.mark.parametrize("bad_count", [-1, 1.5, "oops", None, True])
|
|
303
|
+
def test_malformed_numeric_usage_fails_without_echoing_event(bad_count: object) -> None:
|
|
304
|
+
source_payload = {
|
|
305
|
+
"type": "turn.completed",
|
|
306
|
+
"usage": {
|
|
307
|
+
"input_tokens": bad_count,
|
|
308
|
+
"cached_input_tokens": 0,
|
|
309
|
+
"cache_write_input_tokens": 0,
|
|
310
|
+
"output_tokens": 1,
|
|
311
|
+
},
|
|
312
|
+
"text": "SECRET_ANSWER",
|
|
313
|
+
}
|
|
314
|
+
completed = invoke_cli(
|
|
315
|
+
[*RUN_ARGUMENTS, "--source-format", "codex-json"],
|
|
316
|
+
"\n".join(
|
|
317
|
+
map(
|
|
318
|
+
json.dumps,
|
|
319
|
+
[{"type": "thread.started", "thread_id": "thread-1"}, source_payload],
|
|
320
|
+
)
|
|
321
|
+
),
|
|
322
|
+
)
|
|
323
|
+
assert completed.returncode != 0
|
|
324
|
+
assert completed.stdout == ""
|
|
325
|
+
assert "SECRET_ANSWER" not in completed.stderr
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_codex_rejects_cached_input_exceeding_total_input() -> None:
|
|
329
|
+
source_payload = {
|
|
330
|
+
"type": "turn.completed",
|
|
331
|
+
"usage": {
|
|
332
|
+
"input_tokens": 10,
|
|
333
|
+
"cached_input_tokens": 11,
|
|
334
|
+
"cache_write_input_tokens": 0,
|
|
335
|
+
"output_tokens": 1,
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
completed = invoke_cli(
|
|
339
|
+
[*RUN_ARGUMENTS, "--source-format", "codex-json"],
|
|
340
|
+
"\n".join(
|
|
341
|
+
map(
|
|
342
|
+
json.dumps,
|
|
343
|
+
[{"type": "thread.started", "thread_id": "thread-1"}, source_payload],
|
|
344
|
+
)
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
assert completed.returncode != 0
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_summary_groups_resumed_runs_and_includes_failed_attempt_cost() -> None:
|
|
351
|
+
first_report = {
|
|
352
|
+
"task_id": "task-7",
|
|
353
|
+
"run_id": "run-1",
|
|
354
|
+
"repository": "team/project",
|
|
355
|
+
"outcome": "fail",
|
|
356
|
+
"cost_usd": "0.20",
|
|
357
|
+
}
|
|
358
|
+
second_report = {
|
|
359
|
+
"task_id": "task-7",
|
|
360
|
+
"run_id": "run-2",
|
|
361
|
+
"repository": "team/project",
|
|
362
|
+
"outcome": "pass",
|
|
363
|
+
"cost_usd": "0.30",
|
|
364
|
+
}
|
|
365
|
+
stdin_text = "\n".join(map(json.dumps, [first_report, second_report]))
|
|
366
|
+
completed = invoke_cli(["summary"], stdin_text)
|
|
367
|
+
assert completed.returncode == 0, completed.stderr
|
|
368
|
+
summary = json.loads(completed.stdout)
|
|
369
|
+
assert summary["task_count"] == 1
|
|
370
|
+
assert summary["passed_task_count"] == 1
|
|
371
|
+
assert Decimal(summary["total_cost_usd"]) == Decimal("0.50")
|
|
372
|
+
assert Decimal(summary["cost_per_passed_task_usd"]) == Decimal("0.50")
|
|
373
|
+
assert summary["tasks"][0]["run_ids"] == ["run-1", "run-2"]
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@pytest.mark.parametrize("missing_field", ["cost_usd", "outcome"])
|
|
377
|
+
def test_summary_marks_missing_cost_or_outcome_unknown(missing_field: str) -> None:
|
|
378
|
+
source_report = {
|
|
379
|
+
"task_id": "task-7",
|
|
380
|
+
"run_id": "run-1",
|
|
381
|
+
"repository": "team/project",
|
|
382
|
+
"outcome": "pass",
|
|
383
|
+
"cost_usd": "0.20",
|
|
384
|
+
}
|
|
385
|
+
del source_report[missing_field]
|
|
386
|
+
completed = invoke_cli(["summary"], json.dumps(source_report))
|
|
387
|
+
assert completed.returncode == 0, completed.stderr
|
|
388
|
+
summary = json.loads(completed.stdout)
|
|
389
|
+
assert summary["cost_per_passed_task_usd"] is None
|
|
390
|
+
assert summary["status"] == "unknown"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def test_summary_deduplicates_identical_run_rows() -> None:
|
|
394
|
+
source_report = {
|
|
395
|
+
"task_id": "task-7",
|
|
396
|
+
"run_id": "run-1",
|
|
397
|
+
"repository": "team/project",
|
|
398
|
+
"outcome": "pass",
|
|
399
|
+
"cost_usd": "0.20",
|
|
400
|
+
}
|
|
401
|
+
completed = invoke_cli(
|
|
402
|
+
["summary"], "\n".join(map(json.dumps, [source_report, source_report]))
|
|
403
|
+
)
|
|
404
|
+
assert completed.returncode == 0, completed.stderr
|
|
405
|
+
summary = json.loads(completed.stdout)
|
|
406
|
+
assert Decimal(summary["total_cost_usd"]) == Decimal("0.20")
|
|
407
|
+
assert summary["tasks"][0]["run_ids"] == ["run-1"]
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def test_claude_otel_accepts_flat_event_attributes() -> None:
|
|
411
|
+
source_payload = {
|
|
412
|
+
"event.name": "api_request",
|
|
413
|
+
"session.id": "session-1",
|
|
414
|
+
"model": "claude-sonnet",
|
|
415
|
+
"input_tokens": 4,
|
|
416
|
+
"cache_creation_tokens": 2,
|
|
417
|
+
"cache_read_tokens": 3,
|
|
418
|
+
"output_tokens": 1,
|
|
419
|
+
"cost_usd": 0.002,
|
|
420
|
+
}
|
|
421
|
+
report = report_for("claude-otel", source_payload)
|
|
422
|
+
assert report["uncached_input_tokens"] == 4
|
|
423
|
+
assert report["cache_write_tokens"] == 2
|
|
424
|
+
assert report["cost_usd"] == "0.002"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_codex_mixed_models_need_each_models_rate() -> None:
|
|
428
|
+
all_events = [
|
|
429
|
+
{
|
|
430
|
+
"type": "turn.completed",
|
|
431
|
+
"turn_id": f"turn-{each_index}",
|
|
432
|
+
"model": each_model,
|
|
433
|
+
"usage": {
|
|
434
|
+
"input_tokens": 1_000_000,
|
|
435
|
+
"cached_input_tokens": 0,
|
|
436
|
+
"cache_write_input_tokens": 0,
|
|
437
|
+
"output_tokens": 0,
|
|
438
|
+
},
|
|
439
|
+
}
|
|
440
|
+
for each_index, each_model in enumerate(("gpt-small", "gpt-large"))
|
|
441
|
+
]
|
|
442
|
+
stdin_text = "\n".join(
|
|
443
|
+
map(
|
|
444
|
+
json.dumps,
|
|
445
|
+
[{"type": "thread.started", "thread_id": "thread-1"}, *all_events],
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
base_arguments = [*RUN_ARGUMENTS, "--source-format", "codex-json"]
|
|
449
|
+
rate_provenance = [
|
|
450
|
+
"--rate-source",
|
|
451
|
+
"vendor-price-card",
|
|
452
|
+
"--rate-effective-date",
|
|
453
|
+
"2026-09-24",
|
|
454
|
+
]
|
|
455
|
+
partial = invoke_cli(
|
|
456
|
+
[*base_arguments, "--rate", "gpt-small=1,0,0,0", *rate_provenance],
|
|
457
|
+
stdin_text,
|
|
458
|
+
)
|
|
459
|
+
complete = invoke_cli(
|
|
460
|
+
[
|
|
461
|
+
*base_arguments,
|
|
462
|
+
"--rate",
|
|
463
|
+
"gpt-small=1,0,0,0",
|
|
464
|
+
"--rate",
|
|
465
|
+
"gpt-large=3,0,0,0",
|
|
466
|
+
*rate_provenance,
|
|
467
|
+
],
|
|
468
|
+
stdin_text,
|
|
469
|
+
)
|
|
470
|
+
assert partial.returncode == 0, partial.stderr
|
|
471
|
+
assert complete.returncode == 0, complete.stderr
|
|
472
|
+
assert json.loads(partial.stdout)["cost_usd"] is None
|
|
473
|
+
complete_report = json.loads(complete.stdout)
|
|
474
|
+
assert complete_report["cost_usd"] == "4"
|
|
475
|
+
assert complete_report["models"] == ["gpt-large", "gpt-small"]
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def test_otel_rejects_mixed_sessions_without_leaking_identifiers() -> None:
|
|
479
|
+
all_events = [
|
|
480
|
+
{
|
|
481
|
+
"body": "claude_code.api_request",
|
|
482
|
+
"attributes": {
|
|
483
|
+
"session.id": each_session,
|
|
484
|
+
"request_id": f"request-{each_index}",
|
|
485
|
+
"model": "claude-opus",
|
|
486
|
+
"input_tokens": 1,
|
|
487
|
+
"cache_creation_tokens": 0,
|
|
488
|
+
"cache_read_tokens": 0,
|
|
489
|
+
"output_tokens": 1,
|
|
490
|
+
"cost_usd": 0.01,
|
|
491
|
+
},
|
|
492
|
+
}
|
|
493
|
+
for each_index, each_session in enumerate(
|
|
494
|
+
("SECRET_SESSION_A", "SECRET_SESSION_B")
|
|
495
|
+
)
|
|
496
|
+
]
|
|
497
|
+
completed = invoke_cli(
|
|
498
|
+
[*RUN_ARGUMENTS, "--source-format", "claude-otel"],
|
|
499
|
+
"\n".join(map(json.dumps, all_events)),
|
|
500
|
+
)
|
|
501
|
+
assert completed.returncode != 0
|
|
502
|
+
assert completed.stdout == ""
|
|
503
|
+
assert "SECRET_SESSION" not in completed.stderr
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def test_otel_rejects_non_api_record_from_another_session() -> None:
|
|
507
|
+
all_records = [
|
|
508
|
+
{
|
|
509
|
+
"body": "claude_code.api_request",
|
|
510
|
+
"attributes": {"session.id": "SECRET_SESSION_A"},
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
"body": "claude_code.user_prompt",
|
|
514
|
+
"attributes": {
|
|
515
|
+
"session.id": "SECRET_SESSION_B",
|
|
516
|
+
"prompt": "SECRET_PROMPT",
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
]
|
|
520
|
+
completed = invoke_cli(
|
|
521
|
+
[*RUN_ARGUMENTS, "--source-format", "claude-otel"], json.dumps(all_records)
|
|
522
|
+
)
|
|
523
|
+
assert completed.returncode != 0
|
|
524
|
+
assert completed.stdout == ""
|
|
525
|
+
assert "one session ID" in completed.stderr
|
|
526
|
+
assert "SECRET_SESSION" not in completed.stderr
|
|
527
|
+
assert "SECRET_PROMPT" not in completed.stderr
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def test_otel_rejects_capture_without_api_request() -> None:
|
|
531
|
+
source_payload = {
|
|
532
|
+
"body": "claude_code.user_prompt",
|
|
533
|
+
"attributes": {"session.id": "session-1"},
|
|
534
|
+
}
|
|
535
|
+
completed = invoke_cli(
|
|
536
|
+
[*RUN_ARGUMENTS, "--source-format", "claude-otel"], json.dumps(source_payload)
|
|
537
|
+
)
|
|
538
|
+
assert completed.returncode != 0
|
|
539
|
+
assert completed.stdout == ""
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def test_codex_rejects_concatenated_threads() -> None:
|
|
543
|
+
all_events = [
|
|
544
|
+
{"type": "thread.started", "thread_id": "thread-1"},
|
|
545
|
+
{"type": "turn.completed", "usage": {"input_tokens": 1}},
|
|
546
|
+
{"type": "thread.started", "thread_id": "thread-2"},
|
|
547
|
+
{"type": "turn.completed", "usage": {"input_tokens": 1}},
|
|
548
|
+
]
|
|
549
|
+
completed = invoke_cli(
|
|
550
|
+
[*RUN_ARGUMENTS, "--source-format", "codex-json"],
|
|
551
|
+
"\n".join(map(json.dumps, all_events)),
|
|
552
|
+
)
|
|
553
|
+
assert completed.returncode != 0
|
|
554
|
+
assert completed.stdout == ""
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def test_codex_rate_requires_source_and_effective_date() -> None:
|
|
558
|
+
all_events = [
|
|
559
|
+
{"type": "thread.started", "thread_id": "thread-1"},
|
|
560
|
+
{
|
|
561
|
+
"type": "turn.completed",
|
|
562
|
+
"model": "gpt-x",
|
|
563
|
+
"usage": {
|
|
564
|
+
"input_tokens": 1,
|
|
565
|
+
"cached_input_tokens": 0,
|
|
566
|
+
"cache_write_input_tokens": 0,
|
|
567
|
+
"output_tokens": 1,
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
]
|
|
571
|
+
completed = invoke_cli(
|
|
572
|
+
[*RUN_ARGUMENTS, "--source-format", "codex-json", "--rate", "gpt-x=1,0,0,1"],
|
|
573
|
+
"\n".join(map(json.dumps, all_events)),
|
|
574
|
+
)
|
|
575
|
+
assert completed.returncode != 0
|
|
576
|
+
assert completed.stdout == ""
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def test_codex_fixture_without_cache_write_field_uses_zero_write_cost() -> None:
|
|
580
|
+
completed = invoke_cli(
|
|
581
|
+
[
|
|
582
|
+
*RUN_ARGUMENTS,
|
|
583
|
+
"--source-format",
|
|
584
|
+
"codex-json",
|
|
585
|
+
"--model",
|
|
586
|
+
"gpt-x",
|
|
587
|
+
"--rate",
|
|
588
|
+
"gpt-x=1,2,0.1,4",
|
|
589
|
+
"--rate-source",
|
|
590
|
+
"vendor-price-card",
|
|
591
|
+
"--rate-effective-date",
|
|
592
|
+
"2026-09-24",
|
|
593
|
+
],
|
|
594
|
+
CODEX_STREAM_FIXTURE.read_text(encoding="utf-8"),
|
|
595
|
+
)
|
|
596
|
+
assert completed.returncode == 0, completed.stderr
|
|
597
|
+
report = json.loads(completed.stdout)
|
|
598
|
+
assert report["uncached_input_tokens"] == 12
|
|
599
|
+
assert report["cache_write_tokens"] == 0
|
|
600
|
+
assert report["cache_read_tokens"] == 0
|
|
601
|
+
assert report["output_tokens"] == 6
|
|
602
|
+
assert report["cost_usd"] == "0.000036"
|
|
603
|
+
assert report["cost_basis"] == "caller_rates"
|