jambu-gpu 0.1.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 (80) hide show
  1. jambu_gpu-0.1.0/.env.example +11 -0
  2. jambu_gpu-0.1.0/.gitignore +15 -0
  3. jambu_gpu-0.1.0/CONTRIBUTING.md +75 -0
  4. jambu_gpu-0.1.0/LICENSE +21 -0
  5. jambu_gpu-0.1.0/PKG-INFO +82 -0
  6. jambu_gpu-0.1.0/README.md +59 -0
  7. jambu_gpu-0.1.0/docs/ACCEPTANCE.md +28 -0
  8. jambu_gpu-0.1.0/docs/ADDING_A_PROVIDER.md +121 -0
  9. jambu_gpu-0.1.0/docs/CHOOSING_A_MODEL.md +198 -0
  10. jambu_gpu-0.1.0/docs/CONFIG.md +223 -0
  11. jambu_gpu-0.1.0/docs/LIFECYCLE.md +75 -0
  12. jambu_gpu-0.1.0/docs/USAGE.md +91 -0
  13. jambu_gpu-0.1.0/docs/assets/jambu-logo.jpg +0 -0
  14. jambu_gpu-0.1.0/examples/experiment.py +48 -0
  15. jambu_gpu-0.1.0/examples/jambu.yaml +36 -0
  16. jambu_gpu-0.1.0/examples/mastra-agent/README.md +70 -0
  17. jambu_gpu-0.1.0/examples/mastra-agent/package-lock.json +2235 -0
  18. jambu_gpu-0.1.0/examples/mastra-agent/package.json +19 -0
  19. jambu_gpu-0.1.0/examples/mastra-agent/src/agent.ts +42 -0
  20. jambu_gpu-0.1.0/examples/mastra-agent/src/index.ts +11 -0
  21. jambu_gpu-0.1.0/examples/mastra-agent/src/tools/weather-tool.ts +36 -0
  22. jambu_gpu-0.1.0/examples/mastra-agent/tsconfig.json +15 -0
  23. jambu_gpu-0.1.0/jambu.example.yaml +77 -0
  24. jambu_gpu-0.1.0/jambu.models.yaml +102 -0
  25. jambu_gpu-0.1.0/jambu.yaml +42 -0
  26. jambu_gpu-0.1.0/jambu_gpu/__init__.py +5 -0
  27. jambu_gpu-0.1.0/jambu_gpu/__main__.py +4 -0
  28. jambu_gpu-0.1.0/jambu_gpu/cli/__init__.py +0 -0
  29. jambu_gpu-0.1.0/jambu_gpu/cli/_common.py +117 -0
  30. jambu_gpu-0.1.0/jambu_gpu/cli/inspect.py +207 -0
  31. jambu_gpu-0.1.0/jambu_gpu/cli/lifecycle.py +127 -0
  32. jambu_gpu-0.1.0/jambu_gpu/cli/main.py +103 -0
  33. jambu_gpu-0.1.0/jambu_gpu/cli/providers.py +96 -0
  34. jambu_gpu-0.1.0/jambu_gpu/cli/run.py +37 -0
  35. jambu_gpu-0.1.0/jambu_gpu/cli/setup.py +165 -0
  36. jambu_gpu-0.1.0/jambu_gpu/cli/status.py +214 -0
  37. jambu_gpu-0.1.0/jambu_gpu/cli/validate.py +57 -0
  38. jambu_gpu-0.1.0/jambu_gpu/core/__init__.py +0 -0
  39. jambu_gpu-0.1.0/jambu_gpu/core/beacon.py +124 -0
  40. jambu_gpu-0.1.0/jambu_gpu/core/config.py +596 -0
  41. jambu_gpu-0.1.0/jambu_gpu/core/credentials.py +75 -0
  42. jambu_gpu-0.1.0/jambu_gpu/core/durations.py +47 -0
  43. jambu_gpu-0.1.0/jambu_gpu/core/engine.py +951 -0
  44. jambu_gpu-0.1.0/jambu_gpu/core/errors.py +68 -0
  45. jambu_gpu-0.1.0/jambu_gpu/core/events.py +115 -0
  46. jambu_gpu-0.1.0/jambu_gpu/core/execution.py +169 -0
  47. jambu_gpu-0.1.0/jambu_gpu/core/health.py +62 -0
  48. jambu_gpu-0.1.0/jambu_gpu/core/model_inspect.py +368 -0
  49. jambu_gpu-0.1.0/jambu_gpu/core/models.py +283 -0
  50. jambu_gpu-0.1.0/jambu_gpu/core/policy.py +233 -0
  51. jambu_gpu-0.1.0/jambu_gpu/core/state.py +361 -0
  52. jambu_gpu-0.1.0/jambu_gpu/core/tool_parsers.py +63 -0
  53. jambu_gpu-0.1.0/jambu_gpu/providers/__init__.py +4 -0
  54. jambu_gpu-0.1.0/jambu_gpu/providers/base.py +125 -0
  55. jambu_gpu-0.1.0/jambu_gpu/providers/registry.py +63 -0
  56. jambu_gpu-0.1.0/jambu_gpu/providers/vast/__init__.py +3 -0
  57. jambu_gpu-0.1.0/jambu_gpu/providers/vast/client.py +230 -0
  58. jambu_gpu-0.1.0/jambu_gpu/providers/vast/config.py +51 -0
  59. jambu_gpu-0.1.0/jambu_gpu/providers/vast/mapper.py +221 -0
  60. jambu_gpu-0.1.0/jambu_gpu/providers/vast/provider.py +422 -0
  61. jambu_gpu-0.1.0/jambu_gpu/runtime/__init__.py +6 -0
  62. jambu_gpu-0.1.0/jambu_gpu/runtime/base.py +99 -0
  63. jambu_gpu-0.1.0/jambu_gpu/runtime/vllm.py +224 -0
  64. jambu_gpu-0.1.0/jambu_gpu/watchdog/__init__.py +3 -0
  65. jambu_gpu-0.1.0/jambu_gpu/watchdog/agent_body.py +547 -0
  66. jambu_gpu-0.1.0/jambu_gpu/watchdog/builder.py +178 -0
  67. jambu_gpu-0.1.0/pyproject.toml +47 -0
  68. jambu_gpu-0.1.0/tests/conftest.py +72 -0
  69. jambu_gpu-0.1.0/tests/fakes.py +156 -0
  70. jambu_gpu-0.1.0/tests/test_cli.py +240 -0
  71. jambu_gpu-0.1.0/tests/test_config.py +322 -0
  72. jambu_gpu-0.1.0/tests/test_engine.py +366 -0
  73. jambu_gpu-0.1.0/tests/test_integration.py +289 -0
  74. jambu_gpu-0.1.0/tests/test_model_inspect.py +219 -0
  75. jambu_gpu-0.1.0/tests/test_policy.py +157 -0
  76. jambu_gpu-0.1.0/tests/test_runtime_vllm.py +168 -0
  77. jambu_gpu-0.1.0/tests/test_state.py +155 -0
  78. jambu_gpu-0.1.0/tests/test_vast_mapper.py +119 -0
  79. jambu_gpu-0.1.0/tests/test_vast_provider.py +298 -0
  80. jambu_gpu-0.1.0/tests/test_watchdog.py +327 -0
@@ -0,0 +1,11 @@
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=
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .jambu/
9
+ .env
10
+ .env.*
11
+ !.env.example
12
+ .pytest_cache/
13
+ .coverage
14
+ node_modules/
15
+ *.tsbuildinfo
@@ -0,0 +1,75 @@
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`, `.jambu/state.json`, or provider instance
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.
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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jambu.ai and João Gabriel Lima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.5
2
+ Name: jambu-gpu
3
+ Version: 0.1.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
+ The API key is read from the environment, a project `.env`, or `~/.jambu/credentials`. It never goes in the manifest.
49
+
50
+ ## Declare
51
+
52
+ ```yaml
53
+ gpu_runtime:
54
+ provider: {name: vast}
55
+ model_profile: qwythos-9b
56
+ lifecycle:
57
+ idle_timeout: 15m
58
+ max_lifetime: 6h
59
+ budget:
60
+ max_hourly_cost_usd: 1.50
61
+ ```
62
+
63
+ 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).
64
+
65
+ ## Docs
66
+
67
+ - [Commands, workloads, and agents](docs/USAGE.md)
68
+ - [Every config field](docs/CONFIG.md)
69
+ - [Choosing a model](docs/CHOOSING_A_MODEL.md)
70
+ - [How shutdown works](docs/LIFECYCLE.md)
71
+ - [Adding a provider](docs/ADDING_A_PROVIDER.md)
72
+ - [Contributing](CONTRIBUTING.md)
73
+
74
+ ## License
75
+
76
+ [MIT](LICENSE).
77
+
78
+ <p align="center">
79
+ <a href="https://jambu.ai">
80
+ <img src="docs/assets/jambu-logo.jpg" alt="Jambu.ai — Applied Intelligence Lab" width="280">
81
+ </a>
82
+ </p>
@@ -0,0 +1,59 @@
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
+ The API key is read from the environment, a project `.env`, or `~/.jambu/credentials`. It never goes in the manifest.
26
+
27
+ ## Declare
28
+
29
+ ```yaml
30
+ gpu_runtime:
31
+ provider: {name: vast}
32
+ model_profile: qwythos-9b
33
+ lifecycle:
34
+ idle_timeout: 15m
35
+ max_lifetime: 6h
36
+ budget:
37
+ max_hourly_cost_usd: 1.50
38
+ ```
39
+
40
+ 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).
41
+
42
+ ## Docs
43
+
44
+ - [Commands, workloads, and agents](docs/USAGE.md)
45
+ - [Every config field](docs/CONFIG.md)
46
+ - [Choosing a model](docs/CHOOSING_A_MODEL.md)
47
+ - [How shutdown works](docs/LIFECYCLE.md)
48
+ - [Adding a provider](docs/ADDING_A_PROVIDER.md)
49
+ - [Contributing](CONTRIBUTING.md)
50
+
51
+ ## License
52
+
53
+ [MIT](LICENSE).
54
+
55
+ <p align="center">
56
+ <a href="https://jambu.ai">
57
+ <img src="docs/assets/jambu-logo.jpg" alt="Jambu.ai — Applied Intelligence Lab" width="280">
58
+ </a>
59
+ </p>
@@ -0,0 +1,28 @@
1
+ # Acceptance criteria — where each one is implemented and tested
2
+
3
+ Twelve 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 `.jambu/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
+
20
+ ## The MVP command surface
21
+
22
+ `jambu.yaml` schema, CLI, provider contract, provider registry, Vast.ai adapter,
23
+ vLLM runtime, `setup`, `run`, `status`, `stop`, `destroy`, `lock`/`unlock`, idle
24
+ timeout, max lifetime, remote watchdog, structured logs — all present.
25
+
26
+ Deliberately not implemented: multi-instance scheduling, multi-GPU
27
+ orchestration, Kubernetes, distributed inference, provider optimization, automatic
28
+ provider selection, job queues, web dashboard.
@@ -0,0 +1,121 @@
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.
56
+
57
+ ## 3. Normalize state and errors
58
+
59
+ Map every provider status onto `InstanceState`
60
+ (`provisioning / starting / ready / busy / idle / stopped / failed / destroyed`),
61
+ and raise only the normalized errors from `core/errors.py`:
62
+ `AuthenticationError`, `CapacityUnavailableError`, `ProvisioningError`,
63
+ `ProviderTimeoutError`, `UnsupportedCapabilityError`. CLI behaviour must be
64
+ identical across providers.
65
+
66
+ ## 4. Supply the watchdog actions
67
+
68
+ The lifecycle *policy* is the core's; the adapter contributes only the two calls
69
+ that shut the machine down from inside itself:
70
+
71
+ ```python
72
+ WATCHDOG_ACTIONS = '''
73
+ def provider_stop(ctx):
74
+ ... # stdlib only: urllib, json, os
75
+
76
+ def provider_destroy(ctx):
77
+ ...
78
+ '''
79
+
80
+ def watchdog_spec(self, spec_or_instance=None):
81
+ return WatchdogSpec(
82
+ actions_source=WATCHDOG_ACTIONS,
83
+ env={"RUNPOD_API_KEY": self.credentials.get("RUNPOD_API_KEY") or ""},
84
+ context={"provider": self.name},
85
+ )
86
+ ```
87
+
88
+ The instance id may not exist when the boot script is generated. Either resolve it
89
+ from the container's own environment (as the Vast adapter does with
90
+ `VAST_CONTAINERLABEL`), or rely on the CLI's `/identify` call — support both if you
91
+ can, so the guard works even if the CLI never reconnects.
92
+
93
+ Return `None` only if the provider genuinely cannot self-terminate; users then have
94
+ to run `gpu guard` externally, and `validate` will warn.
95
+
96
+ ## 5. Publish the ports
97
+
98
+ `ComputeSpec.ports` lists the container ports the core needs published. Map them to
99
+ `Instance.endpoints` under the names `model` and `watchdog`. Without a published
100
+ watchdog port the remote guard is unreachable.
101
+
102
+ ## 6. Register it
103
+
104
+ ```python
105
+ # providers/registry.py
106
+ def load_builtin_providers():
107
+ from .vast.provider import VastProvider
108
+ from .runpod.provider import RunpodProvider
109
+
110
+ registry.register(VastProvider)
111
+ registry.register(RunpodProvider)
112
+ return registry
113
+ ```
114
+
115
+ ## 7. Test it
116
+
117
+ Copy `tests/test_vast_provider.py` and swap the mocked payloads. Then check the
118
+ adapter really is swappable — `tests/test_engine.py` drives the whole engine
119
+ through `tests/fakes.py::FakeProvider` with zero core changes, and
120
+ `test_the_core_never_branches_on_a_provider_name` fails the build if `core/` learns
121
+ a provider's name.
@@ -0,0 +1,198 @@
1
+ # Where jambu.yaml's model-specific values come from
2
+
3
+ Every value in `model:`/`compute.gpu:`/`runtime:` that depends on *which model*
4
+ you're deploying (VRAM, dtype, context length, tool-call parser,
5
+ `trust_remote_code`) has a real source. This is that source, and the command
6
+ that reads it for you.
7
+
8
+ ## `gpu inspect-model`
9
+
10
+ ```bash
11
+ gpu inspect-model Qwen/Qwen2.5-7B-Instruct
12
+ ```
13
+
14
+ ```text
15
+ Qwen/Qwen2.5-7B-Instruct @ main
16
+ architecture Qwen2ForCausalLM (model_type: qwen2)
17
+ parameters 7.62B (BF16)
18
+ weight size ~15.2 GB -> suggested min_vram_gb: 22
19
+ context native 32768
20
+ tool calling hermes
21
+
22
+ Sources
23
+ https://huggingface.co/api/models/Qwen/Qwen2.5-7B-Instruct
24
+ https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/raw/main/config.json
25
+
26
+ Suggested gpu_runtime: snippet
27
+ gpu_runtime:
28
+ model:
29
+ id: Qwen/Qwen2.5-7B-Instruct
30
+
31
+ compute:
32
+ gpu:
33
+ min_vram_gb: 22 # estimate, see notes
34
+ count: 1
35
+
36
+ runtime:
37
+ engine: vllm
38
+ context_length: 32768
39
+ tool_calling: auto # resolves to hermes automatically
40
+ ```
41
+
42
+ Paste the snippet under `gpu_runtime:` in your `jambu.yaml`, adjust
43
+ `lifecycle`/`health`/`budget` for your situation, and you have a starting
44
+ config. Add `--json` for machine-readable output, `--revision` for a
45
+ non-`main` branch/tag, `--hf-token-env` if the repo is gated and your token
46
+ lives under a different env var than `HF_TOKEN`.
47
+
48
+ **This is a starting point, not an oracle.** It never talks to Vast.ai and
49
+ never boots anything — it only reads two public HuggingFace documents and does
50
+ arithmetic. Confirm the real number with `gpu offers` (finds actual
51
+ capacity at that size) and a real `gpu setup` (the only way to know a
52
+ model genuinely boots and serves).
53
+
54
+ ## The two documents, and exactly what each field feeds
55
+
56
+ ### 1. `https://huggingface.co/api/models/<repo>` — the Hub API
57
+
58
+ The **real, on-disk** shape of the checkpoint — more reliable than anything in
59
+ prose, because it's read from the actual safetensors file headers, not
60
+ declared by the model's author.
61
+
62
+ | JSON field | Feeds | Notes |
63
+ | --- | --- | --- |
64
+ | `safetensors.total` | `compute.gpu.min_vram_gb` (via weight size) | Total parameter count. Absent for GGUF-only repos — size manually if so. |
65
+ | `safetensors.parameters` | `runtime.dtype`, byte width for the VRAM estimate | `{"BF16": N, "F8_E4M3": M}` style breakdown. `inspect-model` picks the dtype with the most parameters — a few embedding/norm tensors often stay bf16 even in an FP8 release. |
66
+ | `gated` | whether you need `model.hf_token_env` | `false` / `"auto"` / `"manual"` — any non-`false` value means a real token is required, not just recommended. |
67
+ | `tags`, `pipeline_tag` | sanity check | Confirms it's actually a text-generation checkpoint before you provision a GPU for it. |
68
+
69
+ ### 2. `https://huggingface.co/<repo>/raw/<revision>/config.json` — the model's own HF config
70
+
71
+ The architecture's own declaration of itself — what `transformers`/vLLM read
72
+ to know how to load it.
73
+
74
+ | JSON field | Feeds | Notes |
75
+ | --- | --- | --- |
76
+ | `architectures` | recognizing the model family | e.g. `Qwen3_5ForConditionalGeneration`. Cross-reference against [vLLM's supported models](https://docs.vllm.ai/en/latest/models/supported_models/) — an unlisted architecture will fail to load regardless of VRAM. |
77
+ | `model_type` | hybrid-attention detection | gpu flags known linear-attention families (`qwen3_5`/`qwen3_6`/`qwen3_8`, `jamba`, `mamba`, `bamba`, `deltanet`/`gdn` in the string, ...) — these need an **Ampere-or-newer** GPU for their Triton/FLA kernels. Confirmed the hard way: a Turing card (Quadro RTX 8000) failed to boot vLLM at all for a `qwen3_5` model; Ampere (RTX A6000) worked immediately. |
78
+ | `torch_dtype` | `runtime.dtype`, VRAM estimate (fallback) | Used only when the Hub API's `safetensors.parameters` is absent — the API's real on-disk dtype is preferred when both exist, since a repo can ship a different dtype than it trained in. |
79
+ | `quantization_config.quant_method` | VRAM estimate, `runtime.dtype: auto` | `fp8`/`awq`/`gptq`/`bitsandbytes_4bit` change the byte width per parameter; when present, set `runtime.dtype: auto` and let vLLM read the quantization from the checkpoint instead of forcing a dtype that fights it. |
80
+ | `max_position_embeddings` | `runtime.context_length` | The model's *native* window. `inspect-model` suggests `min(native, 32768)` — the native number is often a YaRN-extended marketing ceiling (e.g. 1,048,576), not a sane default: allocating KV cache for the full window is neither necessary nor cheap for typical agentic/tool-use turns. |
81
+ | `auto_map` | `model.trust_remote_code` | Its *presence* (regardless of value) is the actual signal `transformers` uses to know a repo ships custom modeling code. Absence doesn't guarantee you never need `trust_remote_code: true` — some very new architectures need it even with native library support during a transition period — but presence is a reliable "you definitely do." |
82
+ | `rope_scaling` | context length ceiling sanity-check | Confirms whether a large `max_position_embeddings` is native or extended (YaRN/linear/dynamic) — extended windows cost more KV cache per token than the base architecture would suggest. |
83
+
84
+ ### Tool-call parser: a third, unwritten source
85
+
86
+ There is no machine-readable field anywhere that says "use `--tool-call-parser
87
+ X`" — this is `jambu_gpu/core/tool_parsers.py`'s own hand-maintained table,
88
+ matched against vLLM's [tool calling
89
+ docs](https://docs.vllm.ai/en/latest/features/tool_calling/) and, sometimes,
90
+ empirical testing when the docs don't cover a model family yet (the Qwen3.5+
91
+ `qwen3_xml` entry was added after a live test showed `hermes` left the model's
92
+ tool call as inert text in `content` instead of a parsed `tool_calls` array).
93
+
94
+ Consequences:
95
+
96
+ - It only matches on **repo name substrings** (`"qwen3.5"`, `"llama-3"`, ...).
97
+ A fine-tune with a custom name (`empero-ai/Qwythos-9B-v2`, based on Qwen3.5)
98
+ won't be recognized even though the underlying format is known — set
99
+ `runtime.tool_call_parser` explicitly once you know the base model.
100
+ - It can be wrong or incomplete for anything genuinely new. If `inspect-model`
101
+ reports no parser guessed, **or an explicit one you set doesn't actually
102
+ produce a `tool_calls` array**, the fix is the same: pick a candidate
103
+ parser (start from the model's base-architecture family, e.g. Qwen3.8 → try
104
+ what worked for Qwen3.5), restart the server with `--enable-auto-tool-choice
105
+ --tool-call-parser <candidate>`, and send one real tool-calling request —
106
+ a working parser puts the call in `message.tool_calls`, not `message.content`.
107
+ See [`docs/ADDING_A_PROVIDER.md`](ADDING_A_PROVIDER.md) for how the fingerprint
108
+ interacts with runtime-only changes like this if you're doing it through
109
+ `gpu setup` rather than by hand.
110
+ - When you do confirm one empirically, add it to `_TOOL_PARSER_HINTS` in
111
+ `jambu_gpu/core/tool_parsers.py` (a one-line PR) so `tool_calling: auto`
112
+ picks it up for everyone next time, the same way the Qwen3.5+ finding did.
113
+
114
+ ## Don't write a new jambu.yaml per model - use the catalog
115
+
116
+ Every model you add this way is tempting to just copy the whole file: same
117
+ provider, same lifecycle, same budget, only `model:`/`compute.gpu:` actually
118
+ differ. Do that a handful of times and a repo ends up with
119
+ `jambu.qwythos.yaml`, `jambu.model-b.yaml`, `jambu.model-c.yaml`... duplicated
120
+ again in every sub-project directory that wants one - a pile of near-identical
121
+ files where changing a lifecycle default means editing all of them.
122
+
123
+ The fix: split "what varies by model" (the catalog) from "what varies by
124
+ project" (the active `jambu.yaml`), and reference the former by name.
125
+
126
+ ```yaml
127
+ # jambu.models.yaml - one entry per model, shared by every project in the
128
+ # repo (found by walking up from a project's jambu.yaml, same as jambu.yaml
129
+ # itself is found - one catalog can sit at the repo root and serve every
130
+ # sub-project under it).
131
+ qwythos-9b:
132
+ model: {id: empero-ai/Qwythos-9B-v2, trust_remote_code: true}
133
+ compute: {gpu: {min_vram_gb: 40, name_filter: A6000}}
134
+ runtime: {tool_calling: "on", tool_call_parser: qwen3_xml}
135
+
136
+ qwen3.8-27b-uncensored:
137
+ model: {id: orcarouter/Qwen3.8-27B-Uncensored-FP8}
138
+ compute: {gpu: {min_vram_gb: 44, name_filter: A6000}}
139
+ ```
140
+
141
+ ```yaml
142
+ # jambu.yaml - just this project's policy + which model
143
+ gpu_runtime:
144
+ version: 1
145
+ model_profile: qwythos-9b # the only thing that changes to switch models
146
+ lifecycle: {idle_timeout: 15m, max_lifetime: 6h}
147
+ budget: {max_hourly_cost_usd: 1.50}
148
+ ```
149
+
150
+ The project's own fields always win over the profile's - a profile is a set
151
+ of defaults, not a lock, so a project can still override just one leaf
152
+ (`compute.gpu.min_vram_gb: 80`, say) without redeclaring the whole model.
153
+
154
+ `gpu inspect-model <repo> --add-profile <name>` writes straight into
155
+ the catalog (creating it if it doesn't exist) instead of printing a snippet
156
+ to paste into a new file - the research and the config land in the same
157
+ command. `gpu profiles` lists what's in it.
158
+
159
+ This is the same base+overlay pattern as docker-compose's `extends`, Helm
160
+ `values.yaml`, or Kustomize overlays - one place owns "what", each consumer
161
+ only says "which one, and what's different here."
162
+
163
+ **When a full standalone file still makes sense:** a genuinely one-off
164
+ experiment nobody else will reuse, or a config for a completely different
165
+ project that will never share this repo's catalog. Most "I need to try model
166
+ X" situations are the catalog case, not this one.
167
+
168
+ ## Dynamic / un-modeled vLLM flags
169
+
170
+ `jambu.yaml`'s schema deliberately models the fields that affect *lifecycle,
171
+ cost, and provider portability* — it does not, and cannot, keep up with every
172
+ flag vLLM ships (vLLM adds flags faster than any fixed contract could track).
173
+ For everything else, `runtime:` has two escape hatches, applied in this order
174
+ (later wins on conflict, matching how vLLM's own argparse takes the last
175
+ occurrence of a repeated flag):
176
+
177
+ ```yaml
178
+ runtime:
179
+ # Structured: {flag: value} -> --flag value. Handles the common shapes:
180
+ # true -> a bare flag --enable-prefix-caching
181
+ # false -> the negated flag --no-enforce-eager
182
+ # a dict/list -> JSON-encoded (several vLLM flags take JSON)
183
+ # --limit-mm-per-prompt '{"image":0}'
184
+ # anything else -> --flag str(value) --max-num-seqs 64
185
+ vllm_args:
186
+ max_num_seqs: 64
187
+ enable-prefix-caching: true
188
+
189
+ # Raw argv, appended last, verbatim - for anything the structured form
190
+ # can't express (repeated flags, exotic syntax).
191
+ extra_args: ["--kv-cache-dtype", "fp8"]
192
+ ```
193
+
194
+ Setting a flag gpu already manages through another field (`--model`,
195
+ `--port`, `--enable-auto-tool-choice`, ...) via `vllm_args` still works — it
196
+ wins, since it's applied after those — but `gpu validate` warns about it,
197
+ because the config field that's supposed to control that behavior (e.g.
198
+ `runtime.tool_calling`) will silently stop reflecting what's actually running.