jambu-gpu 0.2.0__tar.gz → 0.3.0__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 (95) hide show
  1. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/.gitignore +1 -0
  2. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/CONTRIBUTING.md +2 -2
  3. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/PKG-INFO +2 -2
  4. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/README.md +1 -1
  5. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/ACCEPTANCE.md +2 -2
  6. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/CONFIG.md +1 -1
  7. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/INTEGRATION.md +6 -5
  8. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/LIFECYCLE.md +1 -1
  9. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/USAGE.md +5 -5
  10. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/README.md +1 -1
  11. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/__init__.py +1 -1
  12. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/_common.py +1 -1
  13. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/account.py +2 -2
  14. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/main.py +1 -1
  15. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/timer.py +6 -4
  16. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/config.py +2 -2
  17. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/credentials.py +4 -2
  18. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/events.py +1 -1
  19. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/hosts.py +1 -1
  20. jambu_gpu-0.3.0/jambu_gpu/core/paths.py +42 -0
  21. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/state.py +2 -2
  22. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/sweep.py +6 -6
  23. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/pyproject.toml +2 -2
  24. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/conftest.py +4 -3
  25. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_config.py +1 -1
  26. jambu_gpu-0.3.0/tests/test_paths.py +50 -0
  27. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_state.py +1 -1
  28. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_sweep.py +5 -5
  29. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/.env.example +0 -0
  30. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/LICENSE +0 -0
  31. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/ADDING_A_PROVIDER.md +0 -0
  32. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/CHOOSING_A_MODEL.md +0 -0
  33. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/assets/jambu-logo.jpg +0 -0
  34. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/experiment.py +0 -0
  35. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/jambu.yaml +0 -0
  36. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/README.md +0 -0
  37. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/package-lock.json +0 -0
  38. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/package.json +0 -0
  39. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/src/agent.ts +0 -0
  40. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/src/index.ts +0 -0
  41. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/src/tools/weather-tool.ts +0 -0
  42. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/tsconfig.json +0 -0
  43. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/package-lock.json +0 -0
  44. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/package.json +0 -0
  45. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/src/index.ts +0 -0
  46. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/src/jambuGpu.ts +0 -0
  47. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/tsconfig.json +0 -0
  48. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu.example.yaml +0 -0
  49. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu.models.yaml +0 -0
  50. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu.yaml +0 -0
  51. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/__main__.py +0 -0
  52. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/__init__.py +0 -0
  53. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/inspect.py +0 -0
  54. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/lifecycle.py +0 -0
  55. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/providers.py +0 -0
  56. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/run.py +0 -0
  57. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/setup.py +0 -0
  58. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/status.py +0 -0
  59. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/validate.py +0 -0
  60. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/__init__.py +0 -0
  61. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/beacon.py +0 -0
  62. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/durations.py +0 -0
  63. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/engine.py +0 -0
  64. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/errors.py +0 -0
  65. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/execution.py +0 -0
  66. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/health.py +0 -0
  67. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/model_inspect.py +0 -0
  68. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/models.py +0 -0
  69. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/policy.py +0 -0
  70. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/tool_parsers.py +0 -0
  71. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/__init__.py +0 -0
  72. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/base.py +0 -0
  73. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/registry.py +0 -0
  74. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/vast/__init__.py +0 -0
  75. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/vast/client.py +0 -0
  76. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/vast/config.py +0 -0
  77. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/vast/mapper.py +0 -0
  78. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/vast/provider.py +0 -0
  79. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/runtime/__init__.py +0 -0
  80. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/runtime/base.py +0 -0
  81. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/runtime/vllm.py +0 -0
  82. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/watchdog/__init__.py +0 -0
  83. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/watchdog/agent_body.py +0 -0
  84. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/watchdog/builder.py +0 -0
  85. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/fakes.py +0 -0
  86. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_cli.py +0 -0
  87. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_engine.py +0 -0
  88. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_engine_sessions.py +0 -0
  89. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_integration.py +0 -0
  90. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_model_inspect.py +0 -0
  91. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_policy.py +0 -0
  92. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_runtime_vllm.py +0 -0
  93. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_vast_mapper.py +0 -0
  94. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_vast_provider.py +0 -0
  95. {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_watchdog.py +0 -0
@@ -5,6 +5,7 @@ venv/
5
5
  dist/
6
6
  build/
7
7
  *.egg-info/
8
+ .gpu/
8
9
  .jambu/
9
10
  .env
10
11
  .env.*
@@ -62,9 +62,9 @@ job queues, a web dashboard. See [docs/ACCEPTANCE.md](docs/ACCEPTANCE.md).
62
62
 
63
63
  ## Secrets
64
64
 
65
- Never commit credentials, `.env`, `.jambu/state.json`, or provider instance
65
+ Never commit credentials, `.env`, `.gpu/state.json`, or provider instance
66
66
  ids from a real run. Keys are read from the environment, a gitignored `.env`,
67
- or `~/.jambu/credentials`. `.env.example` ships with empty values only.
67
+ or `~/.gpu/credentials`. `.env.example` ships with empty values only.
68
68
 
69
69
  ## Pull requests
70
70
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: jambu-gpu
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Provider-agnostic CLI for provisioning and managing temporary GPU runtimes
5
5
  Project-URL: Homepage, https://github.com/jambuai/jambu-gpu
6
6
  Project-URL: Repository, https://github.com/jambuai/jambu-gpu
@@ -49,7 +49,7 @@ From an application in any language: `gpu --json setup` returns the OpenAI-compa
49
49
 
50
50
  As a last line of defense, `gpu ls` shows every jambu instance in the account and `gpu sweep --install-timer` sets up a systemd user timer that guards every project on the machine by its own rules and cleans up the orphans no project tracks. See [Keeping the account clean](docs/USAGE.md#keeping-the-account-clean).
51
51
 
52
- The API key is read from the environment, a project `.env`, or `~/.jambu/credentials`. It never goes in the manifest.
52
+ The API key is read from the environment, a project `.env`, or `~/.gpu/credentials`. It never goes in the manifest.
53
53
 
54
54
  ## Declare
55
55
 
@@ -26,7 +26,7 @@ From an application in any language: `gpu --json setup` returns the OpenAI-compa
26
26
 
27
27
  As a last line of defense, `gpu ls` shows every jambu instance in the account and `gpu sweep --install-timer` sets up a systemd user timer that guards every project on the machine by its own rules and cleans up the orphans no project tracks. See [Keeping the account clean](docs/USAGE.md#keeping-the-account-clean).
28
28
 
29
- The API key is read from the environment, a project `.env`, or `~/.jambu/credentials`. It never goes in the manifest.
29
+ The API key is read from the environment, a project `.env`, or `~/.gpu/credentials`. It never goes in the manifest.
30
30
 
31
31
  ## Declare
32
32
 
@@ -4,7 +4,7 @@ Eighteen guarantees, mapped to the code and the test that holds each one.
4
4
 
5
5
  | # | Guarantee | Implementation | Test |
6
6
  | --- | --- | --- | --- |
7
- | 1 | Repeated `setup` does not duplicate infrastructure | `core/engine.py::Engine.setup` reconciles first, compares `config.fingerprint()`, and holds an exclusive `.jambu/runtime.lock` | `test_engine.py::test_setup_twice_does_not_create_two_instances`, `::test_setup_refuses_to_reuse_an_incompatible_instance` |
7
+ | 1 | Repeated `setup` does not duplicate infrastructure | `core/engine.py::Engine.setup` reconciles first, compares `config.fingerprint()`, and holds an exclusive `.gpu/runtime.lock` | `test_engine.py::test_setup_twice_does_not_create_two_instances`, `::test_setup_refuses_to_reuse_an_incompatible_instance` |
8
8
  | 2 | Provider APIs do not leak into core orchestration | `providers/base.py` contract; all Vast wire format confined to `providers/vast/{client,mapper}.py` | `test_engine.py::test_the_core_never_branches_on_a_provider_name` (greps `core/`) |
9
9
  | 3 | Missing credentials fail before provisioning | `VastProvider.validate` returns before any API call; `CredentialResolver` never reads jambu.yaml | `test_vast_provider.py::test_validate_fails_before_provisioning_without_a_key` (asserts zero HTTP calls) |
10
10
  | 4 | Failed provisioning cleans up | `Engine.setup` persists state *before* the long wait, then destroys on failure when `cleanup_on_setup_failure` | `test_engine.py::test_failed_setup_cleans_up_partially_created_resources` |
@@ -19,7 +19,7 @@ Eighteen guarantees, mapped to the code and the test that holds each one.
19
19
  | 13 | A restarted instance stays guarded | `Engine._reconcile` takes running/stopped from the provider; the watchdog's `terminated` flag only counts when the provider cannot tell. `RuntimeState.session_started_at` restarts on every start, and `max_lifetime` and the session budget are measured from it | `test_engine_sessions.py::test_reconcile_trusts_the_provider_over_a_stale_terminated_flag`, `::test_a_stale_terminated_flag_does_not_disable_the_guard`, `::test_restarting_a_stopped_instance_starts_a_new_session`, `::test_guard_budget_resets_with_a_new_session`; watchdog-side session tests in `test_policy.py` / `test_watchdog.py` |
20
20
  | 14 | Traffic on the model endpoint is activity | `VllmRuntime.activity_probe` hands the watchdog vLLM's `/metrics` gauges and counters; `lifecycle.traffic_counts_as_activity` turns it off | `test_engine_sessions.py::test_the_watchdog_is_told_how_to_read_vllm_traffic`, `::test_traffic_can_be_excluded_from_activity`, `test_runtime_vllm.py::test_activity_probe_reads_vllm_metrics_on_loopback`; the watchdog's metrics-reading tests in `test_watchdog.py` |
21
21
  | 15 | A stopped instance does not bill storage forever | `lifecycle.stopped_ttl` -> `PolicyConfig.stopped_ttl_s`; `RuntimeState.stopped_since`; `Engine.guard_tick` destroys with reason `stopped_ttl`; `validate` warns when nothing would remove it | `test_engine_sessions.py::test_guard_destroys_an_instance_stopped_longer_than_stopped_ttl`, `::test_validate_warns_when_nothing_removes_a_billing_stopped_instance`; the stopped-TTL policy tests in `test_policy.py` |
22
- | 16 | Orphans are found account-wide, each project keeps its own rules, and nothing else is touched | `GPUProvider.list_instances` (capability `list_instances`) returns only `jambu-` labels; `gpu setup` registers the project in `~/.jambu/projects.json`; `core/sweep.py::guard_projects` runs each registered project's own `Engine.guard_tick`; only untracked instances are orphans: running past `--orphan-max-age` is stopped (never destroyed), stopped past `--stopped-ttl` (from the sweep's own first sighting in `~/.jambu/sweep.json`) is destroyed; `gpu sweep --install-timer` schedules one working-directory-independent unit and refuses a key that is only in the shell | `test_sweep.py::test_vast_lists_only_jambu_labelled_instances`, `::test_non_jambu_instances_are_never_touched`, `::test_another_projects_restarted_and_locked_instance_is_not_destroyed`, `::test_each_project_is_judged_by_its_own_max_lifetime`, `::test_a_running_orphan_past_orphan_max_age_is_stopped_never_destroyed`, `::test_a_stopped_orphan_is_destroyed_once_seen_stopped_past_stopped_ttl`, `::test_install_timer_writes_one_unit_independent_of_the_project`, `::test_install_timer_refuses_a_key_only_in_the_shell_environment` |
22
+ | 16 | Orphans are found account-wide, each project keeps its own rules, and nothing else is touched | `GPUProvider.list_instances` (capability `list_instances`) returns only `jambu-` labels; `gpu setup` registers the project in `~/.gpu/projects.json`; `core/sweep.py::guard_projects` runs each registered project's own `Engine.guard_tick`; only untracked instances are orphans: running past `--orphan-max-age` is stopped (never destroyed), stopped past `--stopped-ttl` (from the sweep's own first sighting in `~/.gpu/sweep.json`) is destroyed; `gpu sweep --install-timer` schedules one working-directory-independent unit and refuses a key that is only in the shell | `test_sweep.py::test_vast_lists_only_jambu_labelled_instances`, `::test_non_jambu_instances_are_never_touched`, `::test_another_projects_restarted_and_locked_instance_is_not_destroyed`, `::test_each_project_is_judged_by_its_own_max_lifetime`, `::test_a_running_orphan_past_orphan_max_age_is_stopped_never_destroyed`, `::test_a_stopped_orphan_is_destroyed_once_seen_stopped_past_stopped_ttl`, `::test_install_timer_writes_one_unit_independent_of_the_project`, `::test_install_timer_refuses_a_key_only_in_the_shell_environment` |
23
23
  | 17 | The CLI guard never stops a serving endpoint it cannot see | `Engine._idle_blind`: with the watchdog enabled and installed but unreachable, `guard_tick` evaluates without `idle_timeout` and records `idle_skipped`; `max_lifetime`, budget and `stopped_ttl` still apply | `test_engine_sessions.py::test_guard_leaves_idle_to_an_unreachable_watchdog`, `::test_max_lifetime_still_applies_when_the_watchdog_is_unreachable` |
24
24
  | 18 | A fresh stop or a short API outage never costs the disk | `RuntimeState.mark_stopped` restarts `stopped_since` on evidence the instance ran since; the watchdog escalates a failing stop to destroy only after `escalate_after_s` (30m) of continuous failure and resets on renewed use | `test_engine_sessions.py::test_a_stale_stopped_since_does_not_destroy_a_fresh_stop`, `test_watchdog.py::test_an_api_blip_does_not_destroy_the_disk`, `::test_failures_reset_when_the_instance_is_back_in_use` |
25
25
 
@@ -89,7 +89,7 @@ each number comes from.
89
89
  | `disk_gb` | `80` | Disk. Must fit the model weights plus the container. |
90
90
  | `region` | – | Comma-separated geolocations. |
91
91
  | `spot` | `false` | Use interruptible/bid capacity where supported. |
92
- | `host_attempts` | `3` | How many machines `setup` tries. A host that fails to boot (the provider reports it failed, it vanishes, or it is not running within `health.boot_timeout`) is destroyed, replaced from fresh offers (each held to the budget), and remembered in `$JAMBU_HOME/bad_hosts.json` so no setup rents it again for 24h. A model server that dies once its container runs fails at once, since the same config would fail on any machine, unless its log shows the host's network failed it (a dropped connection or timeout while downloading weights): that counts as a host failure too. Needs `lifecycle.cleanup_on_setup_failure`; without it the first failed host is kept for inspection. |
92
+ | `host_attempts` | `3` | How many machines `setup` tries. A host that fails to boot (the provider reports it failed, it vanishes, or it is not running within `health.boot_timeout`) is destroyed, replaced from fresh offers (each held to the budget), and remembered in `$GPU_HOME/bad_hosts.json` so no setup rents it again for 24h. A model server that dies once its container runs fails at once, since the same config would fail on any machine, unless its log shows the host's network failed it (a dropped connection or timeout while downloading weights): that counts as a host failure too. Needs `lifecycle.cleanup_on_setup_failure`; without it the first failed host is kept for inspection. |
93
93
  | `image` | – | Override the runtime's container image. vLLM's default is `vllm/vllm-openai:v0.32.0`. Pin a tag: `validate` warns on `latest` or an untagged image, because the CUDA it needs can change under you. On Vast, `vastai/vllm:v0.32.0-cuda-13.0` (the same vLLM, packaged by Vast) is often cached on the host and boots in minutes where a 12 GB Docker Hub pull can stall. |
94
94
 
95
95
  ---
@@ -15,16 +15,17 @@ The app never has to stop the GPU. The watchdog on the machine stops it once not
15
15
  pipx install jambu-gpu # or: uv tool install jambu-gpu / pip install jambu-gpu
16
16
  cd /srv/my-app # the directory the app runs gpu from
17
17
  gpu init --model Qwen/Qwen2.5-7B-Instruct
18
- echo "VAST_API_KEY=..." >> .env # or export it, or ~/.jambu/credentials
18
+ echo "VAST_API_KEY=..." >> .env # or export it, or ~/.gpu/credentials
19
19
  gpu validate # fails here, not in production, on a bad config
20
20
  gpu sweep --install-timer # optional safety net, see USAGE.md
21
21
  ```
22
22
 
23
23
  Requirements:
24
24
 
25
- - `jambu-gpu` 0.2.0 or later, on Python 3.10+. The app can be in any language.
26
- - The directory holding `jambu.yaml` must be **persistent**. `gpu` keeps what it rented in `.jambu/state.json` next to it. In a container, mount it as a volume: a container that loses `.jambu/` forgets its instance, rents a second one, and leaves the first to the watchdog and the sweep.
27
- - `.jambu/state.json` holds the endpoint key and the watchdog token. It is written `0600`. Add `.jambu/` and `.env` to `.gitignore` and keep them out of images.
25
+ - `jambu-gpu` 0.3.0 or later, on Python 3.10+. The app can be in any language.
26
+ - The directory holding `jambu.yaml` must be **persistent**. `gpu` keeps what it rented in `.gpu/state.json` next to it. In a container, mount it as a volume: a container that loses `.gpu/` forgets its instance, rents a second one, and leaves the first to the watchdog and the sweep.
27
+ - Before 0.3.0 these directories were `.jambu/` and `~/.jambu`. The first command after an upgrade renames them, so an instance already rented is kept, not rented again.
28
+ - `.gpu/state.json` holds the endpoint key and the watchdog token. It is written `0600`. Add `.gpu/` and `.env` to `.gitignore` and keep them out of images.
28
29
 
29
30
  Tune the lifecycle in `jambu.yaml` for the app. For a service with bursty traffic, a longer idle timeout avoids paying a restart on every burst:
30
31
 
@@ -218,7 +219,7 @@ Run `gpu --json setup` as a subprocess, parse the JSON, and point the language's
218
219
  - **The endpoint requires a key.** Unless `runtime.api_key` is set, `setup` generates one per machine (43 random URL-safe characters) and starts vLLM with it. Requests without it get `401`. `/health` and `/metrics` stay open: they reveal no data and the watchdog reads `/metrics`. Instances provisioned before this existed run without a key until they are recreated.
219
220
  - **The endpoint is plain HTTP** on a public IP and port. The key and the prompts travel unencrypted. For sensitive data, put the app and the GPU on a private network or a tunnel, or front the endpoint with a TLS proxy.
220
221
  - **The watchdog holds the provider key** on the instance so it can stop its own machine. A restricted key is the safer choice; see [LIFECYCLE.md](LIFECYCLE.md).
221
- - `jambu.yaml` never holds credentials. `.env` and `.jambu/` do not belong in version control.
222
+ - `jambu.yaml` never holds credentials. `.env` and `.gpu/` do not belong in version control.
222
223
 
223
224
  ## 6. Troubleshooting
224
225
 
@@ -105,7 +105,7 @@ gpu
105
105
  │
106
106
  ├── Lifecycle core/policy.py the only shutdown rule
107
107
  ├── Runtime runtime/vllm.py how the model runs
108
- └── State core/state.py .jambu/state.json
108
+ └── State core/state.py .gpu/state.json
109
109
  │
110
110
  Provider contract providers/base.py
111
111
  │
@@ -111,9 +111,9 @@ Vast.ai instances
111
111
 
112
112
  ### Every project by its own rules
113
113
 
114
- `gpu setup` (and `gpu run`, which can provision) registers the project's absolute config path in `~/.jambu/projects.json` (`$JAMBU_HOME` overrides `~/.jambu`). `gpu sweep` then works in two steps:
114
+ `gpu setup` (and `gpu run`, which can provision) registers the project's absolute config path in `~/.gpu/projects.json` (`$GPU_HOME` overrides `~/.gpu`; a `~/.jambu` from before 0.3.0 is renamed to `~/.gpu` on first use, and `$JAMBU_HOME` is still read). `gpu sweep` then works in two steps:
115
115
 
116
- 1. **Tracked instances.** For every registered project whose config still loads, it runs that project's own `gpu guard`: its own `jambu.yaml` and `.jambu/state.json`, so its own `max_lifetime`, lock, session start, `stopped_ttl` and budget. The project's events land in its own log. A registered project whose config file is gone is dropped from the registry. The project in the current directory, or the one given with `--config`, is guarded too for that run.
116
+ 1. **Tracked instances.** For every registered project whose config still loads, it runs that project's own `gpu guard`: its own `jambu.yaml` and `.gpu/state.json`, so its own `max_lifetime`, lock, session start, `stopped_ttl` and budget. The project's events land in its own log. A registered project whose config file is gone is dropped from the registry. The project in the current directory, or the one given with `--config`, is guarded too for that run.
117
117
  2. **Orphans.** An instance no project tracks has no known policy and nobody knows whether it is in use, so the rules are blunt but never destroy a running machine:
118
118
 
119
119
  | Orphan | Rule | Action |
@@ -134,7 +134,7 @@ $ gpu sweep
134
134
  ! 1 action(s) pending; re-run with --apply
135
135
  ```
136
136
 
137
- Providers do not report when an instance stopped, so the sweep keeps the time it first saw each orphan stopped in `~/.jambu/sweep.json`. An entry goes away when the instance runs again, disappears, or is tracked by a project. `stopped_ttl` for an orphan therefore counts from the first sweep that saw it stopped, never from a project's state. Every orphan action is recorded as a `sweep_action` event with its reason in `~/.jambu/logs/events.jsonl`. Credentials for the orphan step resolve the usual way (the environment, `.env` in the working directory, `~/.jambu/credentials`), then from the `.env` of registered projects.
137
+ Providers do not report when an instance stopped, so the sweep keeps the time it first saw each orphan stopped in `~/.gpu/sweep.json`. An entry goes away when the instance runs again, disappears, or is tracked by a project. `stopped_ttl` for an orphan therefore counts from the first sweep that saw it stopped, never from a project's state. Every orphan action is recorded as a `sweep_action` event with its reason in `~/.gpu/logs/events.jsonl`. Credentials for the orphan step resolve the usual way (the environment, `.env` in the working directory, `~/.gpu/credentials`), then from the `.env` of registered projects.
138
138
 
139
139
  ### Run it on a timer
140
140
 
@@ -143,9 +143,9 @@ gpu sweep --install-timer # every 5m; --every 15m to change
143
143
  systemctl --user daemon-reload && systemctl --user enable --now jambu-gpu-sweep.timer
144
144
  ```
145
145
 
146
- `--install-timer` writes `~/.config/systemd/user/jambu-gpu-sweep.service` and `.timer`, and registers the current project if there is one. The service is a single `ExecStart=<python> -m jambu_gpu sweep --apply` with the interpreter that installed it, run from `~`: which projects get guarded comes from the registry, not from the working directory, so installing from a second project changes nothing in the unit. The oneshot has a 10 minute start timeout, a failing sweep shows as a failed unit, and `JAMBU_HOME` is passed through when it is set. A unit written by an older version, with one `guard` line per project, is rewritten and those projects are registered.
146
+ `--install-timer` writes `~/.config/systemd/user/jambu-gpu-sweep.service` and `.timer`, and registers the current project if there is one. The service is a single `ExecStart=<python> -m jambu_gpu sweep --apply` with the interpreter that installed it, run from `~`: which projects get guarded comes from the registry, not from the working directory, so installing from a second project changes nothing in the unit. The oneshot has a 10 minute start timeout, a failing sweep shows as a failed unit, and `GPU_HOME` is passed through when it is set. A unit written by an older version, with one `guard` line per project, is rewritten and those projects are registered.
147
147
 
148
- A user unit does not inherit your shell's environment. `--install-timer` therefore refuses unless the provider key is in a file: the project `.env` or `~/.jambu/credentials`. A key that is only exported in your shell is not enough. A user timer needs no root, but it stops at logout unless lingering is on (`loginctl enable-linger $USER`).
148
+ A user unit does not inherit your shell's environment. `--install-timer` therefore refuses unless the provider key is in a file: the project `.env` or `~/.gpu/credentials`. A key that is only exported in your shell is not enough. A user timer needs no root, but it stops at logout unless lingering is on (`loginctl enable-linger $USER`).
149
149
 
150
150
  ```bash
151
151
  gpu sweep --uninstall-timer
@@ -6,7 +6,7 @@ Any Node/TypeScript application using a GPU that `gpu` rents and guards, through
6
6
 
7
7
  ## Run it
8
8
 
9
- From the directory that holds `jambu.yaml` and `VAST_API_KEY` (in `.env` or the environment), with `jambu-gpu` 0.2.0 or later on `PATH`:
9
+ From the directory that holds `jambu.yaml` and `VAST_API_KEY` (in `.env` or the environment), with `jambu-gpu` 0.3.0 or later on `PATH`:
10
10
 
11
11
  ```bash
12
12
  npm --prefix examples/typescript-client install
@@ -1,5 +1,5 @@
1
1
  """gpu: provider-agnostic GPU runtime provisioning and lifecycle CLI."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -118,7 +118,7 @@ def fmt_money(value: Optional[float], suffix: str = "") -> Optional[str]:
118
118
 
119
119
 
120
120
  def register_project(context: Context) -> None:
121
- """Put this project in $JAMBU_HOME/projects.json so `gpu sweep` guards it.
121
+ """Put this project in $GPU_HOME/projects.json so `gpu sweep` guards it.
122
122
 
123
123
  Done before provisioning: a setup that dies halfway must still leave an
124
124
  instance the sweep judges by this project's policy, not as an orphan.
@@ -57,7 +57,7 @@ class Account:
57
57
 
58
58
 
59
59
  def account_credentials(project_dir: Path, profile: str = "default") -> CredentialResolver:
60
- """The environment, ./.env, ~/.jambu/credentials, then registered projects' .env.
60
+ """The environment, ./.env, ~/.gpu/credentials, then registered projects' .env.
61
61
 
62
62
  The timer runs from ~ without the shell's environment; a key kept only in
63
63
  a project's .env must still reach the account-wide orphan sweep.
@@ -196,7 +196,7 @@ def _require_file_credentials(context: Context) -> None:
196
196
  raise ConfigError(
197
197
  f"{name} is {where}, so the timer could not use it for {who}: a systemd "
198
198
  f"user unit does not inherit your shell. Put `{name}=...` in "
199
- "~/.jambu/credentials (chmod 600), then re-run --install-timer."
199
+ "~/.gpu/credentials (chmod 600), then re-run --install-timer."
200
200
  )
201
201
 
202
202
 
@@ -19,7 +19,7 @@ app = typer.Typer(
19
19
  name="gpu",
20
20
  help=(
21
21
  "Provider-agnostic CLI for provisioning and managing temporary GPU runtimes.\n\n"
22
- "jambu.yaml is the desired-state source of truth; .jambu/state.json is what "
22
+ "jambu.yaml is the desired-state source of truth; .gpu/state.json is what "
23
23
  "was last observed."
24
24
  ),
25
25
  no_args_is_help=True,
@@ -1,9 +1,9 @@
1
1
  """systemd *user* units that run `gpu sweep --apply` on a timer.
2
2
 
3
3
  One ``ExecStart`` covers every project: the sweep guards each project in
4
- ``$JAMBU_HOME/projects.json`` with its own config, so the unit does not depend
4
+ ``$GPU_HOME/projects.json`` with its own config, so the unit does not depend
5
5
  on its working directory. A user unit needs no root, but it does not inherit
6
- the shell's environment: credentials must live in ``~/.jambu/credentials`` or
6
+ the shell's environment: credentials must live in ``~/.gpu/credentials`` or
7
7
  the project ``.env``. Writing the files is all this module does; enabling them
8
8
  is left to the printed ``systemctl`` command.
9
9
  """
@@ -64,7 +64,7 @@ def render_units(
64
64
  f"TimeoutStartSec={START_TIMEOUT}",
65
65
  ]
66
66
  if jambu_home:
67
- lines.append(_environment("JAMBU_HOME", jambu_home))
67
+ lines.append(_environment("GPU_HOME", jambu_home))
68
68
  # No "-" prefix: a failing sweep must show up as a failed unit.
69
69
  lines.append("ExecStart=" + " ".join(_quote(a) for a in [python, "-m", "jambu_gpu", "sweep", "--apply"]))
70
70
  lines.append("")
@@ -115,7 +115,9 @@ def install_timer(every_s: float, python: Optional[str] = None) -> dict:
115
115
  service_path, timer_path = unit_paths()
116
116
  legacy = legacy_guard_configs(service_path)
117
117
  service, timer = render_units(
118
- every_s=every_s, python=python, jambu_home=os.environ.get("JAMBU_HOME") or None
118
+ every_s=every_s,
119
+ python=python,
120
+ jambu_home=os.environ.get("GPU_HOME") or os.environ.get("JAMBU_HOME") or None,
119
121
  )
120
122
  service_path.parent.mkdir(parents=True, exist_ok=True)
121
123
  service_path.write_text(service)
@@ -33,12 +33,12 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
33
33
 
34
34
  from .durations import parse_duration
35
35
  from .errors import ConfigError
36
+ from .paths import project_state_dir
36
37
 
37
38
  # jambu.yaml (namespaced, shared with other Jambu Lab tools) is searched
38
39
  # first; config.yml (flat, gpu-runtime-only) is the legacy fallback.
39
40
  CONFIG_FILENAMES = ("jambu.yaml", "jambu.yml", "config.yml", "config.yaml")
40
41
  GPU_RUNTIME_KEY = "gpu_runtime"
41
- STATE_DIRNAME = ".jambu"
42
42
 
43
43
  # The model catalog (spec: "Choosing a model" doc) - a project's jambu.yaml
44
44
  # references one entry by name instead of redeclaring the whole model/compute/
@@ -319,7 +319,7 @@ class RuntimeConfig(_Base):
319
319
 
320
320
  @property
321
321
  def state_dir(self) -> Path:
322
- return self.project_dir / STATE_DIRNAME
322
+ return project_state_dir(self.project_dir)
323
323
 
324
324
  def fingerprint(self) -> str:
325
325
  """Identity of the *desired* runtime.
@@ -1,7 +1,7 @@
1
1
  """Credential resolution (spec section 5).
2
2
 
3
3
  Credentials never live in jambu.yaml. They are resolved from, in order:
4
- environment variables, a project-local ``.env``, then ``~/.jambu/credentials``.
4
+ environment variables, a project-local ``.env``, then ``~/.gpu/credentials``.
5
5
  The resolution strategy can grow (keychain, secret managers, CI secrets)
6
6
  without touching jambu.yaml or any adapter.
7
7
  """
@@ -12,6 +12,8 @@ import os
12
12
  from pathlib import Path
13
13
  from typing import Optional
14
14
 
15
+ from .paths import gpu_home
16
+
15
17
  _DOTENV_CACHE: dict[Path, dict[str, str]] = {}
16
18
 
17
19
 
@@ -46,7 +48,7 @@ class CredentialResolver:
46
48
  self.profile = profile
47
49
  self.sources: list[tuple[str, Path]] = [
48
50
  ("project .env", self.project_dir / ".env"),
49
- ("user credentials", Path.home() / ".jambu" / "credentials"),
51
+ ("user credentials", gpu_home() / "credentials"),
50
52
  *(extra_sources or []),
51
53
  ]
52
54
 
@@ -1,6 +1,6 @@
1
1
  """Structured lifecycle events (spec section 24).
2
2
 
3
- Every lifecycle action appends one JSON object to ``.jambu/logs/events.jsonl``
3
+ Every lifecycle action appends one JSON object to ``.gpu/logs/events.jsonl``
4
4
  so unexpected GPU cost can always be traced back to a decision and a reason.
5
5
  """
6
6
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  A host that could not pull the image or reach the network fails the next
4
4
  setup the same way, and the cheapest-offer search keeps picking it. Each
5
- failure is kept for ``BAD_HOST_TTL_S`` in ``$JAMBU_HOME/bad_hosts.json``.
5
+ failure is kept for ``BAD_HOST_TTL_S`` in ``$GPU_HOME/bad_hosts.json``.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
@@ -0,0 +1,42 @@
1
+ """Where gpu keeps its files: ``.gpu/`` in a project, ``~/.gpu`` per user.
2
+
3
+ Both were named ``.jambu`` before 0.3.0. A legacy directory is renamed in
4
+ place the first time it is needed, so an instance a project already tracks
5
+ is never forgotten (and never rented twice) after an upgrade.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+
13
+ STATE_DIRNAME = ".gpu"
14
+ LEGACY_STATE_DIRNAME = ".jambu"
15
+
16
+
17
+ def adopt_legacy(path: Path, legacy: Path) -> Path:
18
+ """``path``, after moving ``legacy`` there when only the legacy one exists."""
19
+ if not path.exists() and legacy.is_dir():
20
+ try:
21
+ legacy.rename(path)
22
+ except OSError:
23
+ # Read-only or racing another gpu process: keep using the old one.
24
+ return path if path.exists() else legacy
25
+ return path
26
+
27
+
28
+ def project_state_dir(project_dir: Path) -> Path:
29
+ project_dir = Path(project_dir)
30
+ return adopt_legacy(project_dir / STATE_DIRNAME, project_dir / LEGACY_STATE_DIRNAME)
31
+
32
+
33
+ def gpu_home() -> Path:
34
+ """Per-user files: the project registry, sweep sightings, bad hosts, credentials.
35
+
36
+ ``$GPU_HOME`` overrides it (``$JAMBU_HOME`` is still honored).
37
+ """
38
+ override = os.environ.get("GPU_HOME") or os.environ.get("JAMBU_HOME")
39
+ if override:
40
+ return Path(override).expanduser()
41
+ home = Path.home()
42
+ return adopt_legacy(home / STATE_DIRNAME, home / LEGACY_STATE_DIRNAME)
@@ -1,6 +1,6 @@
1
1
  """Observed runtime state (spec section 9).
2
2
 
3
- jambu.yaml is the desired-state SoT; ``.jambu/state.json`` is what we last
3
+ jambu.yaml is the desired-state SoT; ``.gpu/state.json`` is what we last
4
4
  observed. This module owns reading/writing it atomically, plus the on-disk
5
5
  process lock that keeps two CLI invocations from provisioning twice.
6
6
  """
@@ -312,7 +312,7 @@ class RuntimeState:
312
312
 
313
313
 
314
314
  class StateStore:
315
- """Atomic reader/writer for ``.jambu/state.json``."""
315
+ """Atomic reader/writer for ``.gpu/state.json``."""
316
316
 
317
317
  def __init__(self, state_dir: Path) -> None:
318
318
  self._depth = 0 # flock is per-open-file-description: keep it reentrant
@@ -4,7 +4,7 @@ The watchdog guards one instance from the inside and `gpu guard` guards the
4
4
  one instance a project tracks. The sweep covers every project on this machine
5
5
  and whatever is left over:
6
6
 
7
- - every project registered in ``$JAMBU_HOME/projects.json`` (by `gpu setup`)
7
+ - every project registered in ``$GPU_HOME/projects.json`` (by `gpu setup`)
8
8
  gets its own ``Engine.guard_tick``, with its own config and state: its own
9
9
  max_lifetime, lock, session and stopped_ttl. Those instances are *tracked*;
10
10
  - an instance no project tracks is an *orphan*. Nobody knows its policy or
@@ -14,7 +14,7 @@ and whatever is left over:
14
14
  ago is destroyed.
15
15
 
16
16
  Providers do not report when an instance stopped, so the first time the sweep
17
- saw each orphan stopped is kept in ``$JAMBU_HOME/sweep.json``.
17
+ saw each orphan stopped is kept in ``$GPU_HOME/sweep.json``.
18
18
  """
19
19
 
20
20
  from __future__ import annotations
@@ -30,7 +30,8 @@ from typing import Any, Callable, Iterator, Optional
30
30
 
31
31
  from . import events as ev
32
32
  from . import policy
33
- from .config import STATE_DIRNAME, load_config
33
+ from .config import load_config
34
+ from .paths import gpu_home, project_state_dir
34
35
  from .errors import JambuError, ProviderError
35
36
  from .events import EventLog
36
37
  from .models import Instance, InstanceState, is_jambu_label
@@ -46,8 +47,7 @@ SWEEP_ACTION = "sweep_action"
46
47
 
47
48
 
48
49
  def jambu_home() -> Path:
49
- override = os.environ.get("JAMBU_HOME")
50
- return Path(override).expanduser() if override else Path.home() / ".jambu"
50
+ return gpu_home()
51
51
 
52
52
 
53
53
  class SweepRegistry:
@@ -198,7 +198,7 @@ class ProjectGuard:
198
198
 
199
199
  def _state_beside(config_path: Path) -> Optional[RuntimeState]:
200
200
  """The project's state read straight from disk (no API call)."""
201
- store = StateStore(Path(config_path).parent / STATE_DIRNAME)
201
+ store = StateStore(project_state_dir(Path(config_path).parent))
202
202
  try:
203
203
  return store.load() if store.exists() else None
204
204
  except JambuError:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "jambu-gpu"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Provider-agnostic CLI for provisioning and managing temporary GPU runtimes"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -36,7 +36,7 @@ gpu = "jambu_gpu.cli.main:main"
36
36
  packages = ["jambu_gpu"]
37
37
 
38
38
  [tool.hatch.build.targets.sdist]
39
- exclude = ["/.env", "/.venv", "/.jambu"]
39
+ exclude = ["/.env", "/.venv", "/.gpu", "/.jambu"]
40
40
 
41
41
  [tool.pytest.ini_options]
42
42
  testpaths = ["tests"]
@@ -29,9 +29,10 @@ BASE_CONFIG = {
29
29
 
30
30
  @pytest.fixture(autouse=True)
31
31
  def jambu_home(tmp_path, monkeypatch):
32
- """Keep the project registry and sweep state out of the real ~/.jambu."""
33
- home = tmp_path / "jambu-home"
34
- monkeypatch.setenv("JAMBU_HOME", str(home))
32
+ """Keep the project registry and sweep state out of the real ~/.gpu."""
33
+ home = tmp_path / "gpu-home"
34
+ monkeypatch.setenv("GPU_HOME", str(home))
35
+ monkeypatch.delenv("JAMBU_HOME", raising=False)
35
36
  return home
36
37
 
37
38
 
@@ -100,7 +100,7 @@ def test_fingerprint_tracks_runtime_identity(write_config):
100
100
 
101
101
 
102
102
  def test_state_dir_sits_next_to_the_config(config):
103
- assert config.state_dir == config.source_path.parent / ".jambu"
103
+ assert config.state_dir == config.source_path.parent / ".gpu"
104
104
 
105
105
 
106
106
  # -- jambu.yaml shared-manifest namespacing ----------------------------------
@@ -0,0 +1,50 @@
1
+ """Files live in .gpu/ and ~/.gpu; a legacy .jambu is adopted, never forgotten."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from jambu_gpu.core.paths import gpu_home, project_state_dir
6
+ from jambu_gpu.core.state import RuntimeState, StateStore
7
+
8
+
9
+ def test_a_new_project_keeps_its_state_in_dot_gpu(tmp_path):
10
+ assert project_state_dir(tmp_path) == tmp_path / ".gpu"
11
+
12
+
13
+ def test_a_project_from_before_0_3_keeps_its_instance(tmp_path):
14
+ StateStore(tmp_path / ".jambu").save(RuntimeState(provider="vast", instance_id="42", state="ready"))
15
+
16
+ state_dir = project_state_dir(tmp_path)
17
+
18
+ assert state_dir == tmp_path / ".gpu"
19
+ assert not (tmp_path / ".jambu").exists()
20
+ assert StateStore(state_dir).load().instance_id == "42"
21
+
22
+
23
+ def test_both_present_means_dot_gpu_wins_and_the_legacy_is_left_alone(tmp_path):
24
+ (tmp_path / ".gpu").mkdir()
25
+ (tmp_path / ".jambu").mkdir()
26
+ assert project_state_dir(tmp_path) == tmp_path / ".gpu"
27
+ assert (tmp_path / ".jambu").is_dir()
28
+
29
+
30
+ def test_the_user_home_moves_from_dot_jambu(tmp_path, monkeypatch):
31
+ monkeypatch.delenv("GPU_HOME", raising=False)
32
+ monkeypatch.delenv("JAMBU_HOME", raising=False)
33
+ monkeypatch.setenv("HOME", str(tmp_path))
34
+ (tmp_path / ".jambu").mkdir()
35
+ (tmp_path / ".jambu" / "projects.json").write_text("[]")
36
+
37
+ assert gpu_home() == tmp_path / ".gpu"
38
+ assert (tmp_path / ".gpu" / "projects.json").read_text() == "[]"
39
+
40
+
41
+ def test_gpu_home_and_the_legacy_jambu_home_override_it(tmp_path, monkeypatch):
42
+ monkeypatch.delenv("GPU_HOME", raising=False)
43
+ monkeypatch.setenv("JAMBU_HOME", str(tmp_path / "old"))
44
+ assert gpu_home() == tmp_path / "old"
45
+ monkeypatch.setenv("GPU_HOME", str(tmp_path / "new"))
46
+ assert gpu_home() == tmp_path / "new"
47
+
48
+
49
+ def test_a_config_resolves_its_state_dir_to_dot_gpu(config):
50
+ assert config.state_dir == config.project_dir / ".gpu"
@@ -11,7 +11,7 @@ from jambu_gpu.core.state import RuntimeState, StateStore, WorkloadRecord, utc_n
11
11
 
12
12
  @pytest.fixture
13
13
  def store(tmp_path) -> StateStore:
14
- return StateStore(tmp_path / ".jambu")
14
+ return StateStore(tmp_path / ".gpu")
15
15
 
16
16
 
17
17
  def test_empty_state_is_not_an_error(store):
@@ -549,8 +549,8 @@ def home(tmp_path, monkeypatch):
549
549
 
550
550
 
551
551
  @pytest.fixture
552
- def credentials_file(home):
553
- path = home / ".jambu" / "credentials"
552
+ def credentials_file(home, jambu_home):
553
+ path = jambu_home / "credentials"
554
554
  path.parent.mkdir(parents=True)
555
555
  path.write_text("VAST_API_KEY=from-file\n")
556
556
  return path
@@ -570,7 +570,7 @@ def test_install_timer_writes_one_unit_independent_of_the_project(
570
570
  assert execs == [f"ExecStart={sys.executable} -m jambu_gpu sweep --apply"] # no "-"
571
571
  assert "WorkingDirectory=~" in service
572
572
  assert "TimeoutStartSec=10min" in service
573
- assert f'Environment="JAMBU_HOME={jambu_home}"' in service
573
+ assert f'Environment="GPU_HOME={jambu_home}"' in service
574
574
  assert "Type=oneshot" in service
575
575
  assert "OnUnitActiveSec=600s" in timer
576
576
  assert "WantedBy=timers.target" in timer
@@ -623,7 +623,7 @@ def test_install_timer_refuses_a_key_only_in_the_shell_environment(config, home,
623
623
  result = runner.invoke(app, ["sweep", "--install-timer"])
624
624
  assert result.exit_code == 2
625
625
  message = " ".join(result.output.split())
626
- assert "~/.jambu/credentials" in message and "VAST_API_KEY" in message
626
+ assert "~/.gpu/credentials" in message and "VAST_API_KEY" in message
627
627
  assert not (home / ".config/systemd/user/jambu-gpu-sweep.service").exists()
628
628
 
629
629
 
@@ -641,7 +641,7 @@ def test_unit_values_escape_percent_and_dollar_and_quote_spaces():
641
641
  every_s=300, python="/opt/my env/100%/py$thon", jambu_home="/data/50% \"x\""
642
642
  )
643
643
  assert 'ExecStart="/opt/my env/100%%/py$$thon" -m jambu_gpu sweep --apply' in service
644
- assert 'Environment="JAMBU_HOME=/data/50%% \\"x\\""' in service
644
+ assert 'Environment="GPU_HOME=/data/50%% \\"x\\""' in service
645
645
  assert "Environment" not in timer_mod.render_units(every_s=300, python="/p")[0]
646
646
 
647
647
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes