claude-dev-env 8.14.0 → 8.15.1
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/agents/test_agent_frontmatter.py +0 -72
- package/.agents/agents-archived/code-quality-agent.md +16 -16
- package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +5 -21
- package/.agents/skills/e-code-review/SKILL.md +1 -3
- package/.agents/skills/e-simplify/SKILL.md +6 -6
- package/.agents/skills/eli5/SKILL.md +4 -14
- package/.agents/skills/eli5/reference/task-seeds.md +3 -4
- package/.agents/skills/everything-search/SKILL.md +3 -3
- package/.agents/skills/fresh-branch/SKILL.md +2 -3
- package/.agents/skills/grok-spawn/SKILL.md +3 -8
- package/.agents/skills/issue-tracker/SKILL.md +4 -6
- package/.agents/skills/orchestrator-refresh/SKILL.md +7 -7
- package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +3 -3
- package/.agents/skills/privacy-hygiene/SKILL.md +6 -10
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/.agents/skills/pull-request/SKILL.md +11 -14
- package/.agents/skills/pull-request/reference/publication-tasks.md +2 -4
- package/.agents/skills/recovering-codex-startup/SKILL.md +3 -6
- package/.agents/skills/recovering-codex-startup/reference/task-seeds.md +1 -1
- package/.agents/skills/repairing-hook-boundaries/SKILL.md +4 -7
- package/.agents/skills/repairing-hook-boundaries/reference/task-seeds.md +1 -2
- 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/.agents/skills/syncing-submodules/SKILL.md +7 -17
- package/.agents/skills/syncing-submodules/reference/run-tasks.md +1 -1
- package/.agents/skills/task-build/SKILL.md +1 -1
- package/.agents/skills/task-build/reference/tool-routing.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -4
- package/.agents/skills/usage-pause/SKILL.md +1 -1
- package/.agents/skills/windows-scheduled-task/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/advisor/advisor-protocol.md +4 -4
- package/_shared/advisor/reference/consult-format.md +1 -1
- package/_shared/advisor/reference/identity.md +2 -2
- package/_shared/advisor/reference/warm-up.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -16
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install.mjs +1 -0
- package/bin/install.prompt-context.test.mjs +46 -0
- package/bin/install.test.mjs +3 -2
- package/commands/sr-loop.md +2 -2
- package/docs/CODE_RULES.md +2 -2
- package/docs/DJANGO_PATTERNS.md +5 -5
- package/docs/REACT_PATTERNS.md +6 -6
- package/docs/TEST_QUALITY.md +10 -28
- package/docs/agent-spawn-protocol.md +4 -4
- package/docs/codex-compatibility.md +4 -33
- package/docs/references/team-advisor-skill.md +0 -1
- package/docs/second-claude-account.md +4 -0
- package/docs/worker-completion-gate.md +0 -1
- package/hooks/advisory/migration_safety_advisor.py +3 -2
- package/hooks/advisory/refactor_guard.py +3 -3
- package/hooks/hooks.json +0 -5
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +1 -2
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +1 -2
- package/package.json +1 -1
- package/rules/explore-thoroughly.md +0 -1
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +5 -5
- package/rules/long-horizon-autonomy.md +1 -5
- package/rules/re-stage-before-commit.md +0 -2
- package/rules/research-mode.md +1 -1
- package/rules/shell-invocation.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/dev_env_scripts_constants/grok_worker_constants.py +8 -2
- package/scripts/grok_headless_runner.py +2 -5
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/policy_lint/engine.py +5 -2
- package/scripts/repository_checks/config/constants.py +1 -1
- package/scripts/spawn_grok_batch.py +12 -1
- 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_spawn_grok_batch.py +22 -0
- package/scripts/test_task_usage_events.py +47 -0
- package/scripts/test_task_usage_report.py +603 -0
- package/scripts/tests/test_banned_prose_words.py +1 -1
- package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +2 -2
- package/scripts/tests/test_rules.py +9 -9
- package/system-prompts/software-engineer.xml +15 -30
- package/audit-rubrics/audit-categories.json +0 -700
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +0 -90
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +0 -36
- package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +0 -35
- package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +0 -35
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +0 -40
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +0 -41
- package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +0 -38
- package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +0 -40
- package/audit-rubrics/category_rubrics/category-i-concurrency.md +0 -38
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +0 -59
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +0 -66
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +0 -48
- package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +0 -44
- package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +0 -54
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +0 -59
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +0 -46
- package/audit-rubrics/prompts/category-a-api-contracts.md +0 -407
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +0 -401
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +0 -420
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +0 -414
- package/audit-rubrics/prompts/category-e-dead-code.md +0 -434
- package/audit-rubrics/prompts/category-f-silent-failures.md +0 -432
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +0 -383
- package/audit-rubrics/prompts/category-h-security-boundaries.md +0 -423
- package/audit-rubrics/prompts/category-i-concurrency.md +0 -429
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +0 -458
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +0 -338
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +0 -131
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +0 -129
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +0 -139
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +0 -89
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +0 -51
- package/hooks/hooks_constants/style_reminder_prompt_constants.py +0 -9
- package/hooks/session/style_reminder_prompt.py +0 -41
- package/hooks/session/test_style_reminder_prompt.py +0 -32
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""Parse vendor usage events and compute token costs for task reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from dataclasses import fields as dataclass_fields
|
|
8
|
+
from decimal import Decimal, InvalidOperation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UsageReportRunFatal(ValueError):
|
|
12
|
+
"""Stop a report when its input cannot be used safely."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ModelRates:
|
|
17
|
+
input_per_million: Decimal
|
|
18
|
+
write_per_million: Decimal
|
|
19
|
+
read_per_million: Decimal
|
|
20
|
+
generated_per_million: Decimal
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class UsageEvent:
|
|
25
|
+
model: str
|
|
26
|
+
uncached_input_tokens: int | None
|
|
27
|
+
cache_write_tokens: int | None
|
|
28
|
+
cache_read_tokens: int | None
|
|
29
|
+
generated_tokens: int | None
|
|
30
|
+
reasoning_generated_tokens: int | None = None
|
|
31
|
+
cost_usd: Decimal | None = None
|
|
32
|
+
event_id: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _require_object(candidate: object, field_name: str) -> dict[str, object]:
|
|
36
|
+
if not isinstance(candidate, dict):
|
|
37
|
+
raise UsageReportRunFatal(f"{field_name} must be an object")
|
|
38
|
+
return {
|
|
39
|
+
each_key: each_entry
|
|
40
|
+
for each_key, each_entry in candidate.items()
|
|
41
|
+
if isinstance(each_key, str)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _require_list(candidate: object, field_name: str) -> list[object]:
|
|
46
|
+
if not isinstance(candidate, list):
|
|
47
|
+
raise UsageReportRunFatal(f"{field_name} must be a list")
|
|
48
|
+
return candidate
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _usage_object(candidate: object) -> dict[str, object]:
|
|
52
|
+
if candidate is None:
|
|
53
|
+
return {}
|
|
54
|
+
return _require_object(candidate, "usage")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _read_count(all_fields: dict[str, object], field_name: str) -> int | None:
|
|
58
|
+
if field_name not in all_fields:
|
|
59
|
+
return None
|
|
60
|
+
candidate = all_fields[field_name]
|
|
61
|
+
if isinstance(candidate, bool):
|
|
62
|
+
raise UsageReportRunFatal(f"{field_name} must be a nonnegative integer")
|
|
63
|
+
if isinstance(candidate, int) and candidate >= 0:
|
|
64
|
+
return candidate
|
|
65
|
+
if isinstance(candidate, str) and candidate.isascii() and candidate.isdecimal():
|
|
66
|
+
return int(candidate)
|
|
67
|
+
raise ValueError(f"{field_name} must be a nonnegative integer")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _read_money(all_fields: Mapping[str, object], field_name: str) -> Decimal | None:
|
|
71
|
+
if field_name not in all_fields or all_fields[field_name] is None:
|
|
72
|
+
return None
|
|
73
|
+
candidate = all_fields[field_name]
|
|
74
|
+
if isinstance(candidate, bool) or not isinstance(candidate, (int, float, str)):
|
|
75
|
+
raise UsageReportRunFatal(f"{field_name} must be a nonnegative amount")
|
|
76
|
+
try:
|
|
77
|
+
amount = Decimal(str(candidate))
|
|
78
|
+
except InvalidOperation as error:
|
|
79
|
+
raise ValueError(f"{field_name} must be a nonnegative amount") from error
|
|
80
|
+
if not amount.is_finite() or amount < 0:
|
|
81
|
+
raise ValueError(f"{field_name} must be a nonnegative amount")
|
|
82
|
+
return amount
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _safe_label(candidate: object) -> str:
|
|
86
|
+
if not isinstance(candidate, str):
|
|
87
|
+
return "unknown"
|
|
88
|
+
if not candidate:
|
|
89
|
+
return "unknown"
|
|
90
|
+
if not candidate.isascii() or not all(
|
|
91
|
+
each_character.isalnum() or each_character in "._-/"
|
|
92
|
+
for each_character in candidate
|
|
93
|
+
):
|
|
94
|
+
return "unknown"
|
|
95
|
+
return candidate
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _require_label(candidate: str, field_name: str) -> str:
|
|
99
|
+
label = _safe_label(candidate)
|
|
100
|
+
if label == "unknown":
|
|
101
|
+
raise ValueError(f"{field_name} needs a nonempty identifier")
|
|
102
|
+
return label
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _sum_known(all_counts: list[int | None]) -> int | None:
|
|
106
|
+
if not all_counts or any(each_count is None for each_count in all_counts):
|
|
107
|
+
return None
|
|
108
|
+
return sum(each_count for each_count in all_counts if each_count is not None)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _sum_costs(all_costs: list[Decimal | None]) -> Decimal | None:
|
|
112
|
+
if not all_costs or any(each_cost is None for each_cost in all_costs):
|
|
113
|
+
return None
|
|
114
|
+
return sum(
|
|
115
|
+
(each_cost for each_cost in all_costs if each_cost is not None), Decimal(0)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _display_money(amount: Decimal | None) -> str | None:
|
|
120
|
+
if amount is None:
|
|
121
|
+
return None
|
|
122
|
+
return format(amount.normalize(), "f")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _parse_rate(specification: str) -> tuple[str, ModelRates]:
|
|
126
|
+
model_text, separator, rate_text = specification.partition("=")
|
|
127
|
+
if not separator or _safe_label(model_text) == "unknown":
|
|
128
|
+
raise ValueError("rate needs MODEL=INPUT,WRITE,READ,OUTPUT")
|
|
129
|
+
all_rate_texts = rate_text.split(",")
|
|
130
|
+
if len(all_rate_texts) != len(dataclass_fields(ModelRates)):
|
|
131
|
+
raise ValueError("rate needs four prices per million tokens")
|
|
132
|
+
rate_fields = {str(i): each_rate for i, each_rate in enumerate(all_rate_texts)}
|
|
133
|
+
all_rates = [
|
|
134
|
+
_read_money(rate_fields, str(i))
|
|
135
|
+
for i in range(len(dataclass_fields(ModelRates)))
|
|
136
|
+
]
|
|
137
|
+
if any(each_rate is None for each_rate in all_rates):
|
|
138
|
+
raise ValueError("rate needs four prices per million tokens")
|
|
139
|
+
return model_text, ModelRates(
|
|
140
|
+
*[each_rate for each_rate in all_rates if each_rate is not None]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _calculate_codex_cost(
|
|
145
|
+
usage_event: UsageEvent, rates: ModelRates | None
|
|
146
|
+
) -> Decimal | None:
|
|
147
|
+
if rates is None:
|
|
148
|
+
return None
|
|
149
|
+
all_counts = (
|
|
150
|
+
usage_event.uncached_input_tokens,
|
|
151
|
+
usage_event.cache_write_tokens,
|
|
152
|
+
usage_event.cache_read_tokens,
|
|
153
|
+
usage_event.generated_tokens,
|
|
154
|
+
)
|
|
155
|
+
if any(each_count is None for each_count in all_counts):
|
|
156
|
+
return None
|
|
157
|
+
all_prices = (
|
|
158
|
+
rates.input_per_million,
|
|
159
|
+
rates.write_per_million,
|
|
160
|
+
rates.read_per_million,
|
|
161
|
+
rates.generated_per_million,
|
|
162
|
+
)
|
|
163
|
+
return sum(
|
|
164
|
+
Decimal(each_count) * each_price
|
|
165
|
+
for each_count, each_price in zip(all_counts, all_prices)
|
|
166
|
+
if each_count is not None
|
|
167
|
+
) / Decimal(1_000_000)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _parse_claude_usage(
|
|
171
|
+
all_fields: dict[str, object], model: str, is_model_usage: bool
|
|
172
|
+
) -> UsageEvent:
|
|
173
|
+
if is_model_usage:
|
|
174
|
+
return UsageEvent(
|
|
175
|
+
model=model,
|
|
176
|
+
uncached_input_tokens=_read_count(all_fields, "inputTokens"),
|
|
177
|
+
cache_write_tokens=_read_count(all_fields, "cacheCreationInputTokens"),
|
|
178
|
+
cache_read_tokens=_read_count(all_fields, "cacheReadInputTokens"),
|
|
179
|
+
generated_tokens=_read_count(all_fields, "outputTokens"),
|
|
180
|
+
cost_usd=_read_money(all_fields, "costUSD"),
|
|
181
|
+
)
|
|
182
|
+
return UsageEvent(
|
|
183
|
+
model=model,
|
|
184
|
+
uncached_input_tokens=_read_count(all_fields, "input_tokens"),
|
|
185
|
+
cache_write_tokens=_read_count(all_fields, "cache_creation_input_tokens"),
|
|
186
|
+
cache_read_tokens=_read_count(all_fields, "cache_read_input_tokens"),
|
|
187
|
+
generated_tokens=_read_count(all_fields, "output_tokens"),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _parse_claude_json(
|
|
192
|
+
all_document_fields: dict[str, object],
|
|
193
|
+
) -> tuple[list[UsageEvent], Decimal | None]:
|
|
194
|
+
if all_document_fields.get("type") != "result":
|
|
195
|
+
raise ValueError("Claude JSON needs one result object")
|
|
196
|
+
model_usage = all_document_fields.get("modelUsage")
|
|
197
|
+
if isinstance(model_usage, dict) and model_usage:
|
|
198
|
+
all_events = [
|
|
199
|
+
_parse_claude_usage(
|
|
200
|
+
_require_object(each_usage, "model usage"),
|
|
201
|
+
_safe_label(each_model),
|
|
202
|
+
True,
|
|
203
|
+
)
|
|
204
|
+
for each_model, each_usage in model_usage.items()
|
|
205
|
+
]
|
|
206
|
+
else:
|
|
207
|
+
usage_fields = _usage_object(all_document_fields.get("usage"))
|
|
208
|
+
all_events = [
|
|
209
|
+
_parse_claude_usage(
|
|
210
|
+
usage_fields, _safe_label(all_document_fields.get("model")), False
|
|
211
|
+
)
|
|
212
|
+
]
|
|
213
|
+
total_cost = _read_money(all_document_fields, "total_cost_usd")
|
|
214
|
+
return all_events, (
|
|
215
|
+
total_cost
|
|
216
|
+
if total_cost is not None
|
|
217
|
+
else _sum_costs([each_event.cost_usd for each_event in all_events])
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _validate_reasoning_tokens(
|
|
222
|
+
generated_tokens: int | None, reasoning_tokens: int | None
|
|
223
|
+
) -> None:
|
|
224
|
+
if (
|
|
225
|
+
generated_tokens is not None
|
|
226
|
+
and reasoning_tokens is not None
|
|
227
|
+
and reasoning_tokens > generated_tokens
|
|
228
|
+
):
|
|
229
|
+
raise ValueError("reasoning output exceeds output tokens")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _uncached_codex_tokens(
|
|
233
|
+
input_tokens: int | None,
|
|
234
|
+
cache_read_tokens: int | None,
|
|
235
|
+
cache_write_tokens: int | None,
|
|
236
|
+
) -> int | None:
|
|
237
|
+
if input_tokens is None or cache_read_tokens is None or cache_write_tokens is None:
|
|
238
|
+
return None
|
|
239
|
+
uncached_tokens = input_tokens - cache_read_tokens - cache_write_tokens
|
|
240
|
+
if uncached_tokens < 0:
|
|
241
|
+
raise ValueError("cached input exceeds input tokens")
|
|
242
|
+
return uncached_tokens
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _parse_codex_usage(
|
|
246
|
+
all_fields: dict[str, object], model: str, event_id: str | None
|
|
247
|
+
) -> UsageEvent:
|
|
248
|
+
input_tokens = _read_count(all_fields, "input_tokens")
|
|
249
|
+
cache_read_tokens = _read_count(all_fields, "cached_input_tokens")
|
|
250
|
+
cache_write_tokens = (
|
|
251
|
+
_read_count(all_fields, "cache_write_input_tokens")
|
|
252
|
+
if "cache_write_input_tokens" in all_fields
|
|
253
|
+
else 0
|
|
254
|
+
if all_fields
|
|
255
|
+
else None
|
|
256
|
+
)
|
|
257
|
+
generated_tokens = _read_count(all_fields, "output_tokens")
|
|
258
|
+
reasoning_tokens = _read_count(all_fields, "reasoning_output_tokens")
|
|
259
|
+
_validate_reasoning_tokens(generated_tokens, reasoning_tokens)
|
|
260
|
+
uncached_tokens = _uncached_codex_tokens(
|
|
261
|
+
input_tokens, cache_read_tokens, cache_write_tokens
|
|
262
|
+
)
|
|
263
|
+
return UsageEvent(
|
|
264
|
+
model,
|
|
265
|
+
uncached_tokens,
|
|
266
|
+
cache_write_tokens,
|
|
267
|
+
cache_read_tokens,
|
|
268
|
+
generated_tokens,
|
|
269
|
+
reasoning_tokens,
|
|
270
|
+
event_id=event_id,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _parse_codex_event(
|
|
275
|
+
all_document_fields: dict[str, object],
|
|
276
|
+
fallback_model: str,
|
|
277
|
+
rates_by_model: dict[str, ModelRates],
|
|
278
|
+
) -> UsageEvent | None:
|
|
279
|
+
if all_document_fields.get("type") != "turn.completed":
|
|
280
|
+
return None
|
|
281
|
+
model = _safe_label(all_document_fields.get("model"))
|
|
282
|
+
if model == "unknown":
|
|
283
|
+
model = fallback_model
|
|
284
|
+
usage_fields = _usage_object(all_document_fields.get("usage"))
|
|
285
|
+
event_id = _safe_label(all_document_fields.get("turn_id"))
|
|
286
|
+
usage_event = _parse_codex_usage(
|
|
287
|
+
usage_fields, model, None if event_id == "unknown" else event_id
|
|
288
|
+
)
|
|
289
|
+
return UsageEvent(
|
|
290
|
+
model=usage_event.model,
|
|
291
|
+
uncached_input_tokens=usage_event.uncached_input_tokens,
|
|
292
|
+
cache_write_tokens=usage_event.cache_write_tokens,
|
|
293
|
+
cache_read_tokens=usage_event.cache_read_tokens,
|
|
294
|
+
generated_tokens=usage_event.generated_tokens,
|
|
295
|
+
reasoning_generated_tokens=usage_event.reasoning_generated_tokens,
|
|
296
|
+
cost_usd=_calculate_codex_cost(usage_event, rates_by_model.get(model)),
|
|
297
|
+
event_id=usage_event.event_id,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _unwrap_otel_attribute(candidate: object) -> object:
|
|
302
|
+
if not isinstance(candidate, dict):
|
|
303
|
+
return candidate
|
|
304
|
+
for each_key in ("stringValue", "intValue", "doubleValue"):
|
|
305
|
+
if each_key in candidate:
|
|
306
|
+
return candidate[each_key]
|
|
307
|
+
return candidate
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _otel_attributes(all_document_fields: dict[str, object]) -> dict[str, object]:
|
|
311
|
+
raw_attributes = all_document_fields.get("attributes", {})
|
|
312
|
+
if isinstance(raw_attributes, dict):
|
|
313
|
+
return {
|
|
314
|
+
each_key: _unwrap_otel_attribute(each_entry)
|
|
315
|
+
for each_key, each_entry in raw_attributes.items()
|
|
316
|
+
}
|
|
317
|
+
all_attributes = _require_list(raw_attributes, "attributes")
|
|
318
|
+
attributes: dict[str, object] = {}
|
|
319
|
+
for each_attribute in all_attributes:
|
|
320
|
+
attribute = _require_object(each_attribute, "attribute")
|
|
321
|
+
attribute_key = attribute.get("key")
|
|
322
|
+
if isinstance(attribute_key, str):
|
|
323
|
+
attributes[attribute_key] = _unwrap_otel_attribute(attribute.get("value"))
|
|
324
|
+
return attributes
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _parse_otel_event(all_document_fields: dict[str, object]) -> UsageEvent | None:
|
|
328
|
+
attributes = (
|
|
329
|
+
_otel_attributes(all_document_fields)
|
|
330
|
+
if "attributes" in all_document_fields
|
|
331
|
+
else all_document_fields
|
|
332
|
+
)
|
|
333
|
+
event_name = _unwrap_otel_attribute(
|
|
334
|
+
all_document_fields.get("body", all_document_fields.get("event.name"))
|
|
335
|
+
)
|
|
336
|
+
if (
|
|
337
|
+
event_name not in ("claude_code.api_request", "api_request")
|
|
338
|
+
and attributes.get("event.name") != "api_request"
|
|
339
|
+
):
|
|
340
|
+
return None
|
|
341
|
+
cost = _read_money(attributes, "cost_usd")
|
|
342
|
+
if cost is None:
|
|
343
|
+
cost_micros = _read_count(attributes, "cost_usd_micros")
|
|
344
|
+
cost = (
|
|
345
|
+
None if cost_micros is None else Decimal(cost_micros) / Decimal(1_000_000)
|
|
346
|
+
)
|
|
347
|
+
event_id = _safe_label(attributes.get("request_id"))
|
|
348
|
+
return UsageEvent(
|
|
349
|
+
model=_safe_label(attributes.get("model")),
|
|
350
|
+
uncached_input_tokens=_read_count(attributes, "input_tokens"),
|
|
351
|
+
cache_write_tokens=_read_count(attributes, "cache_creation_tokens"),
|
|
352
|
+
cache_read_tokens=_read_count(attributes, "cache_read_tokens"),
|
|
353
|
+
generated_tokens=_read_count(attributes, "output_tokens"),
|
|
354
|
+
cost_usd=cost,
|
|
355
|
+
event_id=None if event_id == "unknown" else event_id,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _require_one_otel_session(all_records: list[dict[str, object]]) -> None:
|
|
360
|
+
all_session_ids: set[str] = set()
|
|
361
|
+
for each_record in all_records:
|
|
362
|
+
attributes = (
|
|
363
|
+
_otel_attributes(each_record)
|
|
364
|
+
if "attributes" in each_record
|
|
365
|
+
else each_record
|
|
366
|
+
)
|
|
367
|
+
all_session_ids.add(_safe_label(attributes.get("session.id")))
|
|
368
|
+
if len(all_session_ids) != 1 or "unknown" in all_session_ids:
|
|
369
|
+
raise ValueError("OTel input needs one session ID across all log records")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _cost_basis(source_format: str, total_cost: Decimal | None) -> str:
|
|
373
|
+
if total_cost is None:
|
|
374
|
+
return "unknown"
|
|
375
|
+
if source_format == "codex-json":
|
|
376
|
+
return "caller_rates"
|
|
377
|
+
return "provider_estimate"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _require_one_codex_thread(all_documents: list[dict[str, object]]) -> None:
|
|
381
|
+
all_thread_starts = [
|
|
382
|
+
_safe_label(each_document.get("thread_id"))
|
|
383
|
+
for each_document in all_documents
|
|
384
|
+
if each_document.get("type") == "thread.started"
|
|
385
|
+
]
|
|
386
|
+
if len(all_thread_starts) != 1 or all_thread_starts[0] == "unknown":
|
|
387
|
+
raise ValueError("Codex input needs one thread.started event")
|