jambu-gpu 0.4.0__tar.gz → 0.5.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 (107) hide show
  1. jambu_gpu-0.5.0/PKG-INFO +27 -0
  2. jambu_gpu-0.5.0/README.md +12 -0
  3. jambu_gpu-0.5.0/pyproject.toml +24 -0
  4. jambu_gpu-0.4.0/.env.example +0 -11
  5. jambu_gpu-0.4.0/.gitignore +0 -17
  6. jambu_gpu-0.4.0/CONTRIBUTING.md +0 -75
  7. jambu_gpu-0.4.0/PKG-INFO +0 -90
  8. jambu_gpu-0.4.0/README.md +0 -67
  9. jambu_gpu-0.4.0/docs/ACCEPTANCE.md +0 -85
  10. jambu_gpu-0.4.0/docs/ADDING_A_PROVIDER.md +0 -132
  11. jambu_gpu-0.4.0/docs/AGENTS.md +0 -56
  12. jambu_gpu-0.4.0/docs/CHOOSING_A_MODEL.md +0 -198
  13. jambu_gpu-0.4.0/docs/CONFIG.md +0 -241
  14. jambu_gpu-0.4.0/docs/INTEGRATION.md +0 -233
  15. jambu_gpu-0.4.0/docs/LIFECYCLE.md +0 -128
  16. jambu_gpu-0.4.0/docs/USAGE.md +0 -156
  17. jambu_gpu-0.4.0/docs/assets/jambu-logo.jpg +0 -0
  18. jambu_gpu-0.4.0/examples/experiment.py +0 -48
  19. jambu_gpu-0.4.0/examples/jambu.yaml +0 -36
  20. jambu_gpu-0.4.0/examples/mastra-agent/README.md +0 -70
  21. jambu_gpu-0.4.0/examples/mastra-agent/package-lock.json +0 -2235
  22. jambu_gpu-0.4.0/examples/mastra-agent/package.json +0 -19
  23. jambu_gpu-0.4.0/examples/mastra-agent/src/agent.ts +0 -42
  24. jambu_gpu-0.4.0/examples/mastra-agent/src/index.ts +0 -11
  25. jambu_gpu-0.4.0/examples/mastra-agent/src/tools/weather-tool.ts +0 -36
  26. jambu_gpu-0.4.0/examples/mastra-agent/tsconfig.json +0 -15
  27. jambu_gpu-0.4.0/examples/typescript-client/README.md +0 -44
  28. jambu_gpu-0.4.0/examples/typescript-client/package-lock.json +0 -71
  29. jambu_gpu-0.4.0/examples/typescript-client/package.json +0 -18
  30. jambu_gpu-0.4.0/examples/typescript-client/src/index.ts +0 -31
  31. jambu_gpu-0.4.0/examples/typescript-client/src/jambuGpu.ts +0 -178
  32. jambu_gpu-0.4.0/examples/typescript-client/tsconfig.json +0 -16
  33. jambu_gpu-0.4.0/jambu.example.yaml +0 -88
  34. jambu_gpu-0.4.0/jambu.models.yaml +0 -102
  35. jambu_gpu-0.4.0/jambu.yaml +0 -42
  36. jambu_gpu-0.4.0/jambu_gpu/__init__.py +0 -5
  37. jambu_gpu-0.4.0/jambu_gpu/__main__.py +0 -4
  38. jambu_gpu-0.4.0/jambu_gpu/agent/__init__.py +0 -1
  39. jambu_gpu-0.4.0/jambu_gpu/agent/install.py +0 -173
  40. jambu_gpu-0.4.0/jambu_gpu/agent/jobs.py +0 -193
  41. jambu_gpu-0.4.0/jambu_gpu/agent/mcp.py +0 -646
  42. jambu_gpu-0.4.0/jambu_gpu/agent/skill/SKILL.md +0 -68
  43. jambu_gpu-0.4.0/jambu_gpu/cli/__init__.py +0 -0
  44. jambu_gpu-0.4.0/jambu_gpu/cli/_common.py +0 -131
  45. jambu_gpu-0.4.0/jambu_gpu/cli/account.py +0 -330
  46. jambu_gpu-0.4.0/jambu_gpu/cli/agent.py +0 -74
  47. jambu_gpu-0.4.0/jambu_gpu/cli/inspect.py +0 -207
  48. jambu_gpu-0.4.0/jambu_gpu/cli/lifecycle.py +0 -127
  49. jambu_gpu-0.4.0/jambu_gpu/cli/main.py +0 -151
  50. jambu_gpu-0.4.0/jambu_gpu/cli/providers.py +0 -96
  51. jambu_gpu-0.4.0/jambu_gpu/cli/run.py +0 -38
  52. jambu_gpu-0.4.0/jambu_gpu/cli/setup.py +0 -164
  53. jambu_gpu-0.4.0/jambu_gpu/cli/status.py +0 -224
  54. jambu_gpu-0.4.0/jambu_gpu/cli/timer.py +0 -143
  55. jambu_gpu-0.4.0/jambu_gpu/cli/validate.py +0 -57
  56. jambu_gpu-0.4.0/jambu_gpu/core/__init__.py +0 -0
  57. jambu_gpu-0.4.0/jambu_gpu/core/beacon.py +0 -133
  58. jambu_gpu-0.4.0/jambu_gpu/core/config.py +0 -636
  59. jambu_gpu-0.4.0/jambu_gpu/core/credentials.py +0 -95
  60. jambu_gpu-0.4.0/jambu_gpu/core/durations.py +0 -47
  61. jambu_gpu-0.4.0/jambu_gpu/core/engine.py +0 -1302
  62. jambu_gpu-0.4.0/jambu_gpu/core/errors.py +0 -94
  63. jambu_gpu-0.4.0/jambu_gpu/core/events.py +0 -117
  64. jambu_gpu-0.4.0/jambu_gpu/core/execution.py +0 -169
  65. jambu_gpu-0.4.0/jambu_gpu/core/health.py +0 -62
  66. jambu_gpu-0.4.0/jambu_gpu/core/hosts.py +0 -58
  67. jambu_gpu-0.4.0/jambu_gpu/core/model_inspect.py +0 -368
  68. jambu_gpu-0.4.0/jambu_gpu/core/models.py +0 -313
  69. jambu_gpu-0.4.0/jambu_gpu/core/paths.py +0 -42
  70. jambu_gpu-0.4.0/jambu_gpu/core/policy.py +0 -264
  71. jambu_gpu-0.4.0/jambu_gpu/core/state.py +0 -406
  72. jambu_gpu-0.4.0/jambu_gpu/core/sweep.py +0 -478
  73. jambu_gpu-0.4.0/jambu_gpu/core/tool_parsers.py +0 -63
  74. jambu_gpu-0.4.0/jambu_gpu/providers/__init__.py +0 -4
  75. jambu_gpu-0.4.0/jambu_gpu/providers/base.py +0 -137
  76. jambu_gpu-0.4.0/jambu_gpu/providers/registry.py +0 -63
  77. jambu_gpu-0.4.0/jambu_gpu/providers/vast/__init__.py +0 -3
  78. jambu_gpu-0.4.0/jambu_gpu/providers/vast/client.py +0 -255
  79. jambu_gpu-0.4.0/jambu_gpu/providers/vast/config.py +0 -56
  80. jambu_gpu-0.4.0/jambu_gpu/providers/vast/mapper.py +0 -292
  81. jambu_gpu-0.4.0/jambu_gpu/providers/vast/provider.py +0 -438
  82. jambu_gpu-0.4.0/jambu_gpu/runtime/__init__.py +0 -6
  83. jambu_gpu-0.4.0/jambu_gpu/runtime/base.py +0 -184
  84. jambu_gpu-0.4.0/jambu_gpu/runtime/vllm.py +0 -295
  85. jambu_gpu-0.4.0/jambu_gpu/watchdog/__init__.py +0 -3
  86. jambu_gpu-0.4.0/jambu_gpu/watchdog/agent_body.py +0 -959
  87. jambu_gpu-0.4.0/jambu_gpu/watchdog/builder.py +0 -248
  88. jambu_gpu-0.4.0/pyproject.toml +0 -47
  89. jambu_gpu-0.4.0/tests/conftest.py +0 -81
  90. jambu_gpu-0.4.0/tests/fakes.py +0 -203
  91. jambu_gpu-0.4.0/tests/test_agent.py +0 -348
  92. jambu_gpu-0.4.0/tests/test_cli.py +0 -304
  93. jambu_gpu-0.4.0/tests/test_config.py +0 -390
  94. jambu_gpu-0.4.0/tests/test_engine.py +0 -848
  95. jambu_gpu-0.4.0/tests/test_engine_sessions.py +0 -445
  96. jambu_gpu-0.4.0/tests/test_integration.py +0 -289
  97. jambu_gpu-0.4.0/tests/test_model_inspect.py +0 -219
  98. jambu_gpu-0.4.0/tests/test_paths.py +0 -50
  99. jambu_gpu-0.4.0/tests/test_policy.py +0 -232
  100. jambu_gpu-0.4.0/tests/test_runtime_vllm.py +0 -263
  101. jambu_gpu-0.4.0/tests/test_state.py +0 -187
  102. jambu_gpu-0.4.0/tests/test_sweep.py +0 -680
  103. jambu_gpu-0.4.0/tests/test_vast_mapper.py +0 -276
  104. jambu_gpu-0.4.0/tests/test_vast_provider.py +0 -438
  105. jambu_gpu-0.4.0/tests/test_watchdog.py +0 -1130
  106. jambu_gpu-0.4.0/uv.lock +0 -870
  107. {jambu_gpu-0.4.0 → jambu_gpu-0.5.0}/LICENSE +0 -0
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.5
2
+ Name: jambu-gpu
3
+ Version: 0.5.0
4
+ Summary: Renamed to gpu4agents (GPU for Agents). This release installs gpu4agents.
5
+ Project-URL: Homepage, https://gpu4agents.jambu.ai
6
+ Project-URL: Repository, https://github.com/jambuai/gpu4agents
7
+ Author: Jambu.ai
8
+ Author-email: João Gabriel Lima <joao.gabriel@jambu.ai>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 7 - Inactive
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: gpu4agents>=0.5.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # jambu-gpu is now gpu4agents
17
+
18
+ This project was renamed to **GPU for Agents**: [`gpu4agents`](https://pypi.org/project/gpu4agents/).
19
+
20
+ This release has no code; it installs `gpu4agents`, which provides the same `gpu` command, reads the same `jambu.yaml` and finds the same machines.
21
+
22
+ ```bash
23
+ pipx uninstall jambu-gpu && pipx install gpu4agents
24
+ # or: pip install -U gpu4agents
25
+ ```
26
+
27
+ Website: https://gpu4agents.jambu.ai · Code: https://github.com/jambuai/gpu4agents
@@ -0,0 +1,12 @@
1
+ # jambu-gpu is now gpu4agents
2
+
3
+ This project was renamed to **GPU for Agents**: [`gpu4agents`](https://pypi.org/project/gpu4agents/).
4
+
5
+ This release has no code; it installs `gpu4agents`, which provides the same `gpu` command, reads the same `jambu.yaml` and finds the same machines.
6
+
7
+ ```bash
8
+ pipx uninstall jambu-gpu && pipx install gpu4agents
9
+ # or: pip install -U gpu4agents
10
+ ```
11
+
12
+ Website: https://gpu4agents.jambu.ai · Code: https://github.com/jambuai/gpu4agents
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "jambu-gpu"
7
+ version = "0.5.0"
8
+ description = "Renamed to gpu4agents (GPU for Agents). This release installs gpu4agents."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Jambu.ai" },
14
+ { name = "João Gabriel Lima", email = "joao.gabriel@jambu.ai" },
15
+ ]
16
+ dependencies = ["gpu4agents>=0.5.0"]
17
+ classifiers = ["Development Status :: 7 - Inactive"]
18
+
19
+ [project.urls]
20
+ Homepage = "https://gpu4agents.jambu.ai"
21
+ Repository = "https://github.com/jambuai/gpu4agents"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ bypass-selection = true
@@ -1,11 +0,0 @@
1
- # Credentials are resolved from the environment, this file, or
2
- # ~/.jambu/credentials - never from jambu.yaml.
3
-
4
- # Required by the vast provider.
5
- VAST_API_KEY=
6
-
7
- # Optional: needed for gated HuggingFace models.
8
- HF_TOKEN=
9
-
10
- # Optional: per-profile override, used when provider.profile: staging
11
- # VAST_API_KEY__staging=
@@ -1,17 +0,0 @@
1
- __pycache__/
2
- *.py[cod]
3
- .venv/
4
- venv/
5
- dist/
6
- build/
7
- *.egg-info/
8
- .gpu/
9
- .jambu/
10
- .env
11
- .env.*
12
- !.env.example
13
- .pytest_cache/
14
- .coverage
15
- node_modules/
16
- *.tsbuildinfo
17
- .claude/
@@ -1,75 +0,0 @@
1
- # Contributing
2
-
3
- `jambu-gpu` provisions a temporary GPU runtime and guarantees it stops billing
4
- when nothing is using it. Contributions are welcome. By submitting a change you
5
- agree it is licensed under the [MIT License](LICENSE).
6
-
7
- ## Command name
8
-
9
- The published command is `gpu`. The package name stays `jambu-gpu`.
10
-
11
- Do not add a second console script. One command, in `pyproject.toml`:
12
-
13
- ```toml
14
- [project.scripts]
15
- gpu = "jambu_gpu.cli.main:main"
16
- ```
17
-
18
- ## Setup
19
-
20
- ```bash
21
- python3 -m venv .venv
22
- .venv/bin/pip install -e ".[dev]"
23
- ```
24
-
25
- Python 3.10 or newer. Tests do not need `VAST_API_KEY`, a GPU, or network access.
26
-
27
- ## Tests
28
-
29
- ```bash
30
- .venv/bin/pytest
31
- .venv/bin/pytest tests/test_policy.py # shutdown rule
32
- .venv/bin/pytest tests/test_watchdog.py # generated guard, as a real process
33
- .venv/bin/pytest tests/test_integration.py # setup -> boot -> run -> auto-stop
34
- ```
35
-
36
- A change to lifecycle behaviour needs a test that fails before the change and
37
- passes after it. The policy in `jambu_gpu/core/policy.py` and the watchdog that
38
- runs on the instance must keep agreeing: the watchdog inlines that module at
39
- provision time.
40
-
41
- `tests/test_integration.py` does not mock the boot. It runs the onstart script
42
- the engine generated, starts the real watchdog, waits on a real health endpoint,
43
- runs a workload with real heartbeats, and asserts the guard stops the instance
44
- with the right reason.
45
-
46
- ## What you may change
47
-
48
- | Change | Where it goes |
49
- | --- | --- |
50
- | New provider | `jambu_gpu/providers/<name>/` plus one registry line. See [docs/ADDING_A_PROVIDER.md](docs/ADDING_A_PROVIDER.md). |
51
- | Shutdown rule | `jambu_gpu/core/policy.py` and `tests/test_policy.py`. |
52
- | How the model server starts | `jambu_gpu/runtime/`. |
53
- | CLI surface | `jambu_gpu/cli/`. Keep `--json` and `--config` on every command. |
54
-
55
- The core must not branch on a provider name. Branch on `provider.capabilities`.
56
- If a provider feature requires an `if provider == "..."` in `core/`, the contract
57
- is wrong — extend the contract instead.
58
-
59
- Out of scope: multi-instance scheduling, multi-GPU
60
- orchestration, Kubernetes, distributed inference, automatic provider selection,
61
- job queues, a web dashboard. See [docs/ACCEPTANCE.md](docs/ACCEPTANCE.md).
62
-
63
- ## Secrets
64
-
65
- Never commit credentials, `.env`, `.gpu/state.json`, or provider instance
66
- ids from a real run. Keys are read from the environment, a gitignored `.env`,
67
- or `~/.gpu/credentials`. `.env.example` ships with empty values only.
68
-
69
- ## Pull requests
70
-
71
- - Explain why the behaviour should change, not only what moved.
72
- - Run `.venv/bin/pytest` and report the result.
73
- - Keep commits focused. A provider adapter and a policy change are two PRs.
74
- - Match the surrounding style: English identifiers and docs, type hints on new
75
- public functions, no new dependency unless the standard library cannot do the job.
jambu_gpu-0.4.0/PKG-INFO DELETED
@@ -1,90 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: jambu-gpu
3
- Version: 0.4.0
4
- Summary: Provider-agnostic CLI for provisioning and managing temporary GPU runtimes
5
- Project-URL: Homepage, https://github.com/jambuai/jambu-gpu
6
- Project-URL: Repository, https://github.com/jambuai/jambu-gpu
7
- Project-URL: Issues, https://github.com/jambuai/jambu-gpu/issues
8
- Author: Jambu.ai
9
- Author-email: João Gabriel Lima <joao.gabriel@jambu.ai>
10
- License: MIT
11
- License-File: LICENSE
12
- Requires-Python: >=3.10
13
- Requires-Dist: httpx>=0.27.0
14
- Requires-Dist: pydantic>=2.6.0
15
- Requires-Dist: pyyaml>=6.0.1
16
- Requires-Dist: rich>=13.7.0
17
- Requires-Dist: typer>=0.12.0
18
- Provides-Extra: dev
19
- Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
20
- Requires-Dist: pytest>=8.0.0; extra == 'dev'
21
- Requires-Dist: respx>=0.21.0; extra == 'dev'
22
- Description-Content-Type: text/markdown
23
-
24
- # gpu
25
-
26
- Temporary GPU runtimes that stop billing when the work stops.
27
-
28
- You declare the model and how long it may live. `gpu` provisions a machine, starts the model server, and runs your job against it. Shutdown happens on the instance, so closing the laptop does not leave the GPU billing.
29
-
30
- Vast.ai is the first provider. Another provider is a new adapter. The lifecycle rule stays the same.
31
-
32
- ## Run
33
-
34
- ```bash
35
- pip install jambu-gpu
36
-
37
- cd your-experiment
38
- gpu init --model empero-ai/Qwythos-9B-v2
39
- export VAST_API_KEY=...
40
-
41
- gpu validate
42
- gpu setup
43
- gpu run python experiment.py
44
- ```
45
-
46
- When you are finished, `gpu destroy`. If you walk away, the idle timeout does it.
47
-
48
- From an application in any language: `gpu --json setup` returns the OpenAI-compatible `base_url`, `api_key` and `model`, and the app uses any OpenAI SDK. See [Integrating gpu into an application](docs/INTEGRATION.md), with a ready TypeScript client.
49
-
50
- From a coding agent: `gpu agent install` gives Claude Code (or `-t cursor`, `-t desktop`) a skill and local MCP tools to rent a GPU, run the experiment and shut it down. See [Letting a coding agent rent GPUs](docs/AGENTS.md).
51
-
52
- 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).
53
-
54
- The API key is read from the environment, a project `.env`, or `~/.gpu/credentials`. It never goes in the manifest.
55
-
56
- ## Declare
57
-
58
- ```yaml
59
- gpu_runtime:
60
- provider: {name: vast}
61
- model_profile: qwythos-9b
62
- lifecycle:
63
- idle_timeout: 15m
64
- max_lifetime: 6h
65
- budget:
66
- max_hourly_cost_usd: 1.50
67
- ```
68
-
69
- The file is `jambu.yaml`. This tool reads only the `gpu_runtime:` key, so other Jambu tools can share the same file. A full example is [`jambu.example.yaml`](jambu.example.yaml).
70
-
71
- ## Docs
72
-
73
- - [Integrating into an application (any language)](docs/INTEGRATION.md)
74
- - [Letting a coding agent rent GPUs (Claude Code, Cursor, Claude Desktop)](docs/AGENTS.md)
75
- - [Commands, workloads, and agents](docs/USAGE.md)
76
- - [Every config field](docs/CONFIG.md)
77
- - [Choosing a model](docs/CHOOSING_A_MODEL.md)
78
- - [How shutdown works](docs/LIFECYCLE.md)
79
- - [Adding a provider](docs/ADDING_A_PROVIDER.md)
80
- - [Contributing](CONTRIBUTING.md)
81
-
82
- ## License
83
-
84
- [MIT](LICENSE).
85
-
86
- <p align="center">
87
- <a href="https://jambu.ai">
88
- <img src="docs/assets/jambu-logo.jpg" alt="Jambu.ai — Applied Intelligence Lab" width="280">
89
- </a>
90
- </p>
jambu_gpu-0.4.0/README.md DELETED
@@ -1,67 +0,0 @@
1
- # gpu
2
-
3
- Temporary GPU runtimes that stop billing when the work stops.
4
-
5
- You declare the model and how long it may live. `gpu` provisions a machine, starts the model server, and runs your job against it. Shutdown happens on the instance, so closing the laptop does not leave the GPU billing.
6
-
7
- Vast.ai is the first provider. Another provider is a new adapter. The lifecycle rule stays the same.
8
-
9
- ## Run
10
-
11
- ```bash
12
- pip install jambu-gpu
13
-
14
- cd your-experiment
15
- gpu init --model empero-ai/Qwythos-9B-v2
16
- export VAST_API_KEY=...
17
-
18
- gpu validate
19
- gpu setup
20
- gpu run python experiment.py
21
- ```
22
-
23
- When you are finished, `gpu destroy`. If you walk away, the idle timeout does it.
24
-
25
- From an application in any language: `gpu --json setup` returns the OpenAI-compatible `base_url`, `api_key` and `model`, and the app uses any OpenAI SDK. See [Integrating gpu into an application](docs/INTEGRATION.md), with a ready TypeScript client.
26
-
27
- From a coding agent: `gpu agent install` gives Claude Code (or `-t cursor`, `-t desktop`) a skill and local MCP tools to rent a GPU, run the experiment and shut it down. See [Letting a coding agent rent GPUs](docs/AGENTS.md).
28
-
29
- 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).
30
-
31
- The API key is read from the environment, a project `.env`, or `~/.gpu/credentials`. It never goes in the manifest.
32
-
33
- ## Declare
34
-
35
- ```yaml
36
- gpu_runtime:
37
- provider: {name: vast}
38
- model_profile: qwythos-9b
39
- lifecycle:
40
- idle_timeout: 15m
41
- max_lifetime: 6h
42
- budget:
43
- max_hourly_cost_usd: 1.50
44
- ```
45
-
46
- The file is `jambu.yaml`. This tool reads only the `gpu_runtime:` key, so other Jambu tools can share the same file. A full example is [`jambu.example.yaml`](jambu.example.yaml).
47
-
48
- ## Docs
49
-
50
- - [Integrating into an application (any language)](docs/INTEGRATION.md)
51
- - [Letting a coding agent rent GPUs (Claude Code, Cursor, Claude Desktop)](docs/AGENTS.md)
52
- - [Commands, workloads, and agents](docs/USAGE.md)
53
- - [Every config field](docs/CONFIG.md)
54
- - [Choosing a model](docs/CHOOSING_A_MODEL.md)
55
- - [How shutdown works](docs/LIFECYCLE.md)
56
- - [Adding a provider](docs/ADDING_A_PROVIDER.md)
57
- - [Contributing](CONTRIBUTING.md)
58
-
59
- ## License
60
-
61
- [MIT](LICENSE).
62
-
63
- <p align="center">
64
- <a href="https://jambu.ai">
65
- <img src="docs/assets/jambu-logo.jpg" alt="Jambu.ai — Applied Intelligence Lab" width="280">
66
- </a>
67
- </p>
@@ -1,85 +0,0 @@
1
- # Acceptance criteria — where each one is implemented and tested
2
-
3
- Eighteen guarantees, mapped to the code and the test that holds each one.
4
-
5
- | # | Guarantee | Implementation | Test |
6
- | --- | --- | --- | --- |
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
- | 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
- | 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
- | 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` |
11
- | 5 | Active workloads are never stopped by the idle guard | `core/policy.py::evaluate` returns `workload_active` before any idle check | `test_policy.py::test_active_workload_is_never_stopped_by_the_idle_guard`, `test_watchdog.py::test_a_heartbeating_workload_is_never_stopped` |
12
- | 6 | An idle unlocked instance stops within `idle_timeout + watchdog_interval` | remote watchdog loop, `watchdog/agent_body.py::guard_loop` | `test_watchdog.py::test_idle_instance_is_stopped_with_a_recorded_reason` (real subprocess) |
13
- | 7 | `max_lifetime` bounds forgotten instances | `policy.evaluate` checks age before activity | `test_policy.py::test_max_lifetime_stops_even_when_busy`, `test_watchdog.py::test_max_lifetime_stops_a_busy_instance` |
14
- | 8 | Locks prevent shutdown until expiry | `policy.Snapshot.lock_active` / `lock_expired`; TTL required unless `allow_indefinite_lock` | `test_policy.py::test_expired_lock_stops_protecting`, `test_watchdog.py::test_a_lock_prevents_shutdown_until_it_expires` |
15
- | 9 | A crashed CLI does not disable protection | the guard runs *on the instance*; heartbeats go stale after `heartbeat_grace` | `test_watchdog.py::test_a_vanished_heartbeat_falls_back_to_idle`, `::test_startup_grace_expires_when_the_cli_never_confirms` |
16
- | 10 | Remote state is reconciled | `Engine._reconcile` merges provider truth + watchdog truth and repairs local state | `test_engine.py::test_status_repairs_state_when_the_instance_vanished` |
17
- | 11 | A new provider needs only the contract | `providers/registry.py`; the engine is driven end to end by `tests/fakes.py::FakeProvider` | `test_engine.py::test_a_new_provider_needs_only_the_contract` |
18
- | 12 | Every automatic shutdown records its reason | `lifecycle_guard_triggered` + `instance_stopped` events carry `reason`, locally and on the instance | `test_engine.py::test_guard_stops_an_idle_unlocked_instance_and_records_the_reason`, `test_watchdog.py::test_idle_instance_is_stopped_with_a_recorded_reason` |
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
- | 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
- | 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 `~/.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
- | 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
- | 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
-
26
- ## Validated on real hardware
27
-
28
- On 2026-10-24 the lifecycle ran end to end on Vast.ai (RTX 3090, Qwen2.5-0.5B, `idle_timeout: 4m`, `stopped_ttl: 5m`), for $0.35 of Vast credit in total across every attempt:
29
-
30
- | What was checked | What happened |
31
- | --- | --- |
32
- | Traffic is activity | 10 requests sent straight to the endpoint over 7.5 minutes, none through `gpu run`. The machine stayed up past the 4-minute idle timeout. |
33
- | Idle stops | 269 s after the last request the watchdog stopped its own machine (`idle_timeout`). Vast showed it `exited`. |
34
- | A restart is guarded | `gpu run` restarted it. The watchdog logged `session_started` with `terminated: false` and measured `max_lifetime` from the restart. After the workload it stopped the machine again on its own. |
35
- | Stopped storage is reclaimed | 334 s after the stop, `gpu guard` destroyed it (`stopped_ttl`). |
36
- | Failed setups leave nothing | An image built for a newer CUDA, a model server that died at import, a host whose network dropped the weights download, a 30-minute image pull, and a Ctrl-C each ended with the instance destroyed. |
37
- | Bad hosts are skipped | Hosts that could not pull the image were destroyed, excluded and replaced, with never two instances alive at once. |
38
-
39
- The run also found what unit tests could not, and each of these is fixed and covered by a test:
40
- - the boot script was over Vast's 16 KB onstart limit;
41
- - `latest` had moved to CUDA 13;
42
- - the `-cu129` image variant could not load torchcodec;
43
- - a 15-minute boot timeout was too short for a 12 GB pull;
44
- - Ctrl-C did not clean up;
45
- - a fresh contract was reported `READY`.
46
-
47
- ### Application integration (0.2.0)
48
-
49
- Driven end to end by [`examples/typescript-client`](../examples/typescript-client/) through the OpenAI SDK, on an RTX 3090 with Qwen2.5-0.5B:
50
-
51
- | Check | Result |
52
- | --- | --- |
53
- | `ensureGpu()` on an empty account | Two bad hosts (weights download cut by the host's network; image pull stalled 30 minutes) were destroyed and replaced; the third served. 37 minutes in all, one instance alive at a time. |
54
- | The endpoint requires its key | No key: `401`. Wrong key: `401`. The key from `gpu --json setup`: `200`. `/health` without a key: `200`. |
55
- | Chat through the SDK | Answered. |
56
- | Healing after a stop | `stopGpu()`, then a call on the stale connection: it failed to connect, `gpu setup` restarted the same instance, and the retry answered 41 s later with the same key. |
57
-
58
- It found two more bugs, both fixed and tested: Vast reports a stopping instance as `running` for a few seconds, which made a setup right after a stop skip the restart; and a Vast rate limit (`429`) failed setup instead of being waited out.
59
-
60
- ## A coding agent driving it (0.4.0)
61
-
62
- On 2026-10-25 a headless Claude Code with no prior context (`claude -p`) was given a fresh directory holding only `gpu agent install`'s skill and `.mcp.json`, plus `VAST_API_KEY` in `.env`. It was asked in one sentence to test Qwen2.5-0.5B on a rented GPU, ask it the capital of Brazil, report time and cost, and destroy the machine. It loaded the `gpu` skill by itself and called `gpu_init` (with the cap and VRAM from the request), `gpu_validate`, `gpu_offers`, then `gpu_setup` until ready, `curl` for the question, `gpu_status`, `gpu_destroy` and `gpu_ls`. The model answered "Brasília", the account was empty at the end, and the agent reported setup time and cost correctly, noting that `session_cost_usd` leaves out the replaced host.
63
-
64
- | What was checked | What happened |
65
- | --- | --- |
66
- | Setup longer than any tool timeout | `gpu_setup` returned `running` every 300 s for 42 minutes and re-attached to the same background job each time: one machine at a time, never a second setup. |
67
- | Bad hosts | The first host could not pull the image for 30 minutes; setup destroyed it and rented another, which served. |
68
- | Cancel | Over MCP (official Python SDK client), `gpu_destroy` during a setup interrupted it like Ctrl-C: the booting machine was destroyed and `gpu_ls` came back empty, 2.3 s after the call. |
69
- | Protocol | The official MCP Python SDK (2.3.0) client initializes, lists the 13 tools and calls them. |
70
-
71
- The first attempt found what the unit tests had not, each fixed and tested:
72
- - during a setup, `gpu status`, `gpu logs`, `gpu stop` and `gpu destroy` failed on the project lock (reported as "no instance"). Now `status` and `logs` report the last saved state with `busy: true`, the MCP `gpu_stop` / `gpu_destroy` cancel the setup first, and a held lock is its own exit code, 12;
73
- - the host that failed last when setup gave up was not remembered as bad, so the next setup rented it again;
74
- - on Vast, three hosts in a row stalled 30 minutes each pulling the 12 GB Docker Hub image, and setup gave up after 90 minutes with nothing left billing. Vast now defaults to `vastai/vllm:v0.32.0-cuda-13.0`, the same vLLM that Vast's hosts cache: in the cancel test the container was running within a minute.
75
-
76
- ## The MVP command surface
77
-
78
- `jambu.yaml` schema, CLI, provider contract, provider registry, Vast.ai adapter,
79
- vLLM runtime, `setup`, `run`, `status`, `stop`, `destroy`, `lock`/`unlock`, idle
80
- timeout, max lifetime, remote watchdog, structured logs — all present. On top of
81
- it, `ls` and `sweep` for account-wide orphan control.
82
-
83
- Deliberately not implemented: multi-instance scheduling, multi-GPU
84
- orchestration, Kubernetes, distributed inference, provider optimization, automatic
85
- provider selection, job queues, web dashboard.
@@ -1,132 +0,0 @@
1
- # Adding a provider
2
-
3
- Adding a provider means adding `jambu_gpu/providers/<name>/` and one line in the
4
- registry. **No lifecycle, execution or CLI code changes.** If you find yourself
5
- editing `core/`, the contract is leaking.
6
-
7
- ## 1. Implement the contract
8
-
9
- ```python
10
- # jambu_gpu/providers/runpod/provider.py
11
- from ...core.models import ProviderCapabilities
12
- from ..base import GPUProvider, WatchdogSpec
13
-
14
-
15
- class RunpodProvider(GPUProvider):
16
- name = "runpod"
17
- display_name = "RunPod"
18
- required_credentials = ["RUNPOD_API_KEY"]
19
- capabilities = ProviderCapabilities(
20
- stop=True,
21
- destroy=True,
22
- ssh=True,
23
- public_ports=True,
24
- gpu_selection=True,
25
- pricing_query=True,
26
- remote_exec=True,
27
- logs=True,
28
- stop_eliminates_billing=True,
29
- )
30
-
31
- def validate(self, config): ...
32
- def setup(self, spec): ...
33
- def start(self, instance): ...
34
- def run(self, instance, workload): ...
35
- def status(self, instance): ...
36
- def stop(self, instance): ...
37
- def destroy(self, instance): ...
38
- def logs(self, instance): ...
39
- ```
40
-
41
- Suggested layout, mirroring the Vast adapter:
42
-
43
- ```
44
- providers/runpod/
45
- ├── provider.py the contract implementation
46
- ├── client.py raw HTTP; the ONLY place that knows the wire format
47
- ├── mapper.py payloads <-> normalized models, state mapping
48
- └── config.py provider.options schema and constants
49
- ```
50
-
51
- ## 2. Declare capabilities honestly
52
-
53
- The core never branches on `provider.name` — only on `provider.capabilities`. Set
54
- `stop_eliminates_billing=False` if a stopped instance still costs money; the policy
55
- then knows it may need to escalate to destroy, and `lifecycle.stopped_ttl` applies.
56
-
57
- Set `list_instances=True` and implement `list_instances()` if the API can enumerate
58
- the account. Return only instances whose label starts with `JAMBU_LABEL_PREFIX`
59
- (`core/models.py`); `gpu ls` and `gpu sweep` are built on it and stay unavailable
60
- otherwise.
61
-
62
- ## 3. Normalize state and errors
63
-
64
- Map every provider status onto `InstanceState`
65
- (`provisioning / starting / ready / busy / idle / stopped / failed / destroyed`),
66
- and raise only the normalized errors from `core/errors.py`:
67
- `AuthenticationError`, `CapacityUnavailableError`, `ProvisioningError`,
68
- `ProviderTimeoutError`, `UnsupportedCapabilityError`. CLI behaviour must be
69
- identical across providers.
70
-
71
- Report a machine that will not boot as `failed` rather than leaving it in
72
- `starting`: `setup` then destroys it at once and rents another machine
73
- (`compute.host_attempts`). Set `Offer.host_id` and `Instance.host_id` to the
74
- physical machine when the API exposes it, and leave out offers whose `host_id` is
75
- in `ComputeSpec.exclude_hosts`, or `setup` may rent the same bad machine again.
76
-
77
- ## 4. Supply the watchdog actions
78
-
79
- The lifecycle *policy* is the core's; the adapter contributes only the two calls
80
- that shut the machine down from inside itself:
81
-
82
- ```python
83
- WATCHDOG_ACTIONS = '''
84
- def provider_stop(ctx):
85
- ... # stdlib only: urllib, json, os
86
-
87
- def provider_destroy(ctx):
88
- ...
89
- '''
90
-
91
- def watchdog_spec(self, spec_or_instance=None):
92
- return WatchdogSpec(
93
- actions_source=WATCHDOG_ACTIONS,
94
- env={"RUNPOD_API_KEY": self.credentials.get("RUNPOD_API_KEY") or ""},
95
- context={"provider": self.name},
96
- )
97
- ```
98
-
99
- The instance id may not exist when the boot script is generated. Either resolve it
100
- from the container's own environment (as the Vast adapter does with
101
- `VAST_CONTAINERLABEL`), or rely on the CLI's `/identify` call — support both if you
102
- can, so the guard works even if the CLI never reconnects.
103
-
104
- Return `None` only if the provider genuinely cannot self-terminate; users then have
105
- to run `gpu guard` externally, and `validate` will warn.
106
-
107
- ## 5. Publish the ports
108
-
109
- `ComputeSpec.ports` lists the container ports the core needs published. Map them to
110
- `Instance.endpoints` under the names `model` and `watchdog`. Without a published
111
- watchdog port the remote guard is unreachable.
112
-
113
- ## 6. Register it
114
-
115
- ```python
116
- # providers/registry.py
117
- def load_builtin_providers():
118
- from .vast.provider import VastProvider
119
- from .runpod.provider import RunpodProvider
120
-
121
- registry.register(VastProvider)
122
- registry.register(RunpodProvider)
123
- return registry
124
- ```
125
-
126
- ## 7. Test it
127
-
128
- Copy `tests/test_vast_provider.py` and swap the mocked payloads. Then check the
129
- adapter really is swappable — `tests/test_engine.py` drives the whole engine
130
- through `tests/fakes.py::FakeProvider` with zero core changes, and
131
- `test_the_core_never_branches_on_a_provider_name` fails the build if `core/` learns
132
- a provider's name.
@@ -1,56 +0,0 @@
1
- # Letting a coding agent rent GPUs
2
-
3
- `gpu` ships a skill and a local MCP server, so Claude Code, Cursor or Claude Desktop can rent a GPU, serve a model, run an experiment against it and shut it down, from a request like "compare Qwen 7B and Llama 8B on these 50 prompts".
4
-
5
- The watchdog on the machine is what makes this safe to hand to an agent: a machine the agent forgets, or a conversation that ends mid-experiment, stops by itself after `lifecycle.idle_timeout`.
6
-
7
- ## Install
8
-
9
- ```bash
10
- pipx install jambu-gpu # or: uv tool install jambu-gpu
11
- cd your-project
12
- echo "VAST_API_KEY=..." >> .env # or ~/.gpu/credentials; never in jambu.yaml
13
- gpu agent install # Claude Code
14
- gpu agent install -t cursor # Cursor
15
- gpu agent install -t desktop # Claude Desktop
16
- ```
17
-
18
- | Target | Skill | MCP server |
19
- | --- | --- | --- |
20
- | `claude` (default) | `.claude/skills/gpu/SKILL.md` | `.mcp.json` (Claude Code asks once to approve it) |
21
- | `cursor` | the same `.claude/skills/gpu/` (Cursor reads Claude's skill directories) | `.cursor/mcp.json`, with `--project ${workspaceFolder}` |
22
- | `desktop` | `~/.gpu/gpu-skill.zip`, to upload under Settings > Capabilities > Skills | `claude_desktop_config.json`, pinned to this project |
23
-
24
- `--global` installs for every project of the user instead: the skill in `~/.claude/skills/gpu/`, the MCP server through `claude mcp add --scope user` and in `~/.cursor/mcp.json`. Existing config files are merged; only the `gpu` entry is written. Re-running is safe.
25
-
26
- Claude Desktop and Cursor do not get your shell's environment. Keep `VAST_API_KEY` in the project `.env` or `~/.gpu/credentials`, not only in an `export`.
27
-
28
- Another agent that reads `SKILL.md` files: `gpu agent skill > path/to/skills/gpu/SKILL.md`. Any MCP client: run `gpu mcp --project /path/to/project` over stdio.
29
-
30
- ## The MCP tools
31
-
32
- | Tool | Does | Costs money |
33
- | --- | --- | --- |
34
- | `gpu_init` | write `jambu.yaml` for a model, with an optional price cap and VRAM | no |
35
- | `gpu_validate` | check the config and credentials | no |
36
- | `gpu_offers` | cheapest matching machines and their price | no |
37
- | `gpu_setup` | rent / restart and wait for the model; returns `base_url`, `api_key`, `model` | yes |
38
- | `gpu_status` | state, cost so far, idle time, the watchdog's next move, setup progress | no |
39
- | `gpu_connection` | `base_url`, `api_key`, `model` without starting anything | no |
40
- | `gpu_run` | run a local command with `OPENAI_BASE_URL` / `OPENAI_API_KEY` / `JAMBU_MODEL_ID` set | yes, if it has to start the machine |
41
- | `gpu_logs` | runtime, onstart, watchdog or provider log | no |
42
- | `gpu_lock` / `gpu_unlock` | keep the machine up through a quiet period | while locked |
43
- | `gpu_stop` | stop now; disk kept, restart takes about a minute | ends billing for compute |
44
- | `gpu_destroy` | delete; needs `confirm=true` | ends all billing |
45
- | `gpu_ls` | every machine in the account, from any project | no |
46
-
47
- Each tool runs the `gpu` CLI with `--json` in the project directory, so errors carry the same `exit_code` as the [application contract](INTEGRATION.md#exit-codes), plus `meaning` and `next` for the agent. A tool takes an optional `project_dir`; by default it is the server's `--project`, `$GPU_PROJECT_DIR`, or the directory the client started it in.
48
-
49
- `gpu_setup` would outlast any client's tool timeout on a first rent (5–15 minutes). It starts setup as a background job, waits up to `wait_seconds` (45 by default), and reports `status: "running"` until the model answers. Calling it again re-attaches to the same job; it never starts a second machine. The job outlives the MCP server, so a client restarted mid-setup finds it again through `gpu_setup` or `gpu_status`, which answers during setup with `"busy": true` and the job's progress. `gpu_stop` and `gpu_destroy` cancel a setup in progress the way Ctrl-C does: the machine still booting is destroyed.
50
-
51
- ## What keeps the agent in line
52
-
53
- - **The skill** tells the agent to show the price and ask before renting, wait out a slow setup instead of starting over, never touch `VAST_API_KEY`, and offer stop or destroy (and check `gpu_ls`) when done.
54
- - **The server's tool annotations** mark `gpu_destroy` destructive and the read-only tools read-only, so a client can let `gpu_status` run freely and ask for every `gpu_setup` and `gpu_destroy`. In Claude Code that is the `permissions` list in `.claude/settings.json`, e.g. `"allow": ["mcp__gpu__gpu_status", "mcp__gpu__gpu_offers", "mcp__gpu__gpu_ls"]`.
55
- - **`jambu.yaml`** holds the hard limits the agent cannot exceed without editing the file: `budget.max_hourly_cost_usd`, `budget.max_session_cost_usd`, `lifecycle.max_lifetime`.
56
- - **The watchdog** stops the machine after `idle_timeout` whatever the agent does, and `gpu sweep --install-timer` catches anything left behind.