gpu4agents 0.5.0__py3-none-any.whl

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 (59) hide show
  1. gpu4agents/__init__.py +5 -0
  2. gpu4agents/__main__.py +4 -0
  3. gpu4agents/agent/__init__.py +1 -0
  4. gpu4agents/agent/install.py +173 -0
  5. gpu4agents/agent/jobs.py +193 -0
  6. gpu4agents/agent/mcp.py +646 -0
  7. gpu4agents/agent/skill/SKILL.md +68 -0
  8. gpu4agents/cli/__init__.py +0 -0
  9. gpu4agents/cli/_common.py +131 -0
  10. gpu4agents/cli/account.py +330 -0
  11. gpu4agents/cli/agent.py +74 -0
  12. gpu4agents/cli/inspect.py +207 -0
  13. gpu4agents/cli/lifecycle.py +127 -0
  14. gpu4agents/cli/main.py +163 -0
  15. gpu4agents/cli/providers.py +96 -0
  16. gpu4agents/cli/run.py +38 -0
  17. gpu4agents/cli/setup.py +164 -0
  18. gpu4agents/cli/status.py +224 -0
  19. gpu4agents/cli/timer.py +143 -0
  20. gpu4agents/cli/validate.py +57 -0
  21. gpu4agents/core/__init__.py +0 -0
  22. gpu4agents/core/beacon.py +133 -0
  23. gpu4agents/core/config.py +647 -0
  24. gpu4agents/core/credentials.py +95 -0
  25. gpu4agents/core/durations.py +47 -0
  26. gpu4agents/core/engine.py +1335 -0
  27. gpu4agents/core/errors.py +94 -0
  28. gpu4agents/core/events.py +117 -0
  29. gpu4agents/core/execution.py +169 -0
  30. gpu4agents/core/health.py +62 -0
  31. gpu4agents/core/hosts.py +58 -0
  32. gpu4agents/core/model_inspect.py +368 -0
  33. gpu4agents/core/models.py +313 -0
  34. gpu4agents/core/paths.py +42 -0
  35. gpu4agents/core/policy.py +264 -0
  36. gpu4agents/core/state.py +406 -0
  37. gpu4agents/core/sweep.py +478 -0
  38. gpu4agents/core/tool_parsers.py +63 -0
  39. gpu4agents/providers/__init__.py +4 -0
  40. gpu4agents/providers/base.py +137 -0
  41. gpu4agents/providers/registry.py +63 -0
  42. gpu4agents/providers/vast/__init__.py +3 -0
  43. gpu4agents/providers/vast/client.py +255 -0
  44. gpu4agents/providers/vast/config.py +56 -0
  45. gpu4agents/providers/vast/mapper.py +292 -0
  46. gpu4agents/providers/vast/provider.py +438 -0
  47. gpu4agents/runtime/__init__.py +6 -0
  48. gpu4agents/runtime/base.py +184 -0
  49. gpu4agents/runtime/vllm.py +295 -0
  50. gpu4agents/watchdog/__init__.py +3 -0
  51. gpu4agents/watchdog/agent_body.py +959 -0
  52. gpu4agents/watchdog/builder.py +248 -0
  53. gpu4agents-0.5.0.dist-info/METADATA +157 -0
  54. gpu4agents-0.5.0.dist-info/RECORD +59 -0
  55. gpu4agents-0.5.0.dist-info/WHEEL +4 -0
  56. gpu4agents-0.5.0.dist-info/entry_points.txt +2 -0
  57. gpu4agents-0.5.0.dist-info/licenses/LICENSE +21 -0
  58. jambu_gpu/__init__.py +9 -0
  59. jambu_gpu/__main__.py +4 -0
@@ -0,0 +1,295 @@
1
+ """vLLM OpenAI-compatible server runtime.
2
+
3
+ This is where "does it support agentic flows?" gets answered: vLLM's server
4
+ already implements both ``/v1/chat/completions`` (with tool/function calling)
5
+ and ``/v1/responses`` (the OpenAI Responses API, MCP-tool-routing included).
6
+ gpu does not implement any of that - it only turns on the right vLLM
7
+ flags and forwards the same endpoint/credentials it always has, so any
8
+ OpenAI-SDK-compatible agent framework (LangChain, LangGraph, CrewAI, the
9
+ `openai` SDK's own `responses.create`, ...) can talk to the instance with no
10
+ extra layer in between.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import shlex
17
+ import re
18
+ from typing import Any, Optional
19
+
20
+ from ..core.models import ValidationResult
21
+ from ..core.tool_parsers import guess_tool_call_parser # noqa: F401 (re-exported)
22
+ from .base import ModelRuntime, cuda_from_tag, image_repo, image_tag
23
+
24
+ # vLLM flags gpu already manages explicitly - collided with via
25
+ # runtime.vllm_args, these would fight the config fields meant to control
26
+ # them (compute.gpu, model.id, lifecycle port wiring, ...). Not a hard block
27
+ # (power users can still reach them through raw extra_args if they really
28
+ # need to), just something validate() warns about.
29
+ _MANAGED_VLLM_FLAGS = frozenset(
30
+ {
31
+ "model",
32
+ "host",
33
+ "port",
34
+ "served-model-name",
35
+ "served_model_name",
36
+ "revision",
37
+ "trust-remote-code",
38
+ "trust_remote_code",
39
+ "enable-auto-tool-choice",
40
+ "enable_auto_tool_choice",
41
+ "tool-call-parser",
42
+ "tool_call_parser",
43
+ "tool-server",
44
+ "tool_server",
45
+ }
46
+ )
47
+
48
+
49
+ def flatten_vllm_args(mapping: dict[str, Any]) -> list[str]:
50
+ """Turn a ``{flag: value}`` mapping into vLLM CLI argv.
51
+
52
+ Rules, matched to how vLLM's own argparse-based CLI takes flags:
53
+ - key: underscores become dashes (``max_num_seqs`` -> ``--max-num-seqs``),
54
+ already-dashed keys pass through unchanged. A leading ``--`` in the
55
+ key is tolerated (stripped) so either spelling works.
56
+ - ``True`` -> a bare flag (``--enable-prefix-caching``)
57
+ - ``False`` -> the negated flag (``--no-enable-prefix-caching``), matching
58
+ vLLM's own store_true/store_false flag-pair convention
59
+ - ``None`` -> the flag is skipped entirely (lets a value come from
60
+ elsewhere, e.g. an env var, without an empty CLI arg)
61
+ - dict/list -> JSON-encoded (several vLLM flags, e.g.
62
+ ``--limit-mm-per-prompt``, take a JSON object as their value)
63
+ - anything else -> ``str(value)``
64
+ """
65
+ args: list[str] = []
66
+ for raw_key, value in mapping.items():
67
+ key = raw_key.lstrip("-").replace("_", "-")
68
+ flag = f"--{key}"
69
+ if value is None:
70
+ continue
71
+ if value is True:
72
+ args.append(flag)
73
+ elif value is False:
74
+ args.append(f"--no-{key}")
75
+ elif isinstance(value, (dict, list)):
76
+ args += [flag, json.dumps(value)]
77
+ else:
78
+ args += [flag, str(value)]
79
+ return args
80
+
81
+ LOG_PATH = "/var/log/jambu/vllm.log"
82
+ PID_PATH = "/var/run/jambu/vllm.pid"
83
+
84
+ # Pinned, never `latest`: it moved to CUDA 13 under us and landed on a host
85
+ # whose driver tops out at 12.2. The main build, not a `-cuNNN` variant: the
86
+ # v0.32.0-cu129 variant ships a torchcodec that cannot load (seen live).
87
+ DEFAULT_IMAGE = "vllm/vllm-openai:v0.32.0"
88
+ VLLM_REPO = "vllm/vllm-openai"
89
+
90
+ # CUDA behind the bare `vX.Y.Z` / `latest` tags (the image's NVIDIA_REQUIRE_CUDA),
91
+ # from the first release that requires it. Older bare tags are left unknown.
92
+ _BARE_TAG_CUDA = [((0, 32), 13.0)]
93
+ _LATEST_CUDA = 13.0
94
+ _VERSION_TAG = re.compile(r"^v?(\d+)\.(\d+)(?:\.\d+)?$")
95
+
96
+
97
+ def vllm_image_cuda(image: str) -> Optional[float]:
98
+ """CUDA a vLLM image needs: the ``-cuNNN`` suffix, else the known bare tags."""
99
+ tag = image_tag(image)
100
+ suffixed = cuda_from_tag(tag)
101
+ if suffixed is not None:
102
+ return suffixed
103
+ if not image_repo(image).endswith(VLLM_REPO):
104
+ return None
105
+ if tag == "latest":
106
+ return _LATEST_CUDA
107
+ match = _VERSION_TAG.match(tag)
108
+ if not match:
109
+ return None
110
+ version = (int(match.group(1)), int(match.group(2)))
111
+ found = None
112
+ for since, cuda in _BARE_TAG_CUDA:
113
+ if version >= since:
114
+ found = cuda
115
+ return found
116
+
117
+
118
+ class VllmRuntime(ModelRuntime):
119
+ engine = "vllm"
120
+ default_image = DEFAULT_IMAGE
121
+ api_key_env = "VLLM_API_KEY"
122
+
123
+ def required_cuda(self) -> Optional[float]:
124
+ return vllm_image_cuda(self.image())
125
+
126
+ def required_compute_cap(self) -> Optional[float]:
127
+ # vLLM wheels built for CUDA 13+ drop Volta (sm_70) and need Ampere
128
+ # (8.0) or newer; those crash at engine-core init on a V100 or older.
129
+ # CUDA 12.x wheels still run Volta. Pascal (GTX 10xx, 6.1) is below
130
+ # vLLM's floor on any build.
131
+ cuda = self.required_cuda()
132
+ if cuda is None:
133
+ return None
134
+ return 8.0 if cuda >= 13.0 else 7.0
135
+
136
+ def runtime_pid_path(self) -> str:
137
+ return PID_PATH
138
+
139
+ def runtime_log_path(self) -> str:
140
+ return LOG_PATH
141
+
142
+ def env(self) -> dict[str, str]:
143
+ env: dict[str, str] = {
144
+ "HF_HOME": "/workspace/hf",
145
+ "HUGGING_FACE_HUB_CACHE": "/workspace/hf/hub",
146
+ "VLLM_LOGGING_LEVEL": "INFO",
147
+ }
148
+ if self.config.model.trust_remote_code:
149
+ env["VLLM_TRUST_REMOTE_CODE"] = "1"
150
+ return env
151
+
152
+ def resolve_tool_call_parser(self) -> "str | None":
153
+ cfg = self.config.runtime
154
+ if cfg.tool_calling == "off":
155
+ return None
156
+ if cfg.tool_call_parser:
157
+ return cfg.tool_call_parser
158
+ return guess_tool_call_parser(self.config.model.id)
159
+
160
+ def server_args(self) -> list[str]:
161
+ cfg = self.config
162
+ args: list[str] = [
163
+ "--model",
164
+ cfg.model.id,
165
+ "--host",
166
+ "0.0.0.0",
167
+ "--port",
168
+ str(cfg.runtime.port),
169
+ "--dtype",
170
+ cfg.runtime.dtype,
171
+ "--gpu-memory-utilization",
172
+ str(cfg.runtime.gpu_memory_utilization),
173
+ ]
174
+ if cfg.model.revision:
175
+ args += ["--revision", cfg.model.revision]
176
+ if cfg.model.served_name:
177
+ args += ["--served-model-name", cfg.model.served_name]
178
+ if cfg.runtime.context_length:
179
+ args += ["--max-model-len", str(cfg.runtime.context_length)]
180
+ tp = cfg.runtime.tensor_parallel_size or cfg.compute.gpu.count
181
+ if tp and tp > 1:
182
+ args += ["--tensor-parallel-size", str(tp)]
183
+ if cfg.model.trust_remote_code:
184
+ args.append("--trust-remote-code")
185
+ if cfg.runtime.api_key:
186
+ args += ["--api-key", cfg.runtime.api_key]
187
+
188
+ # Agentic flows: /v1/chat/completions and /v1/responses both need the
189
+ # model to actually be able to emit tool calls, which needs a parser
190
+ # matched to the model family.
191
+ parser = self.resolve_tool_call_parser()
192
+ if parser:
193
+ args += ["--enable-auto-tool-choice", "--tool-call-parser", parser]
194
+ if cfg.runtime.tool_server:
195
+ # Lets /v1/responses execute MCP tools server-side instead of the
196
+ # calling agent framework doing it client-side.
197
+ args += ["--tool-server", cfg.runtime.tool_server]
198
+
199
+ # Escape hatch for vLLM flags this schema doesn't model explicitly -
200
+ # structured form first, then the raw form, each able to override
201
+ # what came before it (vLLM's argparse takes the last occurrence of
202
+ # a repeated flag).
203
+ args += flatten_vllm_args(cfg.runtime.vllm_args)
204
+ args += list(cfg.runtime.extra_args)
205
+ return args
206
+
207
+ def start_command(self) -> str:
208
+ args = " ".join(shlex.quote(a) for a in self.server_args())
209
+ # The interpreter that can import vllm: images that put a Python shim
210
+ # first on PATH (vastai/vllm) keep vllm under /usr/bin/python3.
211
+ find_python = (
212
+ "PY=python3; for p in /usr/bin/python3 python3 python; do "
213
+ "$p -c 'import vllm' 2>/dev/null && PY=$p && break; done"
214
+ )
215
+ return (
216
+ f"mkdir -p $(dirname {LOG_PATH}) && {find_python}; "
217
+ f"nohup $PY -m vllm.entrypoints.openai.api_server {args} "
218
+ f">> {LOG_PATH} 2>&1 & echo $! > {PID_PATH}"
219
+ )
220
+
221
+ def activity_probe(self) -> dict:
222
+ # The watchdog runs in the same container, so loopback reaches vLLM
223
+ # without the public port. /metrics is served even with --api-key.
224
+ return {
225
+ "kind": "prometheus",
226
+ "url": f"http://127.0.0.1:{self.config.runtime.port}/metrics",
227
+ "gauges": ["vllm:num_requests_running", "vllm:num_requests_waiting"],
228
+ "counters": ["vllm:request_success_total", "vllm:prompt_tokens_total"],
229
+ }
230
+
231
+ def validate(self) -> ValidationResult:
232
+ result = ValidationResult()
233
+ cfg = self.config
234
+ if not (0.1 <= cfg.runtime.gpu_memory_utilization <= 1.0):
235
+ result.error(
236
+ "runtime.gpu_memory_utilization must be between 0.1 and 1.0",
237
+ "runtime.gpu_memory_utilization",
238
+ )
239
+ tp = cfg.runtime.tensor_parallel_size
240
+ if tp and tp > cfg.compute.gpu.count:
241
+ result.error(
242
+ f"runtime.tensor_parallel_size ({tp}) exceeds compute.gpu.count "
243
+ f"({cfg.compute.gpu.count})",
244
+ "runtime.tensor_parallel_size",
245
+ )
246
+ if cfg.runtime.context_length and cfg.runtime.context_length > 131072:
247
+ result.warn(
248
+ "runtime.context_length above 128k needs a lot of KV cache VRAM",
249
+ "runtime.context_length",
250
+ )
251
+ if "/" not in cfg.model.id:
252
+ result.warn(
253
+ f"model.id '{cfg.model.id}' does not look like a HuggingFace repo id",
254
+ "model.id",
255
+ )
256
+
257
+ collisions = {
258
+ key.lstrip("-").replace("_", "-") for key in cfg.runtime.vllm_args
259
+ } & _MANAGED_VLLM_FLAGS
260
+ if collisions:
261
+ result.warn(
262
+ f"runtime.vllm_args sets {sorted(collisions)}, which gpu "
263
+ "already manages via other config fields (compute.gpu, model.id, "
264
+ "runtime.tool_calling, ...) - it will still be passed through and "
265
+ "win (last flag wins), but the field meant to control it won't "
266
+ "reflect what's actually running",
267
+ "runtime.vllm_args",
268
+ )
269
+
270
+ if cfg.runtime.tool_calling == "on" and not self.resolve_tool_call_parser():
271
+ result.error(
272
+ "runtime.tool_calling is 'on' but no --tool-call-parser could be "
273
+ f"resolved for '{cfg.model.id}'. Set runtime.tool_call_parser "
274
+ "explicitly (see docs/CONFIG.md for the list vLLM supports).",
275
+ "runtime.tool_call_parser",
276
+ )
277
+ elif cfg.runtime.tool_calling == "auto" and not self.resolve_tool_call_parser():
278
+ result.warn(
279
+ f"could not guess a --tool-call-parser for '{cfg.model.id}'; "
280
+ "tool/function calling will be OFF on both /v1/chat/completions "
281
+ "and /v1/responses. Set runtime.tool_call_parser to turn it on.",
282
+ "runtime.tool_call_parser",
283
+ )
284
+ if cfg.runtime.tool_server and not self.resolve_tool_call_parser():
285
+ result.warn(
286
+ "runtime.tool_server is set but no tool-call parser is active; "
287
+ "the model will not be able to invoke it",
288
+ "runtime.tool_server",
289
+ )
290
+ return result
291
+
292
+ def describe(self) -> str:
293
+ parser = self.resolve_tool_call_parser()
294
+ tools = f", tools={parser}" if parser else ""
295
+ return f"vLLM :{self.config.runtime.port} ({self.config.model.id}{tools})"
@@ -0,0 +1,3 @@
1
+ from .builder import build_agent_source, build_onstart_script, generate_token
2
+
3
+ __all__ = ["build_agent_source", "build_onstart_script", "generate_token"]