dh-cli 0.17.0__tar.gz → 0.17.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dh_cli-0.17.0 → dh_cli-0.17.1}/PKG-INFO +1 -1
- {dh_cli-0.17.0 → dh_cli-0.17.1}/pyproject.toml +1 -1
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/bedrock/commands.py +10 -5
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/bedrock/cost_report.py +17 -3
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/bedrock/pricing.yaml +23 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/test_build_report.py +21 -13
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/test_cli_exit_codes.py +14 -18
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/test_resolve_base_model.py +35 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/.gitignore +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/LICENSE +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/README.md +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/_identity.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/aws_batch.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/boltz.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/cancel.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/clean.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/cost.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/disco.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/embed_esmc.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/embed_t5.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/equilibrator.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/esmfold2.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/finalize.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/list_jobs.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/local.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/logs.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/orca.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/protmpnn.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/retry.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/rfd3.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/status.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/submit.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/commands/wait_for.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/cost_report.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/fasta_utils.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/h5_utils.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/job_id.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/manifest.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/batch/s3_transport.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/bedrock/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/cloud_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/codeartifact.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/engines_studios/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/engines_studios/api_client.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/engines_studios/auth.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/engines_studios/engine_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/engines_studios/progress.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/engines_studios/retry.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/engines_studios/ssh_config.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/engines_studios/studio_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/engines_studios/studio_resolve.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/github_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/hz/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/hz/auth.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/hz/deploy.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/hz/local.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/hz/mcp_server.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/hz/test.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/hz/tf.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/hz/users.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/main.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/sweep.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/src/dh_cli/warehouse.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/cur_fixtures.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_aws_batch_resources.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_batch_cost_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_batch_cost_filters.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_batch_cost_reader.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_batch_cost_reconcile.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_batch_cost_report.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_image_override.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_import_without_pandas.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_no_train_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_submit_cpu_only.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_submit_image_validation.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_submit_job_tags.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_submit_merge.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/batch/test_submit_mlflow_env.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/conftest.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/fixtures/A_cache_write.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/fixtures/B_cache_read.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/fixtures/C_plain.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/fixtures/E_service_role.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/test_classify_arn.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/test_cost_calc.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/test_cost_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/test_cur_reconciliation.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/test_key_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/test_render_formats.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/test_rotate.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/bedrock/test_s3_walker.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/engines_studios/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/engines_studios/test_engine_retry_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/engines_studios/test_retry.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/engines_studios/test_ssh_config.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/engines_studios/test_studio_resolution_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/engines_studios/test_studio_resolve.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/github/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/github/conftest.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/github/test_identity.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/github/test_login.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/github/test_login_error_paths.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/github/test_login_security.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/github/test_logout.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/github/test_rotate.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/github/test_status.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/hz/live_smoke.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/hz/test_auth.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/hz/test_init.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/hz/test_mcp_server.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/hz/test_suites.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/hz/test_users.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_aws_status.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_cloud_gcp.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_disco_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_disco_jobdef.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_embed_esmc_cli.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_equilibrator_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_esmfold2_cli.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_finalize_boltz_tar.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_finalize_disco.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_finalize_disco_dispatch.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_finalize_equilibrator.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_finalize_esmfold2.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_finalize_protmpnn.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_finalize_rfd3.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_rfd3_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.1}/tests/test_sweep.py +0 -0
|
@@ -389,8 +389,9 @@ def bedrock_cost(
|
|
|
389
389
|
dh bedrock cost --format csv | csvlook # further analysis
|
|
390
390
|
|
|
391
391
|
Exit codes:
|
|
392
|
-
0 success (within 10% of Cost Explorer, or --no-reconcile)
|
|
393
|
-
|
|
392
|
+
0 success (within 10% of Cost Explorer, or --no-reconcile); unpriced
|
|
393
|
+
models are warned about on stderr and counted at $0
|
|
394
|
+
1 bad pricing file or S3 read error
|
|
394
395
|
2 reconciliation drift > 10% AND absolute gap >= $1
|
|
395
396
|
"""
|
|
396
397
|
# Defaults use UTC dates to match the S3 day-partition layout
|
|
@@ -445,13 +446,17 @@ def bedrock_cost(
|
|
|
445
446
|
if cr.classify_arn(rec.get("identity", {}).get("arn", "")).principal_name == my_handle
|
|
446
447
|
)
|
|
447
448
|
report = cr.build_report(records, pricing, group_by=group_by)
|
|
448
|
-
except cr.UnknownModel as exc:
|
|
449
|
-
click.echo(f"error: {exc}", err=True)
|
|
450
|
-
sys.exit(1)
|
|
451
449
|
except Exception as exc:
|
|
452
450
|
click.echo(f"error: failed to process logs: {exc}", err=True)
|
|
453
451
|
sys.exit(1)
|
|
454
452
|
|
|
453
|
+
for model_id, calls in sorted(report.unpriced.items(), key=lambda kv: -kv[1]):
|
|
454
|
+
click.echo(
|
|
455
|
+
f"warning: no pricing for {model_id} ({calls} call{'s' if calls != 1 else ''}); "
|
|
456
|
+
"counted at $0 under an UNPRICED: row. Add it to pricing.yaml.",
|
|
457
|
+
err=True,
|
|
458
|
+
)
|
|
459
|
+
|
|
455
460
|
# Header is only useful for humans — skip in csv mode so the output
|
|
456
461
|
# is a clean csv stream that can be piped.
|
|
457
462
|
if output_format != "csv":
|
|
@@ -61,6 +61,8 @@ class ReportRow:
|
|
|
61
61
|
@dataclass
|
|
62
62
|
class Report:
|
|
63
63
|
rows: list[ReportRow] = field(default_factory=list)
|
|
64
|
+
# modelId -> call count for records whose model has no pricing entry.
|
|
65
|
+
unpriced: dict[str, int] = field(default_factory=dict)
|
|
64
66
|
|
|
65
67
|
|
|
66
68
|
@dataclass(frozen=True)
|
|
@@ -115,6 +117,10 @@ _BASE_MODELS = (
|
|
|
115
117
|
"haiku-4-5",
|
|
116
118
|
"fable-5",
|
|
117
119
|
"fable-5-1",
|
|
120
|
+
"gpt-6-astra",
|
|
121
|
+
"gpt-5.6-sol",
|
|
122
|
+
"gpt-5.6-terra",
|
|
123
|
+
"gpt-5.6-luna",
|
|
118
124
|
)
|
|
119
125
|
|
|
120
126
|
|
|
@@ -185,6 +191,7 @@ def build_report(
|
|
|
185
191
|
if group_by not in _VALID_GROUP_BY:
|
|
186
192
|
raise ValueError(f"group_by={group_by!r} is not one of {sorted(_VALID_GROUP_BY)}")
|
|
187
193
|
agg: dict[tuple, dict[str, Any]] = {}
|
|
194
|
+
unpriced: dict[str, int] = {}
|
|
188
195
|
for rec in records:
|
|
189
196
|
# Bedrock emits records for failed validations / throttles with
|
|
190
197
|
# `errorCode` set and no `input` / `output` fields. No tokens
|
|
@@ -193,8 +200,15 @@ def build_report(
|
|
|
193
200
|
continue
|
|
194
201
|
arn = rec.get("identity", {}).get("arn", "")
|
|
195
202
|
principal = classify_arn(arn)
|
|
196
|
-
|
|
197
|
-
|
|
203
|
+
# An unpriced model gets its own $0 `UNPRICED:` row and is counted in
|
|
204
|
+
# `Report.unpriced`, rather than aborting the whole report.
|
|
205
|
+
try:
|
|
206
|
+
base = resolve_base_model(rec["modelId"])
|
|
207
|
+
rec_cost = cost_of(rec, pricing)
|
|
208
|
+
except UnknownModel:
|
|
209
|
+
base = f"UNPRICED:{rec['modelId']}"
|
|
210
|
+
rec_cost = 0.0
|
|
211
|
+
unpriced[rec["modelId"]] = unpriced.get(rec["modelId"], 0) + 1
|
|
198
212
|
if group_by == "user+model":
|
|
199
213
|
key = (principal.principal_type, principal.principal_name, base)
|
|
200
214
|
elif group_by == "user":
|
|
@@ -236,7 +250,7 @@ def build_report(
|
|
|
236
250
|
for (ptype, pname, model), s in agg.items()
|
|
237
251
|
]
|
|
238
252
|
rows.sort(key=lambda r: r.estimated_cost_usd, reverse=True)
|
|
239
|
-
return Report(rows=rows)
|
|
253
|
+
return Report(rows=rows, unpriced=unpriced)
|
|
240
254
|
|
|
241
255
|
|
|
242
256
|
_COLUMNS = (
|
|
@@ -57,3 +57,26 @@ models:
|
|
|
57
57
|
output_per_1m: 4.00
|
|
58
58
|
cache_write_per_1m: 1.00
|
|
59
59
|
cache_read_per_1m: 0.08
|
|
60
|
+
# OpenAI models reached via `bedrock-runtime` (these write invocation logs;
|
|
61
|
+
# mantle traffic does not). Global-profile rates as billed in Cost Explorer,
|
|
62
|
+
# Sep 2026; `us.`/in-region calls bill ~10% higher, same as Anthropic.
|
|
63
|
+
gpt-6-astra:
|
|
64
|
+
input_per_1m: 10.00
|
|
65
|
+
output_per_1m: 50.00
|
|
66
|
+
cache_write_per_1m: 12.50
|
|
67
|
+
cache_read_per_1m: 1.00
|
|
68
|
+
gpt-5.6-sol:
|
|
69
|
+
input_per_1m: 4.00
|
|
70
|
+
output_per_1m: 20.00
|
|
71
|
+
cache_write_per_1m: 5.00
|
|
72
|
+
cache_read_per_1m: 0.40
|
|
73
|
+
gpt-5.6-terra:
|
|
74
|
+
input_per_1m: 2.00
|
|
75
|
+
output_per_1m: 12.00
|
|
76
|
+
cache_write_per_1m: 2.50
|
|
77
|
+
cache_read_per_1m: 0.20
|
|
78
|
+
gpt-5.6-luna:
|
|
79
|
+
input_per_1m: 0.20
|
|
80
|
+
output_per_1m: 1.20
|
|
81
|
+
cache_write_per_1m: 0.25
|
|
82
|
+
cache_read_per_1m: 0.02
|
|
@@ -100,21 +100,29 @@ def test_aggregation_per_principal_and_model(pricing):
|
|
|
100
100
|
assert haiku_row.input_tokens == 1000
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
def
|
|
104
|
-
"""An unrecognized modelId
|
|
105
|
-
|
|
103
|
+
def test_unknown_model_is_reported_as_unpriced_row(load_fixture, pricing):
|
|
104
|
+
"""An unrecognized modelId gets its own visibly-unpriced row.
|
|
105
|
+
|
|
106
|
+
Contract change (2026-09-25): this used to raise UnknownModel, which let
|
|
107
|
+
one unpriced model abort the whole report — four times, most recently a
|
|
108
|
+
handful of `us.openai.gpt-5.6-luna` calls blanking out ~$40k/month of
|
|
109
|
+
attribution. The unknown model's tokens stay visible under an
|
|
110
|
+
`UNPRICED:` label and the rest of the report is still priced; it is
|
|
111
|
+
never folded into a real model's row.
|
|
112
|
+
"""
|
|
113
|
+
from dh_cli.bedrock.cost_report import build_report
|
|
106
114
|
|
|
107
|
-
records = [
|
|
108
|
-
{
|
|
109
|
-
"modelId": "global.anthropic.claude-someshinynew-99-v42",
|
|
110
|
-
"input": {"inputTokenCount": 10, "cacheReadInputTokenCount": 0, "cacheWriteInputTokenCount": 0},
|
|
111
|
-
"output": {"outputTokenCount": 10},
|
|
112
|
-
"identity": {"arn": "arn:aws:iam::074735440724:user/dev-cursor-dma"},
|
|
113
|
-
}
|
|
114
|
-
]
|
|
115
|
+
records = [load_fixture("G_unknown_model.json"), load_fixture("A_cache_write.json")]
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
report = build_report(records, pricing, group_by="model")
|
|
118
|
+
rows = {r.model: r for r in report.rows}
|
|
119
|
+
|
|
120
|
+
unpriced = rows["UNPRICED:global.anthropic.claude-someshinynew-99-v42"]
|
|
121
|
+
assert unpriced.estimated_cost_usd == 0.0
|
|
122
|
+
assert unpriced.invocations == 1
|
|
123
|
+
assert unpriced.input_tokens == 10
|
|
124
|
+
assert unpriced.output_tokens == 10
|
|
125
|
+
assert rows["opus-4-7"].estimated_cost_usd == pytest.approx(0.0255)
|
|
118
126
|
|
|
119
127
|
|
|
120
128
|
def test_error_records_are_skipped(load_fixture, pricing):
|
|
@@ -48,24 +48,20 @@ def test_cost_exit_0_on_success(load_fixture):
|
|
|
48
48
|
assert result.exit_code == 0, result.output
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
):
|
|
59
|
-
walk.return_value = iter([load_fixture("G_unknown_model.json")])
|
|
60
|
-
result = _invoke(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"2026-04-17",
|
|
66
|
-
]
|
|
67
|
-
)
|
|
68
|
-
assert result.exit_code == 1, result.output
|
|
51
|
+
def test_cost_unknown_model_warns_and_still_reports(load_fixture):
|
|
52
|
+
"""An unpriced model must not abort the report (it used to exit 1).
|
|
53
|
+
|
|
54
|
+
The known-model rows still print, and stderr names the unpriced model
|
|
55
|
+
and its call count so the gap can't pass unnoticed. Any dollar gap it
|
|
56
|
+
causes is still caught by the Cost Explorer reconcile (exit 2).
|
|
57
|
+
"""
|
|
58
|
+
with patch("dh_cli.bedrock.cost_report.walk_logs") as walk:
|
|
59
|
+
walk.return_value = iter([load_fixture("G_unknown_model.json"), load_fixture("A_cache_write.json")])
|
|
60
|
+
result = _invoke(["--start", "2026-04-17", "--end", "2026-04-17", "--no-reconcile", "--group-by", "model"])
|
|
61
|
+
assert result.exit_code == 0, result.output
|
|
62
|
+
assert "opus-4-7" in result.stdout
|
|
63
|
+
assert "global.anthropic.claude-someshinynew-99-v42" in result.stderr
|
|
64
|
+
assert "1 call" in result.stderr
|
|
69
65
|
|
|
70
66
|
|
|
71
67
|
def test_cost_exit_2_on_drift(load_fixture):
|
|
@@ -52,6 +52,21 @@ import pytest
|
|
|
52
52
|
("anthropic.claude-opus-5-5", "opus-5-5"),
|
|
53
53
|
("us.anthropic.claude-opus-5-5", "opus-5-5"),
|
|
54
54
|
("global.anthropic.claude-opus-5-5", "opus-5-5"),
|
|
55
|
+
# OpenAI models on `bedrock-runtime` do write invocation logs (only
|
|
56
|
+
# mantle traffic doesn't). Seen in the dev bucket 2026-09, in both the
|
|
57
|
+
# bare and inference-profile-ARN shapes.
|
|
58
|
+
("us.openai.gpt-5.6-luna", "gpt-5.6-luna"),
|
|
59
|
+
(
|
|
60
|
+
"arn:aws:bedrock:us-east-1:074735440724:inference-profile/global.openai.gpt-5.6-luna",
|
|
61
|
+
"gpt-5.6-luna",
|
|
62
|
+
),
|
|
63
|
+
("openai.gpt-5.6-sol", "gpt-5.6-sol"),
|
|
64
|
+
("global.openai.gpt-5.6-terra", "gpt-5.6-terra"),
|
|
65
|
+
("global.openai.gpt-6-astra", "gpt-6-astra"),
|
|
66
|
+
(
|
|
67
|
+
"arn:aws:bedrock:us-east-1:074735440724:inference-profile/global.openai.gpt-6-astra",
|
|
68
|
+
"gpt-6-astra",
|
|
69
|
+
),
|
|
55
70
|
],
|
|
56
71
|
)
|
|
57
72
|
def test_resolve_base_model_known(model_id: str, expected_base: str):
|
|
@@ -117,6 +132,26 @@ def test_opus_55_is_priced_below_opus_5():
|
|
|
117
132
|
assert pricing["opus-5-5"][rate] < pricing["opus-5"][rate]
|
|
118
133
|
|
|
119
134
|
|
|
135
|
+
@pytest.mark.parametrize(
|
|
136
|
+
"base, rates",
|
|
137
|
+
[
|
|
138
|
+
# Global-profile rates as billed in Cost Explorer, Sep 2026
|
|
139
|
+
# (`USE1-MP:USE1_*_global_standard` unit cost). Sol's regional rate
|
|
140
|
+
# fell from $5.50/$33 in Aug to $4.40/$22 in Sep; Sep is current.
|
|
141
|
+
("gpt-6-astra", (10.00, 50.00, 12.50, 1.00)),
|
|
142
|
+
("gpt-5.6-sol", (4.00, 20.00, 5.00, 0.40)),
|
|
143
|
+
("gpt-5.6-terra", (2.00, 12.00, 2.50, 0.20)),
|
|
144
|
+
("gpt-5.6-luna", (0.20, 1.20, 0.25, 0.02)),
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
def test_openai_runtime_models_are_priced(base, rates):
|
|
148
|
+
from dh_cli.bedrock.cost_report import default_pricing_path, load_pricing
|
|
149
|
+
|
|
150
|
+
pricing = load_pricing(default_pricing_path())
|
|
151
|
+
keys = ("input_per_1m", "output_per_1m", "cache_write_per_1m", "cache_read_per_1m")
|
|
152
|
+
assert tuple(pricing[base][k] for k in keys) == rates
|
|
153
|
+
|
|
154
|
+
|
|
120
155
|
def test_resolve_base_model_unknown_raises():
|
|
121
156
|
from dh_cli.bedrock.cost_report import UnknownModel, resolve_base_model
|
|
122
157
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|