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.
Files changed (137) hide show
  1. {dh_cli-0.17.0 → dh_cli-0.17.2}/PKG-INFO +1 -1
  2. {dh_cli-0.17.0 → dh_cli-0.17.2}/pyproject.toml +1 -1
  3. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/bedrock/commands.py +10 -5
  4. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/bedrock/cost_report.py +19 -3
  5. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/bedrock/pricing.yaml +35 -0
  6. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_build_report.py +21 -13
  7. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_cli_exit_codes.py +14 -18
  8. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_resolve_base_model.py +47 -0
  9. {dh_cli-0.17.0 → dh_cli-0.17.2}/.gitignore +0 -0
  10. {dh_cli-0.17.0 → dh_cli-0.17.2}/LICENSE +0 -0
  11. {dh_cli-0.17.0 → dh_cli-0.17.2}/README.md +0 -0
  12. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/__init__.py +0 -0
  13. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/_identity.py +0 -0
  14. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/__init__.py +0 -0
  15. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/aws_batch.py +0 -0
  16. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/__init__.py +0 -0
  17. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/boltz.py +0 -0
  18. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/cancel.py +0 -0
  19. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/clean.py +0 -0
  20. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/cost.py +0 -0
  21. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/disco.py +0 -0
  22. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/embed_esmc.py +0 -0
  23. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/embed_t5.py +0 -0
  24. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/equilibrator.py +0 -0
  25. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/esmfold2.py +0 -0
  26. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/finalize.py +0 -0
  27. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/list_jobs.py +0 -0
  28. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/local.py +0 -0
  29. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/logs.py +0 -0
  30. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/orca.py +0 -0
  31. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/protmpnn.py +0 -0
  32. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
  33. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/retry.py +0 -0
  34. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/rfd3.py +0 -0
  35. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/status.py +0 -0
  36. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/submit.py +0 -0
  37. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/commands/wait_for.py +0 -0
  38. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/cost_report.py +0 -0
  39. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/fasta_utils.py +0 -0
  40. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/h5_utils.py +0 -0
  41. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/job_id.py +0 -0
  42. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/manifest.py +0 -0
  43. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/batch/s3_transport.py +0 -0
  44. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/bedrock/__init__.py +0 -0
  45. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/cloud_commands.py +0 -0
  46. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/codeartifact.py +0 -0
  47. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/__init__.py +0 -0
  48. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/api_client.py +0 -0
  49. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/auth.py +0 -0
  50. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/engine_commands.py +0 -0
  51. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/progress.py +0 -0
  52. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/retry.py +0 -0
  53. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/ssh_config.py +0 -0
  54. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/studio_commands.py +0 -0
  55. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/engines_studios/studio_resolve.py +0 -0
  56. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/github_commands.py +0 -0
  57. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/__init__.py +0 -0
  58. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/auth.py +0 -0
  59. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/deploy.py +0 -0
  60. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/local.py +0 -0
  61. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/mcp_server.py +0 -0
  62. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/test.py +0 -0
  63. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/tf.py +0 -0
  64. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/hz/users.py +0 -0
  65. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/main.py +0 -0
  66. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/sweep.py +0 -0
  67. {dh_cli-0.17.0 → dh_cli-0.17.2}/src/dh_cli/warehouse.py +0 -0
  68. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/__init__.py +0 -0
  69. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/cur_fixtures.py +0 -0
  70. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_aws_batch_resources.py +0 -0
  71. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_batch_cost_command.py +0 -0
  72. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_batch_cost_filters.py +0 -0
  73. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_batch_cost_reader.py +0 -0
  74. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_batch_cost_reconcile.py +0 -0
  75. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_batch_cost_report.py +0 -0
  76. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_image_override.py +0 -0
  77. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_import_without_pandas.py +0 -0
  78. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_no_train_command.py +0 -0
  79. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_submit_cpu_only.py +0 -0
  80. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_submit_image_validation.py +0 -0
  81. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_submit_job_tags.py +0 -0
  82. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_submit_merge.py +0 -0
  83. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/batch/test_submit_mlflow_env.py +0 -0
  84. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/conftest.py +0 -0
  85. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/A_cache_write.json +0 -0
  86. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/B_cache_read.json +0 -0
  87. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/C_plain.json +0 -0
  88. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
  89. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/E_service_role.json +0 -0
  90. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
  91. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
  92. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_classify_arn.py +0 -0
  93. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_cost_calc.py +0 -0
  94. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_cost_command.py +0 -0
  95. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_cur_reconciliation.py +0 -0
  96. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_key_command.py +0 -0
  97. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_render_formats.py +0 -0
  98. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_rotate.py +0 -0
  99. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/bedrock/test_s3_walker.py +0 -0
  100. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/__init__.py +0 -0
  101. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/test_engine_retry_commands.py +0 -0
  102. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/test_retry.py +0 -0
  103. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/test_ssh_config.py +0 -0
  104. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/test_studio_resolution_commands.py +0 -0
  105. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/engines_studios/test_studio_resolve.py +0 -0
  106. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/__init__.py +0 -0
  107. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/conftest.py +0 -0
  108. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
  109. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_identity.py +0 -0
  110. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_login.py +0 -0
  111. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_login_error_paths.py +0 -0
  112. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_login_security.py +0 -0
  113. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_logout.py +0 -0
  114. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_rotate.py +0 -0
  115. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/github/test_status.py +0 -0
  116. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/live_smoke.py +0 -0
  117. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/test_auth.py +0 -0
  118. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/test_init.py +0 -0
  119. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/test_mcp_server.py +0 -0
  120. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/test_suites.py +0 -0
  121. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/hz/test_users.py +0 -0
  122. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_aws_status.py +0 -0
  123. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_cloud_gcp.py +0 -0
  124. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_disco_command.py +0 -0
  125. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_disco_jobdef.py +0 -0
  126. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_embed_esmc_cli.py +0 -0
  127. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_equilibrator_command.py +0 -0
  128. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_esmfold2_cli.py +0 -0
  129. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_boltz_tar.py +0 -0
  130. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_disco.py +0 -0
  131. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_disco_dispatch.py +0 -0
  132. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_equilibrator.py +0 -0
  133. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_esmfold2.py +0 -0
  134. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_protmpnn.py +0 -0
  135. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_finalize_rfd3.py +0 -0
  136. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_rfd3_command.py +0 -0
  137. {dh_cli-0.17.0 → dh_cli-0.17.2}/tests/test_sweep.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dh-cli
3
- Version: 0.17.0
3
+ Version: 0.17.2
4
4
  Summary: Dayhoff Labs developer CLI
5
5
  Author-email: Dayhoff Labs <dev@dayhofflabs.com>
6
6
  License: # PolyForm Noncommercial License 1.0.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dh-cli"
7
- version = "0.17.0"
7
+ version = "0.17.2"
8
8
  description = "Dayhoff Labs developer CLI"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -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
- 1 unknown model ID, bad pricing file, or S3 read error
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
- base = resolve_base_model(rec["modelId"])
197
- rec_cost = cost_of(rec, pricing)
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 test_missing_pricing_raises(pricing):
104
- """An unrecognized modelId surfaces as UnknownModel, not silent 0."""
105
- from dh_cli.bedrock.cost_report import UnknownModel, build_report
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
- with pytest.raises(UnknownModel):
117
- build_report(records, pricing)
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 test_cost_exit_1_on_unknown_model(load_fixture):
52
- with (
53
- patch("dh_cli.bedrock.cost_report.walk_logs") as walk,
54
- patch(
55
- "dh_cli.bedrock.cost_report.fetch_cost_explorer_total",
56
- return_value=0.0,
57
- ),
58
- ):
59
- walk.return_value = iter([load_fixture("G_unknown_model.json")])
60
- result = _invoke(
61
- [
62
- "--start",
63
- "2026-04-17",
64
- "--end",
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