dh-cli 0.17.0__tar.gz → 0.17.2__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.2}/PKG-INFO +1 -1
- {dh_cli-0.17.0 → dh_cli-0.17.2}/pyproject.toml +1 -1
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/bedrock/commands.py +10 -5
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/bedrock/cost_report.py +19 -3
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/bedrock/pricing.yaml +35 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_build_report.py +21 -13
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_cli_exit_codes.py +14 -18
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_resolve_base_model.py +47 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/.gitignore +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/LICENSE +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/README.md +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/_identity.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/aws_batch.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/boltz.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/cancel.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/clean.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/cost.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/disco.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/embed_esmc.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/embed_t5.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/equilibrator.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/esmfold2.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/finalize.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/list_jobs.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/local.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/logs.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/orca.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/protmpnn.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/retry.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/rfd3.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/status.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/submit.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/wait_for.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/cost_report.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/fasta_utils.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/h5_utils.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/job_id.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/manifest.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/s3_transport.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/bedrock/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/cloud_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/codeartifact.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/api_client.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/auth.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/engine_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/progress.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/retry.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/ssh_config.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/studio_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/studio_resolve.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/github_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/auth.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/deploy.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/local.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/mcp_server.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/test.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/tf.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/users.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/main.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/sweep.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/warehouse.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/cur_fixtures.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_aws_batch_resources.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_batch_cost_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_batch_cost_filters.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_batch_cost_reader.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_batch_cost_reconcile.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_batch_cost_report.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_image_override.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_import_without_pandas.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_no_train_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_submit_cpu_only.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_submit_image_validation.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_submit_job_tags.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_submit_merge.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_submit_mlflow_env.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/conftest.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/A_cache_write.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/B_cache_read.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/C_plain.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/E_service_role.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_classify_arn.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_cost_calc.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_cost_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_cur_reconciliation.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_key_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_render_formats.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_rotate.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_s3_walker.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/test_engine_retry_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/test_retry.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/test_ssh_config.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/test_studio_resolution_commands.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/test_studio_resolve.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/__init__.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/conftest.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_identity.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_login.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_login_error_paths.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_login_security.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_logout.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_rotate.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_status.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/live_smoke.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/test_auth.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/test_init.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/test_mcp_server.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/test_suites.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/test_users.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_aws_status.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_cloud_gcp.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_disco_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_disco_jobdef.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_embed_esmc_cli.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_equilibrator_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_esmfold2_cli.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_boltz_tar.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_disco.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_disco_dispatch.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_equilibrator.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_esmfold2.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_protmpnn.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_rfd3.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_rfd3_command.py +0 -0
- {dh_cli-0.17.0 → dh_cli-0.17.2}/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,12 @@ _BASE_MODELS = (
|
|
|
115
117
|
"haiku-4-5",
|
|
116
118
|
"fable-5",
|
|
117
119
|
"fable-5-1",
|
|
120
|
+
"gpt-6-astra",
|
|
121
|
+
"gpt-6-sol",
|
|
122
|
+
"gpt-6-luna",
|
|
123
|
+
"gpt-5.6-sol",
|
|
124
|
+
"gpt-5.6-terra",
|
|
125
|
+
"gpt-5.6-luna",
|
|
118
126
|
)
|
|
119
127
|
|
|
120
128
|
|
|
@@ -185,6 +193,7 @@ def build_report(
|
|
|
185
193
|
if group_by not in _VALID_GROUP_BY:
|
|
186
194
|
raise ValueError(f"group_by={group_by!r} is not one of {sorted(_VALID_GROUP_BY)}")
|
|
187
195
|
agg: dict[tuple, dict[str, Any]] = {}
|
|
196
|
+
unpriced: dict[str, int] = {}
|
|
188
197
|
for rec in records:
|
|
189
198
|
# Bedrock emits records for failed validations / throttles with
|
|
190
199
|
# `errorCode` set and no `input` / `output` fields. No tokens
|
|
@@ -193,8 +202,15 @@ def build_report(
|
|
|
193
202
|
continue
|
|
194
203
|
arn = rec.get("identity", {}).get("arn", "")
|
|
195
204
|
principal = classify_arn(arn)
|
|
196
|
-
|
|
197
|
-
|
|
205
|
+
# An unpriced model gets its own $0 `UNPRICED:` row and is counted in
|
|
206
|
+
# `Report.unpriced`, rather than aborting the whole report.
|
|
207
|
+
try:
|
|
208
|
+
base = resolve_base_model(rec["modelId"])
|
|
209
|
+
rec_cost = cost_of(rec, pricing)
|
|
210
|
+
except UnknownModel:
|
|
211
|
+
base = f"UNPRICED:{rec['modelId']}"
|
|
212
|
+
rec_cost = 0.0
|
|
213
|
+
unpriced[rec["modelId"]] = unpriced.get(rec["modelId"], 0) + 1
|
|
198
214
|
if group_by == "user+model":
|
|
199
215
|
key = (principal.principal_type, principal.principal_name, base)
|
|
200
216
|
elif group_by == "user":
|
|
@@ -236,7 +252,7 @@ def build_report(
|
|
|
236
252
|
for (ptype, pname, model), s in agg.items()
|
|
237
253
|
]
|
|
238
254
|
rows.sort(key=lambda r: r.estimated_cost_usd, reverse=True)
|
|
239
|
-
return Report(rows=rows)
|
|
255
|
+
return Report(rows=rows, unpriced=unpriced)
|
|
240
256
|
|
|
241
257
|
|
|
242
258
|
_COLUMNS = (
|
|
@@ -57,3 +57,38 @@ 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-6 Sol/Luna: Marketplace rate card `*_global_standard`, 2026-09-25;
|
|
69
|
+
# cache write is the 30-minute tier.
|
|
70
|
+
gpt-6-sol:
|
|
71
|
+
input_per_1m: 2.00
|
|
72
|
+
output_per_1m: 10.00
|
|
73
|
+
cache_write_per_1m: 2.50
|
|
74
|
+
cache_read_per_1m: 0.20
|
|
75
|
+
gpt-6-luna:
|
|
76
|
+
input_per_1m: 0.10
|
|
77
|
+
output_per_1m: 0.50
|
|
78
|
+
cache_write_per_1m: 0.125
|
|
79
|
+
cache_read_per_1m: 0.01
|
|
80
|
+
gpt-5.6-sol:
|
|
81
|
+
input_per_1m: 4.00
|
|
82
|
+
output_per_1m: 20.00
|
|
83
|
+
cache_write_per_1m: 5.00
|
|
84
|
+
cache_read_per_1m: 0.40
|
|
85
|
+
gpt-5.6-terra:
|
|
86
|
+
input_per_1m: 2.00
|
|
87
|
+
output_per_1m: 12.00
|
|
88
|
+
cache_write_per_1m: 2.50
|
|
89
|
+
cache_read_per_1m: 0.20
|
|
90
|
+
gpt-5.6-luna:
|
|
91
|
+
input_per_1m: 0.20
|
|
92
|
+
output_per_1m: 1.20
|
|
93
|
+
cache_write_per_1m: 0.25
|
|
94
|
+
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,29 @@ 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
|
+
),
|
|
70
|
+
("global.openai.gpt-6-sol", "gpt-6-sol"),
|
|
71
|
+
("us.openai.gpt-6-sol", "gpt-6-sol"),
|
|
72
|
+
("openai.gpt-6-sol", "gpt-6-sol"),
|
|
73
|
+
("global.openai.gpt-6-luna", "gpt-6-luna"),
|
|
74
|
+
(
|
|
75
|
+
"arn:aws:bedrock:us-east-1:074735440724:inference-profile/global.openai.gpt-6-luna",
|
|
76
|
+
"gpt-6-luna",
|
|
77
|
+
),
|
|
55
78
|
],
|
|
56
79
|
)
|
|
57
80
|
def test_resolve_base_model_known(model_id: str, expected_base: str):
|
|
@@ -117,6 +140,30 @@ def test_opus_55_is_priced_below_opus_5():
|
|
|
117
140
|
assert pricing["opus-5-5"][rate] < pricing["opus-5"][rate]
|
|
118
141
|
|
|
119
142
|
|
|
143
|
+
@pytest.mark.parametrize(
|
|
144
|
+
"base, rates",
|
|
145
|
+
[
|
|
146
|
+
# Global-profile rates as billed in Cost Explorer, Sep 2026
|
|
147
|
+
# (`USE1-MP:USE1_*_global_standard` unit cost). Sol's regional rate
|
|
148
|
+
# fell from $5.50/$33 in Aug to $4.40/$22 in Sep; Sep is current.
|
|
149
|
+
("gpt-6-astra", (10.00, 50.00, 12.50, 1.00)),
|
|
150
|
+
# GPT-6 Sol/Luna: Marketplace rate card, `*_global_standard`,
|
|
151
|
+
# 2026-09-25 (cache write is the 30-minute tier).
|
|
152
|
+
("gpt-6-sol", (2.00, 10.00, 2.50, 0.20)),
|
|
153
|
+
("gpt-6-luna", (0.10, 0.50, 0.125, 0.01)),
|
|
154
|
+
("gpt-5.6-sol", (4.00, 20.00, 5.00, 0.40)),
|
|
155
|
+
("gpt-5.6-terra", (2.00, 12.00, 2.50, 0.20)),
|
|
156
|
+
("gpt-5.6-luna", (0.20, 1.20, 0.25, 0.02)),
|
|
157
|
+
],
|
|
158
|
+
)
|
|
159
|
+
def test_openai_runtime_models_are_priced(base, rates):
|
|
160
|
+
from dh_cli.bedrock.cost_report import default_pricing_path, load_pricing
|
|
161
|
+
|
|
162
|
+
pricing = load_pricing(default_pricing_path())
|
|
163
|
+
keys = ("input_per_1m", "output_per_1m", "cache_write_per_1m", "cache_read_per_1m")
|
|
164
|
+
assert tuple(pricing[base][k] for k in keys) == rates
|
|
165
|
+
|
|
166
|
+
|
|
120
167
|
def test_resolve_base_model_unknown_raises():
|
|
121
168
|
from dh_cli.bedrock.cost_report import UnknownModel, resolve_base_model
|
|
122
169
|
|
|
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
|