dh-cli 0.13.0__tar.gz → 0.13.3__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.13.0 → dh_cli-0.13.3}/PKG-INFO +1 -2
- {dh_cli-0.13.0 → dh_cli-0.13.3}/pyproject.toml +1 -2
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/bedrock/cost_report.py +8 -3
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/bedrock/pricing.yaml +6 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/github_commands.py +48 -18
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/main.py +2 -6
- dh_cli-0.13.3/tests/bedrock/test_resolve_base_model.py +98 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_login_error_paths.py +30 -0
- dh_cli-0.13.0/src/dh_cli/utility_commands.py +0 -194
- dh_cli-0.13.0/tests/bedrock/test_resolve_base_model.py +0 -50
- {dh_cli-0.13.0 → dh_cli-0.13.3}/.gitignore +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/LICENSE +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/README.md +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/__init__.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/_identity.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/__init__.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/aws_batch.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/__init__.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/boltz.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/cancel.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/clean.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/cost.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/disco.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/embed_t5.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/equilibrator.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/esmfold2.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/finalize.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/list_jobs.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/local.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/logs.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/orca.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/protmpnn.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/retry.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/rfd3.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/status.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/submit.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/wait_for.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/cost_report.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/fasta_utils.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/h5_utils.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/job_id.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/manifest.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/s3_transport.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/bedrock/__init__.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/bedrock/commands.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/cloud_commands.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/codeartifact.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/__init__.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/api_client.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/auth.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/engine_commands.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/progress.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/retry.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/ssh_config.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/studio_commands.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/studio_resolve.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/__init__.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/deploy.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/local.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/test.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/tf.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/users.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/sweep.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/warehouse.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/__init__.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/cur_fixtures.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_aws_batch_resources.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_batch_cost_command.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_batch_cost_filters.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_batch_cost_reader.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_batch_cost_reconcile.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_batch_cost_report.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_image_override.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_no_train_command.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_submit_cpu_only.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_submit_image_validation.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_submit_job_tags.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_submit_merge.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_submit_mlflow_env.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/conftest.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/A_cache_write.json +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/B_cache_read.json +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/C_plain.json +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/E_service_role.json +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_build_report.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_classify_arn.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_cli_exit_codes.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_cost_calc.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_cost_command.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_cur_reconciliation.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_key_command.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_render_formats.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_rotate.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_s3_walker.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/__init__.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/test_engine_retry_commands.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/test_retry.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/test_ssh_config.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/test_studio_resolution_commands.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/test_studio_resolve.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/__init__.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/conftest.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_identity.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_login.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_login_security.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_logout.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_rotate.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_status.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/hz/test_init.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/hz/test_suites.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/hz/test_users.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_aws_status.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_cloud_gcp.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_disco_command.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_disco_jobdef.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_equilibrator_command.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_esmfold2_cli.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_boltz_tar.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_disco.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_disco_dispatch.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_equilibrator.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_esmfold2.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_protmpnn.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_rfd3.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_rfd3_command.py +0 -0
- {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_sweep.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: dh-cli
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.3
|
|
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
|
|
@@ -131,7 +131,6 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
131
131
|
Requires-Dist: pyyaml>=6.0
|
|
132
132
|
Requires-Dist: questionary>=2.0.1
|
|
133
133
|
Requires-Dist: requests>=2.31.0
|
|
134
|
-
Requires-Dist: toml>=0.10
|
|
135
134
|
Requires-Dist: typer>=0.9.0
|
|
136
135
|
Provides-Extra: h5
|
|
137
136
|
Requires-Dist: h5py>=3.0; extra == 'h5'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dh-cli"
|
|
7
|
-
version = "0.13.
|
|
7
|
+
version = "0.13.3"
|
|
8
8
|
description = "Dayhoff Labs developer CLI"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
readme = "README.md"
|
|
@@ -20,7 +20,6 @@ dependencies = [
|
|
|
20
20
|
"pyyaml>=6.0",
|
|
21
21
|
"questionary>=2.0.1",
|
|
22
22
|
"requests>=2.31.0",
|
|
23
|
-
"toml>=0.10",
|
|
24
23
|
"typer>=0.9.0",
|
|
25
24
|
]
|
|
26
25
|
|
|
@@ -113,6 +113,7 @@ _BASE_MODELS = (
|
|
|
113
113
|
"sonnet-4-5",
|
|
114
114
|
"haiku-4-5",
|
|
115
115
|
"fable-5",
|
|
116
|
+
"fable-5-1",
|
|
116
117
|
)
|
|
117
118
|
|
|
118
119
|
|
|
@@ -122,9 +123,13 @@ def resolve_base_model(model_id: str) -> str:
|
|
|
122
123
|
if stripped.startswith(prefix):
|
|
123
124
|
stripped = stripped[len(prefix) :]
|
|
124
125
|
break
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
# Longest match wins: one base model's name can be a prefix of another's
|
|
127
|
+
# (`fable-5` inside `claude-fable-5-1`), and a first match would silently
|
|
128
|
+
# price the newer model at the older one's rates. Matching on length rather
|
|
129
|
+
# than position keeps `_BASE_MODELS` order irrelevant.
|
|
130
|
+
matches = [base for base in _BASE_MODELS if base in stripped]
|
|
131
|
+
if matches:
|
|
132
|
+
return max(matches, key=len)
|
|
128
133
|
raise UnknownModel(f"Unrecognized modelId: {model_id}")
|
|
129
134
|
|
|
130
135
|
|
|
@@ -26,6 +26,12 @@ models:
|
|
|
26
26
|
output_per_1m: 50.00
|
|
27
27
|
cache_write_per_1m: 12.50
|
|
28
28
|
cache_read_per_1m: 1.00
|
|
29
|
+
fable-5-1:
|
|
30
|
+
input_per_1m: 10.00
|
|
31
|
+
output_per_1m: 50.00
|
|
32
|
+
cache_write_per_1m: 12.50
|
|
33
|
+
# Fable 5.1 cut cache reads 75% vs Fable 5; the only rate that differs.
|
|
34
|
+
cache_read_per_1m: 0.25
|
|
29
35
|
sonnet-5:
|
|
30
36
|
input_per_1m: 2.00
|
|
31
37
|
output_per_1m: 10.00
|
|
@@ -41,6 +41,12 @@ NC = "\033[0m"
|
|
|
41
41
|
MAX_PAT_AGE_DAYS = 90
|
|
42
42
|
WARN_PAT_AGE_DAYS = 87
|
|
43
43
|
|
|
44
|
+
# Account that holds the github-pat/* secrets (dev). Used only to turn a
|
|
45
|
+
# ResourceNotFound into a "you're in the wrong account" diagnosis; the
|
|
46
|
+
# authoritative grant is the per-secret resource policy in
|
|
47
|
+
# blueprints/terraform/environments/dev/github_pat_secrets.tf.
|
|
48
|
+
PAT_ACCOUNT_ID = "074735440724"
|
|
49
|
+
|
|
44
50
|
# Path to the gh hosts file (resolved at call time via Path.home()).
|
|
45
51
|
_HOSTS_YML_RELATIVE = Path(".config") / "gh" / "hosts.yml"
|
|
46
52
|
|
|
@@ -178,6 +184,46 @@ def _access_denied_diagnosis(session, requested_handle: str) -> None:
|
|
|
178
184
|
raise typer.Exit(1)
|
|
179
185
|
|
|
180
186
|
|
|
187
|
+
def _not_found_diagnosis(session, secret_id: str) -> None:
|
|
188
|
+
"""Explain a ResourceNotFound on the PAT secret. Always exits 1.
|
|
189
|
+
|
|
190
|
+
A not-found is far more often a wrong-account session than
|
|
191
|
+
un-provisioned Terraform: `awslogin <admin-profile>` persists
|
|
192
|
+
AWS_PROFILE into ~/.bashrc, so every subsequent shell talks to that
|
|
193
|
+
account, where `github-pat/*` genuinely does not exist. Name the
|
|
194
|
+
account mismatch before sending anyone to an admin.
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
arn = session.client("sts").get_caller_identity()["Arn"]
|
|
198
|
+
# arn:aws:sts::<account>:assumed-role/... — account is field 4.
|
|
199
|
+
# Parsed from the ARN rather than GetCallerIdentity's "Account"
|
|
200
|
+
# because the ARN is the identity fact this module already
|
|
201
|
+
# trusts for handle resolution (see _identity.py).
|
|
202
|
+
account = arn.split(":")[4]
|
|
203
|
+
except Exception: # noqa: BLE001 — diagnosis must not mask the original error
|
|
204
|
+
account = ""
|
|
205
|
+
|
|
206
|
+
if account and account != PAT_ACCOUNT_ID:
|
|
207
|
+
typer.echo(
|
|
208
|
+
RED
|
|
209
|
+
+ f"Secret `{secret_id}` not found — your session is in account "
|
|
210
|
+
+ f"{account}, but GitHub PATs live in {PAT_ACCOUNT_ID} (dev). "
|
|
211
|
+
+ "Run `awslogin dev-devaccess` and retry."
|
|
212
|
+
+ NC,
|
|
213
|
+
err=True,
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
typer.echo(
|
|
217
|
+
RED
|
|
218
|
+
+ f"GitHub PAT secret `{secret_id}` does not exist. "
|
|
219
|
+
+ "Ask an admin to run the Terraform that provisions "
|
|
220
|
+
+ "per-developer github_pat_secrets."
|
|
221
|
+
+ NC,
|
|
222
|
+
err=True,
|
|
223
|
+
)
|
|
224
|
+
raise typer.Exit(1)
|
|
225
|
+
|
|
226
|
+
|
|
181
227
|
def _fetch_pat_and_metadata(session, handle: str) -> Tuple[str, dt.datetime]:
|
|
182
228
|
"""Fetch the PAT and its CreatedDate from Secrets Manager.
|
|
183
229
|
|
|
@@ -192,15 +238,7 @@ def _fetch_pat_and_metadata(session, handle: str) -> Tuple[str, dt.datetime]:
|
|
|
192
238
|
try:
|
|
193
239
|
describe = sm.describe_secret(SecretId=secret_id)
|
|
194
240
|
except sm.exceptions.ResourceNotFoundException:
|
|
195
|
-
|
|
196
|
-
RED
|
|
197
|
-
+ f"GitHub PAT secret `{secret_id}` does not exist. "
|
|
198
|
-
+ "Ask an admin to run the Terraform that provisions "
|
|
199
|
-
+ "per-developer github_pat_secrets."
|
|
200
|
-
+ NC,
|
|
201
|
-
err=True,
|
|
202
|
-
)
|
|
203
|
-
raise typer.Exit(1)
|
|
241
|
+
_not_found_diagnosis(session, secret_id)
|
|
204
242
|
except botocore.exceptions.ClientError as exc:
|
|
205
243
|
code = exc.response.get("Error", {}).get("Code", "")
|
|
206
244
|
if code == "AccessDeniedException":
|
|
@@ -623,15 +661,7 @@ def gh_rotate(
|
|
|
623
661
|
try:
|
|
624
662
|
describe = sm.describe_secret(SecretId=secret_id)
|
|
625
663
|
except sm.exceptions.ResourceNotFoundException:
|
|
626
|
-
|
|
627
|
-
RED
|
|
628
|
-
+ f"GitHub PAT secret `{secret_id}` does not exist. "
|
|
629
|
-
+ "Ask an admin to run the Terraform that provisions "
|
|
630
|
-
+ "per-developer github_pat_secrets."
|
|
631
|
-
+ NC,
|
|
632
|
-
err=True,
|
|
633
|
-
)
|
|
634
|
-
raise typer.Exit(1)
|
|
664
|
+
_not_found_diagnosis(session, secret_id)
|
|
635
665
|
except botocore.exceptions.ClientError as exc:
|
|
636
666
|
code = exc.response.get("Error", {}).get("Code", "")
|
|
637
667
|
if code == "AccessDeniedException":
|
|
@@ -7,7 +7,6 @@ import typer
|
|
|
7
7
|
from dh_cli.cloud_commands import aws_app, gcp_app
|
|
8
8
|
from dh_cli.github_commands import gh_app
|
|
9
9
|
from dh_cli.sweep import sweep
|
|
10
|
-
from dh_cli.utility_commands import build_and_upload_wheel
|
|
11
10
|
from dh_cli.warehouse import get_from_warehouse_typer
|
|
12
11
|
|
|
13
12
|
|
|
@@ -112,7 +111,7 @@ def _version_option(
|
|
|
112
111
|
@app.command("wheel")
|
|
113
112
|
def wheel_command(
|
|
114
113
|
target: str = typer.Argument(
|
|
115
|
-
help="What to publish: 'cli', 'batch', 'embedders',
|
|
114
|
+
help="What to publish: 'cli', 'batch', 'embedders', or 'chem'.",
|
|
116
115
|
),
|
|
117
116
|
bump: str = typer.Option(
|
|
118
117
|
"patch",
|
|
@@ -146,7 +145,6 @@ def wheel_command(
|
|
|
146
145
|
batch Build and publish dh-batch to CodeArtifact
|
|
147
146
|
embedders Build and publish dh-embedders to CodeArtifact
|
|
148
147
|
chem Build and publish dh-chem to CodeArtifact
|
|
149
|
-
legacy Build and publish dayhoff-tools to PyPI (default)
|
|
150
148
|
"""
|
|
151
149
|
from dh_cli.codeartifact import PACKAGE_DIRS
|
|
152
150
|
|
|
@@ -160,10 +158,8 @@ def wheel_command(
|
|
|
160
158
|
dry_run=dry_run,
|
|
161
159
|
skip_pypi=no_pypi,
|
|
162
160
|
)
|
|
163
|
-
elif target == "legacy":
|
|
164
|
-
build_and_upload_wheel(bump_part=bump)
|
|
165
161
|
else:
|
|
166
|
-
typer.echo(f"Unknown target '{target}'. Use 'cli', 'batch', 'embedders',
|
|
162
|
+
typer.echo(f"Unknown target '{target}'. Use 'cli', 'batch', 'embedders', or 'chem'.")
|
|
167
163
|
raise typer.Exit(1)
|
|
168
164
|
|
|
169
165
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Model-ID resolution tests.
|
|
2
|
+
|
|
3
|
+
A Bedrock modelId can appear in three shapes for the same underlying
|
|
4
|
+
model:
|
|
5
|
+
- direct: anthropic.claude-opus-4-7
|
|
6
|
+
- US profile: us.anthropic.claude-opus-4-7
|
|
7
|
+
- Global profile: global.anthropic.claude-opus-4-7
|
|
8
|
+
|
|
9
|
+
`resolve_base_model` must collapse all three to the same pricing key.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize(
|
|
18
|
+
"model_id, expected_base",
|
|
19
|
+
[
|
|
20
|
+
("anthropic.claude-opus-4-7", "opus-4-7"),
|
|
21
|
+
("us.anthropic.claude-opus-4-7", "opus-4-7"),
|
|
22
|
+
("global.anthropic.claude-opus-4-7", "opus-4-7"),
|
|
23
|
+
("anthropic.claude-opus-4-8", "opus-4-8"),
|
|
24
|
+
("us.anthropic.claude-opus-4-8", "opus-4-8"),
|
|
25
|
+
("global.anthropic.claude-opus-4-8", "opus-4-8"),
|
|
26
|
+
("anthropic.claude-opus-4-6-v1", "opus-4-6"),
|
|
27
|
+
("us.anthropic.claude-opus-4-6-v1", "opus-4-6"),
|
|
28
|
+
("global.anthropic.claude-opus-4-6-v1", "opus-4-6"),
|
|
29
|
+
("anthropic.claude-sonnet-4-6", "sonnet-4-6"),
|
|
30
|
+
("us.anthropic.claude-sonnet-4-6", "sonnet-4-6"),
|
|
31
|
+
("global.anthropic.claude-sonnet-4-6", "sonnet-4-6"),
|
|
32
|
+
("anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
|
|
33
|
+
("us.anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
|
|
34
|
+
("global.anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
|
|
35
|
+
("anthropic.claude-fable-5", "fable-5"),
|
|
36
|
+
("us.anthropic.claude-fable-5", "fable-5"),
|
|
37
|
+
("global.anthropic.claude-fable-5", "fable-5"),
|
|
38
|
+
# A base model whose name is a prefix of a newer one must not swallow
|
|
39
|
+
# it: `fable-5` is a substring of `claude-fable-5-1`, so a first-match
|
|
40
|
+
# or badly-ordered scan silently prices 5.1 as 5. The two differ only
|
|
41
|
+
# in cache reads ($1.00/M vs $0.25/M), so the error is invisible in
|
|
42
|
+
# every other column.
|
|
43
|
+
("anthropic.claude-fable-5-1", "fable-5-1"),
|
|
44
|
+
("us.anthropic.claude-fable-5-1", "fable-5-1"),
|
|
45
|
+
("global.anthropic.claude-fable-5-1", "fable-5-1"),
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
def test_resolve_base_model_known(model_id: str, expected_base: str):
|
|
49
|
+
from dh_cli.bedrock.cost_report import resolve_base_model
|
|
50
|
+
|
|
51
|
+
assert resolve_base_model(model_id) == expected_base
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_resolve_prefers_the_most_specific_base():
|
|
55
|
+
"""Resolution must not depend on `_BASE_MODELS` ordering.
|
|
56
|
+
|
|
57
|
+
Reversing the tuple would flip a first-match implementation's answer.
|
|
58
|
+
"""
|
|
59
|
+
from dh_cli.bedrock import cost_report
|
|
60
|
+
|
|
61
|
+
original = cost_report._BASE_MODELS
|
|
62
|
+
try:
|
|
63
|
+
cost_report._BASE_MODELS = tuple(reversed(original))
|
|
64
|
+
assert cost_report.resolve_base_model("global.anthropic.claude-fable-5-1") == "fable-5-1"
|
|
65
|
+
assert cost_report.resolve_base_model("global.anthropic.claude-fable-5") == "fable-5"
|
|
66
|
+
finally:
|
|
67
|
+
cost_report._BASE_MODELS = original
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_every_base_model_has_pricing():
|
|
71
|
+
"""A resolvable base with no pricing row raises only at report time."""
|
|
72
|
+
from dh_cli.bedrock.cost_report import (
|
|
73
|
+
_BASE_MODELS,
|
|
74
|
+
default_pricing_path,
|
|
75
|
+
load_pricing,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
pricing = load_pricing(default_pricing_path())
|
|
79
|
+
missing = [base for base in _BASE_MODELS if base not in pricing]
|
|
80
|
+
assert not missing, f"base models with no pricing entry: {missing}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_fable_51_cache_reads_are_priced_below_fable_5():
|
|
84
|
+
"""The one rate that differs between the two Fable versions."""
|
|
85
|
+
from dh_cli.bedrock.cost_report import default_pricing_path, load_pricing
|
|
86
|
+
|
|
87
|
+
pricing = load_pricing(default_pricing_path())
|
|
88
|
+
assert pricing["fable-5-1"]["cache_read_per_1m"] == 0.25
|
|
89
|
+
assert pricing["fable-5"]["cache_read_per_1m"] == 1.00
|
|
90
|
+
for rate in ("input_per_1m", "output_per_1m", "cache_write_per_1m"):
|
|
91
|
+
assert pricing["fable-5-1"][rate] == pricing["fable-5"][rate]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_resolve_base_model_unknown_raises():
|
|
95
|
+
from dh_cli.bedrock.cost_report import UnknownModel, resolve_base_model
|
|
96
|
+
|
|
97
|
+
with pytest.raises(UnknownModel):
|
|
98
|
+
resolve_base_model("global.anthropic.claude-someshinynew-99-v42")
|
|
@@ -80,6 +80,36 @@ def test_login_secret_not_found_tells_user_to_ask_admin(fake_subprocess, patch_g
|
|
|
80
80
|
assert "--init" not in combined
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def test_login_secret_not_found_in_wrong_account_says_wrong_account(fake_subprocess, patch_gh_path, tmp_hosts_yml):
|
|
84
|
+
"""A not-found in a non-dev account is a wrong-profile error.
|
|
85
|
+
|
|
86
|
+
Regression: `awslogin admin-secadmin` persists AWS_PROFILE into
|
|
87
|
+
~/.bashrc, so every later shell talks to the admin account, where
|
|
88
|
+
github-pat/* genuinely does not exist. The old message blamed
|
|
89
|
+
un-provisioned Terraform and sent the user to an admin.
|
|
90
|
+
"""
|
|
91
|
+
session = make_session(arn="arn:aws:sts::293882909581:assumed-role/AWSReservedSSO_SecurityAdministrator_abc123/dma")
|
|
92
|
+
err_response = {
|
|
93
|
+
"Error": {
|
|
94
|
+
"Code": "ResourceNotFoundException",
|
|
95
|
+
"Message": "Secrets Manager can't find the specified secret.",
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
session._sm.describe_secret.side_effect = session._sm.exceptions.ResourceNotFoundException(
|
|
99
|
+
err_response, "DescribeSecret"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
result = _invoke([], session=session, fake_subprocess=fake_subprocess)
|
|
103
|
+
assert result.exit_code != 0
|
|
104
|
+
combined = _all(result)
|
|
105
|
+
# Names both accounts so the user can see the mismatch.
|
|
106
|
+
assert "293882909581" in combined
|
|
107
|
+
assert "074735440724" in combined
|
|
108
|
+
# Points at the fix, not at an admin.
|
|
109
|
+
assert "awslogin dev-devaccess" in combined
|
|
110
|
+
assert "ask an admin" not in combined.lower()
|
|
111
|
+
|
|
112
|
+
|
|
83
113
|
def test_login_empty_secret_suggests_rotate_init(fake_subprocess, patch_gh_path, tmp_hosts_yml):
|
|
84
114
|
# Secret exists but no AWSCURRENT version — new-hire state.
|
|
85
115
|
session = make_session(version_stages={})
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
"""CLI commands common to all repos."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import re
|
|
5
|
-
import shutil
|
|
6
|
-
import subprocess
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
import toml
|
|
10
|
-
|
|
11
|
-
# Import cloud helper lazily inside functions to avoid heavy deps at module load
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def get_current_version_from_toml(file_path="pyproject.toml"):
|
|
15
|
-
"""Reads the version from a pyproject.toml file."""
|
|
16
|
-
try:
|
|
17
|
-
with open(file_path, "r") as f:
|
|
18
|
-
content = f.read()
|
|
19
|
-
version_match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
|
20
|
-
if version_match:
|
|
21
|
-
return version_match.group(1)
|
|
22
|
-
else:
|
|
23
|
-
raise ValueError(f"Could not find version string in {file_path}")
|
|
24
|
-
except FileNotFoundError:
|
|
25
|
-
raise FileNotFoundError(f"{file_path} not found.")
|
|
26
|
-
except Exception as e:
|
|
27
|
-
raise e
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def build_and_upload_wheel(bump_part: str = "patch"):
|
|
31
|
-
"""Build a Python wheel and upload to PyPI using UV.
|
|
32
|
-
|
|
33
|
-
Automatically increments the version number in pyproject.toml before building
|
|
34
|
-
based on the bump_part argument ('major', 'minor', 'patch').
|
|
35
|
-
|
|
36
|
-
Expects PyPI authentication to be configured via the environment variable:
|
|
37
|
-
- UV_PUBLISH_TOKEN
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
bump_part (str): The part of the version to bump. Defaults to 'patch'.
|
|
41
|
-
"""
|
|
42
|
-
if bump_part not in ["major", "minor", "patch"]:
|
|
43
|
-
print(f"Error: Invalid bump_part '{bump_part}'. Must be 'major', 'minor', or 'patch'.")
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
# ANSI color codes
|
|
47
|
-
BLUE = "\033[94m"
|
|
48
|
-
RESET = "\033[0m"
|
|
49
|
-
|
|
50
|
-
# --- Authentication Setup ---
|
|
51
|
-
token = os.environ.get("UV_PUBLISH_TOKEN")
|
|
52
|
-
|
|
53
|
-
if not token:
|
|
54
|
-
print("Error: PyPI authentication not configured.")
|
|
55
|
-
print("Please set the UV_PUBLISH_TOKEN environment variable with your PyPI API token.")
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
# Build the command with token authentication
|
|
59
|
-
# IMPORTANT: Mask token for printing
|
|
60
|
-
publish_cmd_safe_print = ["uv", "publish", "--token", "*****"]
|
|
61
|
-
publish_cmd = ["uv", "publish", "--token", token]
|
|
62
|
-
print("Using UV_PUBLISH_TOKEN for authentication.")
|
|
63
|
-
|
|
64
|
-
# Use standard pyproject.toml
|
|
65
|
-
pyproject_path = "pyproject.toml"
|
|
66
|
-
if not Path(pyproject_path).exists():
|
|
67
|
-
print("Error: pyproject.toml not found in current directory.")
|
|
68
|
-
return
|
|
69
|
-
current_version = None # Initialize in case the first try block fails
|
|
70
|
-
|
|
71
|
-
try:
|
|
72
|
-
# --- Clean dist directory ---
|
|
73
|
-
dist_dir = Path("dist")
|
|
74
|
-
if dist_dir.exists():
|
|
75
|
-
print(f"Removing existing build directory: {dist_dir}")
|
|
76
|
-
shutil.rmtree(dist_dir)
|
|
77
|
-
# --- End Clean dist directory ---
|
|
78
|
-
|
|
79
|
-
# --- Version Bumping Logic ---
|
|
80
|
-
current_version = get_current_version_from_toml(pyproject_path)
|
|
81
|
-
print(f"Current version: {current_version}")
|
|
82
|
-
|
|
83
|
-
try:
|
|
84
|
-
major, minor, patch = map(int, current_version.split("."))
|
|
85
|
-
except ValueError:
|
|
86
|
-
print(f"Error: Could not parse version '{current_version}'. Expected format X.Y.Z")
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
if bump_part == "major":
|
|
90
|
-
major += 1
|
|
91
|
-
minor = 0
|
|
92
|
-
patch = 0
|
|
93
|
-
elif bump_part == "minor":
|
|
94
|
-
minor += 1
|
|
95
|
-
patch = 0
|
|
96
|
-
else: # patch
|
|
97
|
-
patch += 1
|
|
98
|
-
|
|
99
|
-
new_version = f"{major}.{minor}.{patch}"
|
|
100
|
-
print(f"Bumping {bump_part} version to: {new_version}")
|
|
101
|
-
|
|
102
|
-
# Read pyproject.toml
|
|
103
|
-
with open(pyproject_path, "r") as f:
|
|
104
|
-
content = f.read()
|
|
105
|
-
|
|
106
|
-
# Replace the version string
|
|
107
|
-
pattern = re.compile(f'^version\s*=\s*"{re.escape(current_version)}"', re.MULTILINE)
|
|
108
|
-
new_content, num_replacements = pattern.subn(f'version = "{new_version}"', content)
|
|
109
|
-
|
|
110
|
-
if num_replacements == 0:
|
|
111
|
-
print(f"Error: Could not find 'version = \"{current_version}\"' in {pyproject_path}")
|
|
112
|
-
return # Exit before build/publish if version wasn't updated
|
|
113
|
-
if num_replacements > 1:
|
|
114
|
-
print(f"Warning: Found multiple version lines for '{current_version}'. Only the first was updated.")
|
|
115
|
-
|
|
116
|
-
# Write the updated content back
|
|
117
|
-
with open(pyproject_path, "w") as f:
|
|
118
|
-
f.write(new_content)
|
|
119
|
-
print(f"Updated {pyproject_path} with version {new_version}")
|
|
120
|
-
|
|
121
|
-
# --- End Version Bumping Logic ---
|
|
122
|
-
|
|
123
|
-
# Build wheel and sdist
|
|
124
|
-
build_cmd = ["uv", "build"]
|
|
125
|
-
print(f"Running command: {BLUE}{' '.join(build_cmd)}{RESET}")
|
|
126
|
-
subprocess.run(build_cmd, check=True)
|
|
127
|
-
|
|
128
|
-
# Upload to PyPI
|
|
129
|
-
print(f"Running command: {BLUE}{' '.join(publish_cmd_safe_print)}{RESET}")
|
|
130
|
-
subprocess.run(publish_cmd, check=True)
|
|
131
|
-
|
|
132
|
-
print(f"Successfully built and uploaded version {new_version} to PyPI")
|
|
133
|
-
|
|
134
|
-
# Re-install DHT in Pixi environment when building from DHT itself
|
|
135
|
-
try:
|
|
136
|
-
proj_toml = toml.load(pyproject_path)
|
|
137
|
-
proj_name = proj_toml.get("project", {}).get("name")
|
|
138
|
-
if proj_name == "dayhoff-tools":
|
|
139
|
-
print("Re-installing dayhoff-tools into the Pixi environment...")
|
|
140
|
-
# Detect if CUDA is available - use cpu environment if not
|
|
141
|
-
has_cuda = shutil.which("nvidia-smi") is not None
|
|
142
|
-
if has_cuda:
|
|
143
|
-
reinstall_cmd = ["pixi", "install"]
|
|
144
|
-
else:
|
|
145
|
-
reinstall_cmd = ["pixi", "install", "-e", "cpu"]
|
|
146
|
-
print(f"Running command: {BLUE}{' '.join(reinstall_cmd)}{RESET}")
|
|
147
|
-
subprocess.run(reinstall_cmd, check=True)
|
|
148
|
-
print("dayhoff-tools reinstalled in the Pixi environment.")
|
|
149
|
-
except subprocess.CalledProcessError as e:
|
|
150
|
-
print(f"Warning: Failed to reinstall dayhoff-tools locally: {e}")
|
|
151
|
-
except Exception:
|
|
152
|
-
pass # Not dayhoff-tools or couldn't read toml
|
|
153
|
-
|
|
154
|
-
except FileNotFoundError:
|
|
155
|
-
print(f"Error: {pyproject_path} not found.")
|
|
156
|
-
# No version change happened, so no rollback needed
|
|
157
|
-
except subprocess.CalledProcessError as e:
|
|
158
|
-
print(f"Error during build/upload: {e}")
|
|
159
|
-
# Attempt to roll back version change only if it was bumped successfully
|
|
160
|
-
if current_version and new_version:
|
|
161
|
-
try:
|
|
162
|
-
print(f"Attempting to revert version in {pyproject_path} back to {current_version}...")
|
|
163
|
-
with open(pyproject_path, "r") as f:
|
|
164
|
-
content_revert = f.read()
|
|
165
|
-
# Use new_version in pattern for reverting
|
|
166
|
-
pattern_revert = re.compile(f'^version\s*=\s*"{re.escape(new_version)}"', re.MULTILINE)
|
|
167
|
-
reverted_content, num_revert = pattern_revert.subn(f'version = "{current_version}"', content_revert)
|
|
168
|
-
if num_revert > 0:
|
|
169
|
-
with open(pyproject_path, "w") as f:
|
|
170
|
-
f.write(reverted_content)
|
|
171
|
-
print(f"Successfully reverted version in {pyproject_path}.")
|
|
172
|
-
else:
|
|
173
|
-
print(f"Warning: Could not find version {new_version} to revert in {pyproject_path}.")
|
|
174
|
-
|
|
175
|
-
except Exception as revert_e:
|
|
176
|
-
print(f"Warning: Failed to revert version change: {revert_e}")
|
|
177
|
-
except Exception as e:
|
|
178
|
-
print(f"An unexpected error occurred: {e}")
|
|
179
|
-
# Attempt rollback if version was bumped
|
|
180
|
-
if current_version and "new_version" in locals() and new_version:
|
|
181
|
-
try:
|
|
182
|
-
print(f"Attempting to revert version back to {current_version}...")
|
|
183
|
-
with open(pyproject_path, "r") as f:
|
|
184
|
-
content_revert = f.read()
|
|
185
|
-
pattern_revert = re.compile(f'^version\\s*=\\s*"{re.escape(new_version)}"', re.MULTILINE)
|
|
186
|
-
reverted_content, num_revert = pattern_revert.subn(f'version = "{current_version}"', content_revert)
|
|
187
|
-
if num_revert > 0:
|
|
188
|
-
with open(pyproject_path, "w") as f:
|
|
189
|
-
f.write(reverted_content)
|
|
190
|
-
print("Successfully reverted version in pyproject.toml.")
|
|
191
|
-
else:
|
|
192
|
-
print(f"Warning: Could not find version {new_version} to revert.")
|
|
193
|
-
except Exception as revert_e:
|
|
194
|
-
print(f"Warning: Failed to revert version change: {revert_e}")
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"""Model-ID resolution tests.
|
|
2
|
-
|
|
3
|
-
A Bedrock modelId can appear in three shapes for the same underlying
|
|
4
|
-
model:
|
|
5
|
-
- direct: anthropic.claude-opus-4-7
|
|
6
|
-
- US profile: us.anthropic.claude-opus-4-7
|
|
7
|
-
- Global profile: global.anthropic.claude-opus-4-7
|
|
8
|
-
|
|
9
|
-
`resolve_base_model` must collapse all three to the same pricing key.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import pytest
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest.mark.parametrize(
|
|
18
|
-
"model_id, expected_base",
|
|
19
|
-
[
|
|
20
|
-
("anthropic.claude-opus-4-7", "opus-4-7"),
|
|
21
|
-
("us.anthropic.claude-opus-4-7", "opus-4-7"),
|
|
22
|
-
("global.anthropic.claude-opus-4-7", "opus-4-7"),
|
|
23
|
-
("anthropic.claude-opus-4-8", "opus-4-8"),
|
|
24
|
-
("us.anthropic.claude-opus-4-8", "opus-4-8"),
|
|
25
|
-
("global.anthropic.claude-opus-4-8", "opus-4-8"),
|
|
26
|
-
("anthropic.claude-opus-4-6-v1", "opus-4-6"),
|
|
27
|
-
("us.anthropic.claude-opus-4-6-v1", "opus-4-6"),
|
|
28
|
-
("global.anthropic.claude-opus-4-6-v1", "opus-4-6"),
|
|
29
|
-
("anthropic.claude-sonnet-4-6", "sonnet-4-6"),
|
|
30
|
-
("us.anthropic.claude-sonnet-4-6", "sonnet-4-6"),
|
|
31
|
-
("global.anthropic.claude-sonnet-4-6", "sonnet-4-6"),
|
|
32
|
-
("anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
|
|
33
|
-
("us.anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
|
|
34
|
-
("global.anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
|
|
35
|
-
("anthropic.claude-fable-5", "fable-5"),
|
|
36
|
-
("us.anthropic.claude-fable-5", "fable-5"),
|
|
37
|
-
("global.anthropic.claude-fable-5", "fable-5"),
|
|
38
|
-
],
|
|
39
|
-
)
|
|
40
|
-
def test_resolve_base_model_known(model_id: str, expected_base: str):
|
|
41
|
-
from dh_cli.bedrock.cost_report import resolve_base_model
|
|
42
|
-
|
|
43
|
-
assert resolve_base_model(model_id) == expected_base
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_resolve_base_model_unknown_raises():
|
|
47
|
-
from dh_cli.bedrock.cost_report import UnknownModel, resolve_base_model
|
|
48
|
-
|
|
49
|
-
with pytest.raises(UnknownModel):
|
|
50
|
-
resolve_base_model("global.anthropic.claude-someshinynew-99-v42")
|
|
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
|