okstra 0.26.0 → 0.27.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/README.kr.md +15 -0
- package/README.md +15 -0
- package/docs/kr/architecture.md +2 -6
- package/docs/kr/cli.md +40 -6
- package/docs/kr/performance-improvement-plan-v2.md +23 -0
- package/docs/kr/performance-improvement-plan.md +22 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/okstra.sh +0 -1
- package/runtime/prompts/profiles/_common-contract.md +25 -1
- package/runtime/prompts/profiles/error-analysis.md +12 -0
- package/runtime/prompts/profiles/implementation-planning.md +20 -0
- package/runtime/prompts/profiles/requirements-discovery.md +20 -0
- package/runtime/python/lib/okstra/cli.sh +1 -7
- package/runtime/python/lib/okstra/globals.sh +0 -1
- package/runtime/python/lib/okstra/usage.sh +1 -4
- package/runtime/python/okstra_ctl/render.py +3 -0
- package/runtime/python/okstra_ctl/run.py +0 -6
- package/runtime/python/okstra_ctl/run_context.py +1 -1
- package/runtime/python/okstra_ctl/wizard.py +25 -2
- package/runtime/python/okstra_token_usage/blocks.py +5 -1
- package/runtime/python/okstra_token_usage/claude.py +16 -1
- package/runtime/python/okstra_token_usage/collect.py +17 -3
- package/runtime/python/okstra_token_usage/pricing.py +159 -24
- package/runtime/skills/okstra-brief/SKILL.md +532 -65
- package/runtime/skills/okstra-context-loader/SKILL.md +25 -11
- package/runtime/skills/okstra-convergence/SKILL.md +37 -13
- package/runtime/skills/okstra-history/SKILL.md +68 -37
- package/runtime/skills/okstra-logs/SKILL.md +26 -4
- package/runtime/skills/okstra-report-finder/SKILL.md +49 -22
- package/runtime/skills/okstra-report-writer/SKILL.md +59 -64
- package/runtime/skills/okstra-run/SKILL.md +35 -34
- package/runtime/skills/okstra-schedule/SKILL.md +51 -20
- package/runtime/skills/okstra-setup/SKILL.md +31 -12
- package/runtime/skills/okstra-status/SKILL.md +20 -8
- package/runtime/skills/okstra-team-contract/SKILL.md +27 -15
- package/runtime/skills/okstra-time-summary/SKILL.md +53 -16
- package/runtime/templates/reports/settings.template.json +7 -4
- package/runtime/validators/lib/fixtures.sh +10 -2
- package/runtime/validators/lib/validate-assets.sh +50 -24
- package/runtime/validators/validate-brief.py +385 -0
- package/runtime/validators/validate-brief.sh +35 -0
- package/runtime/validators/validate-workflow.sh +7 -33
|
@@ -10,6 +10,7 @@ from .paths import claude_project_dir
|
|
|
10
10
|
def claude_session_totals(jsonl_path: Path) -> dict:
|
|
11
11
|
"""Return totals + agentName + assistant model + time window for a Claude session jsonl."""
|
|
12
12
|
input_t = output_t = cache_create_t = cache_read_t = 0
|
|
13
|
+
cache_create_5m_t = cache_create_1h_t = 0
|
|
13
14
|
tool_uses = 0
|
|
14
15
|
agent_name: str | None = None
|
|
15
16
|
model: str | None = None
|
|
@@ -23,8 +24,20 @@ def claude_session_totals(jsonl_path: Path) -> dict:
|
|
|
23
24
|
if usage:
|
|
24
25
|
input_t += usage.get("input_tokens", 0) or 0
|
|
25
26
|
output_t += usage.get("output_tokens", 0) or 0
|
|
26
|
-
|
|
27
|
+
cc_total = usage.get("cache_creation_input_tokens", 0) or 0
|
|
28
|
+
cache_create_t += cc_total
|
|
27
29
|
cache_read_t += usage.get("cache_read_input_tokens", 0) or 0
|
|
30
|
+
# Split into 5m / 1h ephemeral tiers when the API breakdown is
|
|
31
|
+
# present. If only the aggregate is given, attribute all of it to
|
|
32
|
+
# the 5m tier (1.25x — the cheaper assumption, matches prior
|
|
33
|
+
# behavior).
|
|
34
|
+
cc_break = usage.get("cache_creation") or {}
|
|
35
|
+
if isinstance(cc_break, dict) and (cc_break.get("ephemeral_5m_input_tokens") is not None
|
|
36
|
+
or cc_break.get("ephemeral_1h_input_tokens") is not None):
|
|
37
|
+
cache_create_5m_t += cc_break.get("ephemeral_5m_input_tokens", 0) or 0
|
|
38
|
+
cache_create_1h_t += cc_break.get("ephemeral_1h_input_tokens", 0) or 0
|
|
39
|
+
else:
|
|
40
|
+
cache_create_5m_t += cc_total
|
|
28
41
|
if rec.get("type") == "assistant":
|
|
29
42
|
if model is None and msg.get("model"):
|
|
30
43
|
model = msg["model"]
|
|
@@ -51,6 +64,8 @@ def claude_session_totals(jsonl_path: Path) -> dict:
|
|
|
51
64
|
"inputTokens": input_t,
|
|
52
65
|
"outputTokens": output_t,
|
|
53
66
|
"cacheCreationTokens": cache_create_t,
|
|
67
|
+
"cacheCreation5mTokens": cache_create_5m_t,
|
|
68
|
+
"cacheCreation1hTokens": cache_create_1h_t,
|
|
54
69
|
"cacheReadTokens": cache_read_t,
|
|
55
70
|
"toolUses": tool_uses,
|
|
56
71
|
"durationMs": duration_ms,
|
|
@@ -54,14 +54,16 @@ def _aggregate_totals(items: list[dict]) -> dict:
|
|
|
54
54
|
"""
|
|
55
55
|
aggregate: dict = {
|
|
56
56
|
"totalTokens": 0, "inputTokens": 0, "outputTokens": 0,
|
|
57
|
-
"cacheCreationTokens": 0, "
|
|
57
|
+
"cacheCreationTokens": 0, "cacheCreation5mTokens": 0, "cacheCreation1hTokens": 0,
|
|
58
|
+
"cacheReadTokens": 0,
|
|
58
59
|
"toolUses": 0, "durationMs": 0,
|
|
59
60
|
"agentName": None, "model": None,
|
|
60
61
|
"startedAt": None, "endedAt": None,
|
|
61
62
|
}
|
|
62
63
|
for t in items:
|
|
63
64
|
for k in ("totalTokens", "inputTokens", "outputTokens",
|
|
64
|
-
"cacheCreationTokens", "
|
|
65
|
+
"cacheCreationTokens", "cacheCreation5mTokens", "cacheCreation1hTokens",
|
|
66
|
+
"cacheReadTokens", "toolUses"):
|
|
65
67
|
aggregate[k] += t.get(k, 0) or 0
|
|
66
68
|
if aggregate["agentName"] is None and t.get("agentName"):
|
|
67
69
|
aggregate["agentName"] = t["agentName"]
|
|
@@ -210,6 +212,17 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
|
|
|
210
212
|
worker_billable = sum((w.get("usage") or {}).get("billableEquivalentTokens", 0) or 0 for w in workers)
|
|
211
213
|
worker_cost = sum((w.get("usage") or {}).get("estimatedCostUsd", 0) or 0 for w in workers)
|
|
212
214
|
cli_cost = sum((w.get("usage") or {}).get("cliEstimatedCostUsd", 0) or 0 for w in workers)
|
|
215
|
+
|
|
216
|
+
# Surface models whose pricing lookup failed so the silent-zero case is visible.
|
|
217
|
+
unmatched_models: list[str] = []
|
|
218
|
+
if lead.get("model") and lead.get("estimatedCostUsd") is None and (lead.get("totalTokens") or 0) > 0:
|
|
219
|
+
unmatched_models.append(lead["model"])
|
|
220
|
+
for w in workers:
|
|
221
|
+
u = w.get("usage") or {}
|
|
222
|
+
if u.get("model") and u.get("estimatedCostUsd") is None and (u.get("totalTokens") or 0) > 0:
|
|
223
|
+
unmatched_models.append(u["model"])
|
|
224
|
+
if u.get("cliModel") and u.get("cliEstimatedCostUsd") is None and (u.get("cliTotalTokens") or 0) > 0:
|
|
225
|
+
unmatched_models.append(u["cliModel"])
|
|
213
226
|
state["usageSummary"] = {
|
|
214
227
|
"leadTotalTokens": lead_total,
|
|
215
228
|
"workerTotalTokens": worker_total,
|
|
@@ -226,9 +239,10 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
|
|
|
226
239
|
"collectedAt": utc_now(),
|
|
227
240
|
"teamName": team_name,
|
|
228
241
|
"sessionsFound": len(claude_sessions),
|
|
242
|
+
"unmatchedModels": sorted(set(unmatched_models)),
|
|
229
243
|
"definitions": {
|
|
230
244
|
"totalTokens": "Sum of input + output + cache_creation + cache_read tokens (raw processed volume; matches Anthropic API breakdown). Cache reads are 95%+ in long sessions.",
|
|
231
|
-
"billableEquivalentTokens": "Tokens normalized to base-input-price units (
|
|
245
|
+
"billableEquivalentTokens": "Tokens normalized to base-input-price units (cache_creation_5m x1.25, cache_creation_1h x2.0, cache_read x0.1, output x5). 5m vs 1h is split from usage.cache_creation when the API breakdown is present; otherwise all cache_creation falls into 5m.",
|
|
232
246
|
"estimatedCostUsd": "USD cost using public list pricing for the model recorded in the session. cliWorkers covers Codex/Gemini CLI calls billed under those providers.",
|
|
233
247
|
},
|
|
234
248
|
}
|
|
@@ -1,31 +1,131 @@
|
|
|
1
|
-
"""Public list pricing tables and per-provider cost helpers.
|
|
1
|
+
"""Public list pricing tables and per-provider cost helpers.
|
|
2
|
+
|
|
3
|
+
Pricing is matched by substring against the model id recorded in the session
|
|
4
|
+
transcript, so keys must reflect the *actual* model id form emitted by each
|
|
5
|
+
provider:
|
|
6
|
+
|
|
7
|
+
* Anthropic — `claude-opus-4-*`, `claude-sonnet-4-*`, `claude-haiku-4-5-*`,
|
|
8
|
+
`claude-3-5-sonnet-*`, `claude-3-5-haiku-*`, `claude-3-opus-*`,
|
|
9
|
+
`claude-3-haiku-*`.
|
|
10
|
+
* OpenAI / Codex — `gpt-5*`, `gpt-4o*`, `gpt-4*`.
|
|
11
|
+
* Google / Gemini — `gemini-2.5-pro*`, `gemini-2.5-flash*`, `gemini-2.0-flash*`.
|
|
12
|
+
|
|
13
|
+
Insertion order is the match order, so list more specific keys first. Update
|
|
14
|
+
when providers change list pricing.
|
|
15
|
+
|
|
16
|
+
Sources (last verified 2026-05-17, public list prices, USD per 1M tokens):
|
|
17
|
+
* Anthropic: https://www.anthropic.com/pricing
|
|
18
|
+
* OpenAI: https://openai.com/api/pricing
|
|
19
|
+
* Google: https://ai.google.dev/gemini-api/docs/pricing
|
|
20
|
+
"""
|
|
2
21
|
from __future__ import annotations
|
|
3
22
|
|
|
4
23
|
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
24
|
+
# Anthropic billing ratios relative to base input: cache_creation (5m) = 1.25x,
|
|
25
|
+
# cache_creation (1h) = 2x, cache_read = 0.1x, output = 5x. The CLAUDE_PRICING
|
|
26
|
+
# entries below carry the 5m tier; the 1h price is derived as base_input * 2x
|
|
27
|
+
# at call time so the table stays compact.
|
|
8
28
|
CLAUDE_PRICING = {
|
|
9
|
-
# model substring -> (input,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
# model substring -> (input, cache_creation_5m, cache_read, output) USD/1M.
|
|
30
|
+
#
|
|
31
|
+
# Order matters — list more specific keys (e.g. `opus-4-7`, `3-7-sonnet`)
|
|
32
|
+
# before the family fallbacks (`opus-4`, `3-5-sonnet`).
|
|
33
|
+
#
|
|
34
|
+
# For the newer 4.x point releases (Opus 4.7, Sonnet 4.6, Haiku 4.5),
|
|
35
|
+
# Anthropic's public price page only lists input/output. Cache-write and
|
|
36
|
+
# cache-read are filled in using Anthropic's published billing ratios
|
|
37
|
+
# (5m cache_creation = 1.25x input, cache_read = 0.1x input), which have
|
|
38
|
+
# been consistent across the Claude 3 / 4 families.
|
|
39
|
+
|
|
40
|
+
# Claude 3 series (legacy).
|
|
41
|
+
"3-7-sonnet": (3.0, 3.75, 0.30, 15.0), # Sonnet 3.7
|
|
42
|
+
"3-5-sonnet": (3.0, 3.75, 0.30, 15.0), # Sonnet 3.5
|
|
43
|
+
"3-5-haiku": (0.80, 1.0, 0.08, 4.0), # Haiku 3.5
|
|
44
|
+
"3-opus": (15.0, 18.75, 1.50, 75.0), # Opus 3
|
|
45
|
+
"3-sonnet": (3.0, 3.75, 0.30, 15.0), # legacy 3 Sonnet
|
|
46
|
+
"3-haiku": (0.25, 0.30, 0.03, 1.25), # Haiku 3
|
|
47
|
+
|
|
48
|
+
# Claude 4 point releases (explicit so future divergence is easy to see).
|
|
49
|
+
"opus-4-7": (5.0, 6.25, 0.50, 25.0), # Opus 4.7 (cache prices derived from ratios)
|
|
50
|
+
"sonnet-4-6": (3.0, 3.75, 0.30, 15.0), # Sonnet 4.6 (cache prices derived from ratios)
|
|
51
|
+
"haiku-4-5": (1.0, 1.25, 0.10, 5.0), # Haiku 4.5 (cache prices derived from ratios)
|
|
52
|
+
|
|
53
|
+
# Claude 4 family fallbacks (Opus 4 / Sonnet 4 / Haiku 4 base).
|
|
54
|
+
"opus-4": (15.0, 18.75, 1.50, 75.0),
|
|
55
|
+
"sonnet-4": (3.0, 3.75, 0.30, 15.0),
|
|
56
|
+
"haiku-4": (1.0, 1.25, 0.10, 5.0),
|
|
16
57
|
}
|
|
17
58
|
|
|
59
|
+
# Anthropic 1h ephemeral cache_creation multiplier on the base input rate.
|
|
60
|
+
CLAUDE_CACHE_CREATE_1H_MULT = 2.0
|
|
61
|
+
|
|
18
62
|
CODEX_PRICING = {
|
|
19
|
-
# model substring -> (input USD/1M, cached_input USD/1M, output USD/1M)
|
|
20
|
-
|
|
21
|
-
|
|
63
|
+
# model substring -> (input USD/1M, cached_input USD/1M, output USD/1M).
|
|
64
|
+
# IMPORTANT: substring match order is insertion order. List the most
|
|
65
|
+
# specific keys first (e.g. `gpt-5-mini` before `gpt-5`, `o3-mini` before
|
|
66
|
+
# `o3`, `gpt-4o-mini` before `gpt-4o`, `gpt-4o` before the legacy `gpt-4`).
|
|
67
|
+
# For models with no published cached-input rate (o1-pro, o3-pro), cached
|
|
68
|
+
# is set equal to input as a conservative no-discount default.
|
|
69
|
+
|
|
70
|
+
# GPT-5 series.
|
|
71
|
+
"gpt-5.5": (5.00, 0.50, 30.0),
|
|
72
|
+
"gpt-5.4-mini": (0.75, 0.075, 4.50),
|
|
73
|
+
"gpt-5.4": (2.50, 0.25, 15.0),
|
|
74
|
+
"gpt-5.2-pro": (21.0, 2.10, 168.0),
|
|
75
|
+
"gpt-5.2": (1.75, 0.175, 14.0),
|
|
76
|
+
"gpt-5.1": (1.25, 0.125, 10.0),
|
|
77
|
+
"gpt-5-mini": (0.25, 0.025, 2.00),
|
|
78
|
+
"gpt-5-nano": (0.05, 0.005, 0.40),
|
|
79
|
+
"gpt-5": (1.25, 0.125, 10.0), # base GPT-5 (also matches gpt-5-codex)
|
|
80
|
+
|
|
81
|
+
# O-series reasoning models.
|
|
82
|
+
"o1-pro": (150.0, 150.0, 600.0), # no cached rate published
|
|
83
|
+
"o3-pro": (20.0, 20.0, 80.0), # no cached rate published
|
|
84
|
+
"o4-mini": (1.10, 0.275, 4.40),
|
|
85
|
+
"o3-mini": (1.10, 0.275, 4.40),
|
|
86
|
+
"o1": (15.0, 7.50, 60.0),
|
|
87
|
+
"o3": (2.00, 1.00, 8.00),
|
|
88
|
+
|
|
89
|
+
# GPT-4 series.
|
|
90
|
+
"gpt-4.1-nano": (0.10, 0.01, 0.40),
|
|
91
|
+
"gpt-4.1-mini": (0.40, 0.04, 1.60),
|
|
92
|
+
"gpt-4.1": (2.00, 0.20, 8.00),
|
|
93
|
+
"gpt-4o-mini": (0.15, 0.075, 0.60),
|
|
94
|
+
"gpt-4o": (2.50, 1.25, 10.0),
|
|
95
|
+
"gpt-4": (2.50, 0.625, 10.0), # legacy gpt-4 fallback
|
|
22
96
|
}
|
|
23
97
|
|
|
24
98
|
GEMINI_PRICING = {
|
|
25
|
-
# model substring -> (input USD/1M, output USD/1M)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
99
|
+
# model substring -> (input USD/1M, output USD/1M).
|
|
100
|
+
#
|
|
101
|
+
# Cached-input prices exist for some models but are not separately priced
|
|
102
|
+
# here because the Gemini transcript collector does not yet record cached
|
|
103
|
+
# input tokens. Models with two-tier context pricing (Gemini 2.5 Pro,
|
|
104
|
+
# Gemini 3.1 Pro) are charged at the ≤200K rate; runs above 200K input
|
|
105
|
+
# will be slightly undercounted.
|
|
106
|
+
#
|
|
107
|
+
# Both dotted (`gemini-3.1-pro`) and hyphenated (`gemini-3-1-pro`) id
|
|
108
|
+
# forms appear in the wild, so include both for the new 3.x families.
|
|
109
|
+
|
|
110
|
+
# Gemini 3 series (preview).
|
|
111
|
+
"3.1-pro": (2.00, 12.0),
|
|
112
|
+
"3-1-pro": (2.00, 12.0),
|
|
113
|
+
"3-flash": (0.50, 3.00),
|
|
114
|
+
|
|
115
|
+
# Gemini 2.5 series.
|
|
116
|
+
"2.5-flash-lite": (0.10, 0.40),
|
|
117
|
+
"2.5-flash": (0.30, 2.50),
|
|
118
|
+
"2.5-pro": (1.25, 10.0),
|
|
119
|
+
|
|
120
|
+
# Gemini 2.0 series.
|
|
121
|
+
"2.0-flash-lite": (0.075, 0.30),
|
|
122
|
+
"2.0-flash": (0.10, 0.40),
|
|
123
|
+
|
|
124
|
+
# Fallbacks for unspecified family names.
|
|
125
|
+
"flash-lite": (0.10, 0.40), # assume 2.5 Flash-Lite
|
|
126
|
+
"pro": (1.25, 10.0), # assume 2.5 Pro
|
|
127
|
+
"flash": (0.30, 2.50), # assume 2.5 Flash
|
|
128
|
+
"auto": (1.25, 10.0), # treat unknown/auto as 2.5 Pro
|
|
29
129
|
}
|
|
30
130
|
|
|
31
131
|
|
|
@@ -39,17 +139,53 @@ def _match_pricing(model: str | None, table: dict) -> tuple | None:
|
|
|
39
139
|
return None
|
|
40
140
|
|
|
41
141
|
|
|
42
|
-
def claude_billable_equivalent(
|
|
43
|
-
|
|
44
|
-
|
|
142
|
+
def claude_billable_equivalent(
|
|
143
|
+
input_t: int,
|
|
144
|
+
cache_create_t: int,
|
|
145
|
+
cache_read_t: int,
|
|
146
|
+
output_t: int,
|
|
147
|
+
cache_create_1h_t: int = 0,
|
|
148
|
+
) -> int:
|
|
149
|
+
"""Sum normalized to base-input units.
|
|
45
150
|
|
|
151
|
+
Ratios: cache_creation_5m=1.25x, cache_creation_1h=2x, cache_read=0.1x,
|
|
152
|
+
output=5x. `cache_create_t` is the total cache_creation tokens; pass the
|
|
153
|
+
1h portion separately via `cache_create_1h_t` so the 5m vs 1h tiers are
|
|
154
|
+
weighted correctly (the 5m portion is the difference).
|
|
155
|
+
"""
|
|
156
|
+
cc_1h = max(0, cache_create_1h_t)
|
|
157
|
+
cc_5m = max(0, cache_create_t - cc_1h)
|
|
158
|
+
return int(round(
|
|
159
|
+
input_t
|
|
160
|
+
+ 1.25 * cc_5m
|
|
161
|
+
+ CLAUDE_CACHE_CREATE_1H_MULT * cc_1h
|
|
162
|
+
+ 0.1 * cache_read_t
|
|
163
|
+
+ 5.0 * output_t
|
|
164
|
+
))
|
|
46
165
|
|
|
47
|
-
|
|
166
|
+
|
|
167
|
+
def claude_cost_usd(
|
|
168
|
+
model: str | None,
|
|
169
|
+
input_t: int,
|
|
170
|
+
cache_create_t: int,
|
|
171
|
+
cache_read_t: int,
|
|
172
|
+
output_t: int,
|
|
173
|
+
cache_create_1h_t: int = 0,
|
|
174
|
+
) -> float | None:
|
|
48
175
|
p = _match_pricing(model, CLAUDE_PRICING)
|
|
49
176
|
if p is None:
|
|
50
177
|
return None
|
|
51
178
|
pi, pcc, pcr, po = p
|
|
52
|
-
|
|
179
|
+
cc_1h = max(0, cache_create_1h_t)
|
|
180
|
+
cc_5m = max(0, cache_create_t - cc_1h)
|
|
181
|
+
pcc_1h = pi * CLAUDE_CACHE_CREATE_1H_MULT
|
|
182
|
+
return round((
|
|
183
|
+
input_t * pi
|
|
184
|
+
+ cc_5m * pcc
|
|
185
|
+
+ cc_1h * pcc_1h
|
|
186
|
+
+ cache_read_t * pcr
|
|
187
|
+
+ output_t * po
|
|
188
|
+
) / 1_000_000, 4)
|
|
53
189
|
|
|
54
190
|
|
|
55
191
|
def codex_cost_usd(model: str | None, input_t: int, cached_input_t: int, output_t: int) -> float | None:
|
|
@@ -68,4 +204,3 @@ def gemini_cost_usd(model: str | None, input_t: int, output_t: int) -> float | N
|
|
|
68
204
|
return None
|
|
69
205
|
pi, po = p
|
|
70
206
|
return round((input_t * pi + output_t * po) / 1_000_000, 4)
|
|
71
|
-
|