agentic-api 0.7.0__py3-none-macosx_11_0_arm64.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.
@@ -0,0 +1,4 @@
1
+ from importlib.metadata import version
2
+
3
+
4
+ __version__ = version("agentic-api")
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from agentic_api import cli
4
+
5
+
6
+ def main() -> int:
7
+ return cli.main()
8
+
9
+
10
+ if __name__ == "__main__":
11
+ raise SystemExit(main())
agentic_api/binary.py ADDED
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import sysconfig
8
+ from pathlib import Path
9
+
10
+
11
+ REMEDIATION_MESSAGE = "Reinstall agentic-api for this platform"
12
+ BINARY_VERSION_TIMEOUT_S = 5.0
13
+
14
+
15
+ class PackagedBinaryNotFoundError(FileNotFoundError):
16
+ """Raised when a packaged executable cannot be located."""
17
+
18
+
19
+ class PackagedBinaryVersionError(RuntimeError):
20
+ """Raised when a packaged executable cannot report its version."""
21
+
22
+
23
+ def find_packaged_binary(name: str) -> Path:
24
+ for candidate in _candidate_paths(name, include_ambient_path=False):
25
+ if _is_executable_file(candidate):
26
+ return candidate
27
+ raise PackagedBinaryNotFoundError(f"{name} not found; {REMEDIATION_MESSAGE}")
28
+
29
+
30
+ def find_active_environment_executable(name: str) -> Path:
31
+ for candidate in _candidate_paths(name, include_ambient_path=False):
32
+ if _is_executable_file(candidate):
33
+ return candidate
34
+ raise FileNotFoundError(f"{name} executable not found in the active environment")
35
+
36
+
37
+ def read_binary_version(path: Path) -> str:
38
+ try:
39
+ completed = subprocess.run(
40
+ [str(path), "--version"],
41
+ check=True,
42
+ capture_output=True,
43
+ text=True,
44
+ timeout=BINARY_VERSION_TIMEOUT_S,
45
+ )
46
+ except OSError as error: # pragma: no cover - exercised via unit tests.
47
+ raise PackagedBinaryVersionError(f"unable to launch {path}: {error.strerror or error}") from error
48
+ except subprocess.TimeoutExpired as error:
49
+ raise PackagedBinaryVersionError(f"{path} timed out while reporting its version") from error
50
+ except subprocess.CalledProcessError as error:
51
+ raise PackagedBinaryVersionError(
52
+ f"{path} exited with status {error.returncode} while reporting its version"
53
+ ) from error
54
+
55
+ output = (completed.stdout or completed.stderr).strip()
56
+ if not output:
57
+ raise PackagedBinaryVersionError(f"{path} did not report a version")
58
+ return output.splitlines()[0].strip()
59
+
60
+
61
+ def _candidate_paths(name: str, *, include_ambient_path: bool = True) -> list[Path]:
62
+ candidates: list[Path] = []
63
+
64
+ if sys.argv and sys.argv[0]:
65
+ candidates.append(Path(sys.argv[0]).resolve().parent / name)
66
+
67
+ scripts_dir = sysconfig.get_path("scripts")
68
+ if scripts_dir:
69
+ candidates.append(Path(scripts_dir) / name)
70
+
71
+ candidates.append(Path(sys.executable).resolve().parent / name)
72
+
73
+ if include_ambient_path:
74
+ which_path = shutil.which(name)
75
+ if which_path:
76
+ candidates.append(Path(which_path))
77
+
78
+ unique_candidates: list[Path] = []
79
+ seen: set[Path] = set()
80
+ for candidate in candidates:
81
+ resolved = candidate.resolve(strict=False)
82
+ if resolved in seen:
83
+ continue
84
+ seen.add(resolved)
85
+ unique_candidates.append(candidate)
86
+ return unique_candidates
87
+
88
+
89
+ def _is_executable_file(path: Path) -> bool:
90
+ return path.is_file() and os.access(path, os.X_OK)
agentic_api/cli.py ADDED
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import math
5
+ import sys
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Sequence
9
+ from urllib.parse import urlparse
10
+
11
+ from agentic_api import __version__
12
+ from agentic_api.diagnostics import doctor
13
+ from agentic_api.version import version_report
14
+
15
+
16
+ DEFAULT_HOST = "0.0.0.0"
17
+ DEFAULT_PORT = 9000
18
+ DEFAULT_STARTUP_TIMEOUT_S = 600.0
19
+ DEFAULT_SHUTDOWN_TIMEOUT_S = 10.0
20
+ DEFAULT_VLLM_PORT = 8000
21
+ DEFAULT_GATEWAY_API_KEY_ENV = "OPENAI_API_KEY"
22
+ DEFAULT_VLLM_API_KEY_ENV = "AGENTIC_VLLM_API_KEY"
23
+ RESERVED_VLLM_FLAGS = {"--host", "--port", "--api-key"}
24
+ INCOMPATIBLE_VLLM_FLAGS = {"--uds"}
25
+ MAX_TIMEOUT_S = 86_400.0
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class ServeOptions:
30
+ mode: str
31
+ model: str | None
32
+ vllm_base_url: str | None
33
+ host: str
34
+ port: int
35
+ startup_timeout_s: float
36
+ shutdown_timeout_s: float
37
+ vllm_port: int
38
+ gateway_api_key_env: str
39
+ vllm_api_key_env: str
40
+ vllm_args: list[str]
41
+
42
+
43
+ class _AgenticArgumentParser(argparse.ArgumentParser):
44
+ def parse_args(self, args: Sequence[str] | None = None, namespace: argparse.Namespace | None = None) -> argparse.Namespace:
45
+ parsed = super().parse_args(args=args, namespace=namespace)
46
+ command = getattr(parsed, "command", None)
47
+
48
+ if command == "serve":
49
+ parsed.options = _build_serve_options(self, parsed)
50
+
51
+ return parsed
52
+
53
+
54
+ def build_parser() -> argparse.ArgumentParser:
55
+ parser = _AgenticArgumentParser(
56
+ prog="agentic-api",
57
+ description="Python launcher for packaged Agentic API binaries",
58
+ allow_abbrev=False,
59
+ )
60
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
61
+ subparsers = parser.add_subparsers(dest="command", required=True)
62
+
63
+ serve_parser = subparsers.add_parser(
64
+ "serve", help="Launch Agentic API in local or remote mode", allow_abbrev=False
65
+ )
66
+ serve_parser.add_argument("--model")
67
+ serve_parser.add_argument("--vllm-base-url")
68
+ serve_parser.add_argument("--host", default=DEFAULT_HOST)
69
+ serve_parser.add_argument("--port", type=_tcp_port, default=DEFAULT_PORT)
70
+ serve_parser.add_argument("--startup-timeout-s", type=_bounded_timeout, default=DEFAULT_STARTUP_TIMEOUT_S)
71
+ serve_parser.add_argument("--shutdown-timeout-s", type=_bounded_timeout, default=DEFAULT_SHUTDOWN_TIMEOUT_S)
72
+ serve_parser.add_argument("--vllm-port", type=_tcp_port, default=DEFAULT_VLLM_PORT)
73
+ serve_parser.add_argument("--gateway-api-key-env", default=DEFAULT_GATEWAY_API_KEY_ENV)
74
+ serve_parser.add_argument("--vllm-api-key-env", default=DEFAULT_VLLM_API_KEY_ENV)
75
+ serve_parser.add_argument("vllm_args", nargs=argparse.REMAINDER)
76
+
77
+ doctor_parser = subparsers.add_parser(
78
+ "doctor", help="Report packaged binary and compatibility diagnostics", allow_abbrev=False
79
+ )
80
+ doctor_parser.add_argument("--mode", choices=("local", "remote"))
81
+ doctor_parser.add_argument(
82
+ "--json", action="store_true", dest="json_output", help="Emit a machine-readable JSON report"
83
+ )
84
+
85
+ subparsers.add_parser("version", help="Print package and packaged binary versions")
86
+ return parser
87
+
88
+
89
+ def main(argv: Sequence[str] | None = None) -> int:
90
+ parser = build_parser()
91
+ try:
92
+ namespace = parser.parse_args(argv)
93
+ except SystemExit as error:
94
+ code = error.code
95
+ return code if isinstance(code, int) else 1
96
+
97
+ if namespace.command == "version":
98
+ try:
99
+ print(version_report())
100
+ return 0
101
+ except (FileNotFoundError, RuntimeError) as error:
102
+ print(str(error), file=sys.stderr)
103
+ return 1
104
+
105
+ if namespace.command == "doctor":
106
+ return doctor(namespace.mode, json_output=namespace.json_output)
107
+
108
+ if namespace.command == "serve":
109
+ try:
110
+ from agentic_api.launcher import run_serve
111
+ except ModuleNotFoundError as error:
112
+ if error.name != "agentic_api.launcher":
113
+ raise
114
+ print(
115
+ "agentic-api serve is unavailable because the Python launcher is missing; "
116
+ "reinstall agentic-api for this platform.",
117
+ file=sys.stderr,
118
+ )
119
+ return 1
120
+
121
+ return run_serve(namespace.options)
122
+
123
+ parser.error(f"unknown command: {namespace.command}")
124
+ return 1
125
+
126
+
127
+ def _build_serve_options(parser: argparse.ArgumentParser, namespace: argparse.Namespace) -> ServeOptions:
128
+ if namespace.model is not None and not namespace.model.strip():
129
+ parser.error("--model must not be empty")
130
+
131
+ source_count = int(bool(namespace.model)) + int(bool(namespace.vllm_base_url))
132
+ if source_count != 1:
133
+ parser.error("exactly one of --model or --vllm-base-url is required")
134
+
135
+ vllm_args = _normalize_vllm_args(namespace.vllm_args)
136
+ reserved_flag = _reserved_vllm_flag(vllm_args)
137
+ if reserved_flag is not None:
138
+ parser.error(f"{reserved_flag} is reserved for the launcher and cannot be forwarded after --")
139
+
140
+ vllm_base_url = None
141
+ mode = "local"
142
+ if namespace.vllm_base_url is not None:
143
+ vllm_base_url = _normalize_base_url(parser, namespace.vllm_base_url)
144
+ mode = "remote"
145
+
146
+ if mode == "remote" and vllm_args:
147
+ parser.error("vLLM passthrough arguments are only supported with --model (local mode)")
148
+
149
+ return ServeOptions(
150
+ mode=mode,
151
+ model=namespace.model,
152
+ vllm_base_url=vllm_base_url,
153
+ host=namespace.host,
154
+ port=namespace.port,
155
+ startup_timeout_s=namespace.startup_timeout_s,
156
+ shutdown_timeout_s=namespace.shutdown_timeout_s,
157
+ vllm_port=namespace.vllm_port,
158
+ gateway_api_key_env=namespace.gateway_api_key_env,
159
+ vllm_api_key_env=namespace.vllm_api_key_env,
160
+ vllm_args=vllm_args,
161
+ )
162
+
163
+
164
+ def _normalize_vllm_args(values: Sequence[str]) -> list[str]:
165
+ if values and values[0] == "--":
166
+ return list(values[1:])
167
+ return list(values)
168
+
169
+
170
+ def _reserved_vllm_flag(values: Sequence[str]) -> str | None:
171
+ for value in values:
172
+ if not value.startswith("--"):
173
+ continue
174
+ option_name = value.partition("=")[0].replace("_", "-")
175
+ if len(option_name) > 2 and any(
176
+ reserved.startswith(option_name) for reserved in RESERVED_VLLM_FLAGS | INCOMPATIBLE_VLLM_FLAGS
177
+ ):
178
+ return option_name
179
+ return None
180
+
181
+
182
+ def _tcp_port(value: str) -> int:
183
+ try:
184
+ port = int(value)
185
+ except ValueError as error:
186
+ raise argparse.ArgumentTypeError("must be between 1 and 65535") from error
187
+ if not 1 <= port <= 65_535:
188
+ raise argparse.ArgumentTypeError("must be between 1 and 65535")
189
+ return port
190
+
191
+
192
+ def _bounded_timeout(value: str) -> float:
193
+ try:
194
+ timeout = float(value)
195
+ except ValueError as error:
196
+ raise argparse.ArgumentTypeError(
197
+ f"must be finite and greater than 0, up to {MAX_TIMEOUT_S:g} seconds"
198
+ ) from error
199
+ if not math.isfinite(timeout) or not 0 < timeout <= MAX_TIMEOUT_S:
200
+ raise argparse.ArgumentTypeError(f"must be finite and greater than 0, up to {MAX_TIMEOUT_S:g} seconds")
201
+ return timeout
202
+
203
+
204
+ def _normalize_base_url(parser: argparse.ArgumentParser, value: str) -> str:
205
+ if any(character.isspace() for character in value):
206
+ parser.error("--vllm-base-url must not contain whitespace")
207
+
208
+ try:
209
+ parsed = urlparse(value)
210
+ hostname = parsed.hostname
211
+ port = parsed.port
212
+ except ValueError as error:
213
+ parser.error(f"--vllm-base-url is malformed: {error}")
214
+
215
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc or not hostname:
216
+ parser.error("--vllm-base-url must be an http:// or https:// base URL")
217
+ if parsed.username is not None or parsed.password is not None:
218
+ parser.error("--vllm-base-url must not contain credentials; use an environment variable for API keys")
219
+ if port is not None and not 1 <= port <= 65_535:
220
+ parser.error("--vllm-base-url port must be between 1 and 65535")
221
+ if parsed.query or parsed.fragment:
222
+ parser.error("--vllm-base-url must not include a query string or fragment")
223
+ return value.rstrip("/")
@@ -0,0 +1 @@
1
+ SUPPORTED_VLLM_VERSION = "0.11.0"
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import platform
6
+ from dataclasses import asdict, dataclass
7
+ from importlib.metadata import PackageNotFoundError, version as metadata_version
8
+ from pathlib import Path
9
+
10
+ from agentic_api import __version__
11
+ from agentic_api.binary import (
12
+ PackagedBinaryNotFoundError,
13
+ PackagedBinaryVersionError,
14
+ find_active_environment_executable,
15
+ find_packaged_binary,
16
+ read_binary_version,
17
+ )
18
+ from agentic_api.compatibility import SUPPORTED_VLLM_VERSION
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class DoctorReport:
23
+ python_version: str
24
+ platform_summary: str
25
+ package_version: str
26
+ rust_binary_path: str
27
+ rust_binary_executable: bool
28
+ rust_binary_version: str
29
+ supported_vllm_version: str
30
+ installed_vllm_version: str
31
+ vllm_executable_path: str
32
+ local_ok: bool
33
+ remote_ok: bool
34
+ local_message: str
35
+ remote_message: str
36
+
37
+
38
+ def doctor(mode: str | None, *, json_output: bool = False) -> int:
39
+ report = collect_doctor_report()
40
+ print(render_doctor_report(report, mode, json_output=json_output))
41
+
42
+ if mode == "local":
43
+ return 0 if report.local_ok else 1
44
+ if mode == "remote":
45
+ return 0 if report.remote_ok else 1
46
+ return 0 if report.remote_ok else 1
47
+
48
+
49
+ def collect_doctor_report() -> DoctorReport:
50
+ rust_binary_path, rust_binary_executable, rust_binary_version = _rust_binary_details()
51
+ package_version = _package_version("agentic-api")
52
+ installed_vllm_version = _package_version("vllm")
53
+ try:
54
+ vllm_executable_path = str(find_active_environment_executable("vllm"))
55
+ except FileNotFoundError:
56
+ vllm_executable_path = "not found"
57
+ local_runtime_ok, local_message = _local_health(
58
+ installed_vllm_version,
59
+ vllm_executable_path,
60
+ platform_name=platform.system(),
61
+ )
62
+ remote_ok = rust_binary_executable and not rust_binary_version.startswith("error:")
63
+ remote_message = _remote_health_message(rust_binary_executable, rust_binary_version)
64
+ if local_runtime_ok and not remote_ok:
65
+ local_message = remote_message
66
+
67
+ return DoctorReport(
68
+ python_version=platform.python_version(),
69
+ platform_summary=f"{platform.system()} {platform.machine()}",
70
+ package_version=package_version,
71
+ rust_binary_path=rust_binary_path,
72
+ rust_binary_executable=rust_binary_executable,
73
+ rust_binary_version=rust_binary_version,
74
+ supported_vllm_version=SUPPORTED_VLLM_VERSION,
75
+ installed_vllm_version=installed_vllm_version,
76
+ vllm_executable_path=vllm_executable_path,
77
+ local_ok=local_runtime_ok and remote_ok,
78
+ remote_ok=remote_ok,
79
+ local_message=local_message,
80
+ remote_message=remote_message,
81
+ )
82
+
83
+
84
+ def render_doctor_report(report: DoctorReport, mode: str | None, *, json_output: bool = False) -> str:
85
+ if json_output:
86
+ payload = asdict(report)
87
+ payload["selected_mode"] = mode or "all"
88
+ return json.dumps(payload, sort_keys=True)
89
+
90
+ local_health = "ok" if report.local_ok else "unavailable"
91
+ remote_health = "ok" if report.remote_ok else "unavailable"
92
+
93
+ lines = [
94
+ f"Selected mode: {mode or 'all'}",
95
+ f"Python version: {report.python_version}",
96
+ f"Platform: {report.platform_summary}",
97
+ f"agentic-api version: {report.package_version}",
98
+ f"Rust binary path: {report.rust_binary_path}",
99
+ f"Rust binary executable: {'yes' if report.rust_binary_executable else 'no'}",
100
+ f"Rust binary version: {report.rust_binary_version}",
101
+ f"Supported vLLM version: {report.supported_vllm_version}",
102
+ f"Installed vLLM version: {report.installed_vllm_version}",
103
+ f"vLLM executable path: {report.vllm_executable_path}",
104
+ f"Local mode health: {local_health}",
105
+ f"Local mode details: {report.local_message}",
106
+ f"Remote mode health: {remote_health}",
107
+ f"Remote mode details: {report.remote_message}",
108
+ ]
109
+ return "\n".join(lines)
110
+
111
+
112
+ def _package_version(name: str) -> str:
113
+ try:
114
+ return metadata_version(name)
115
+ except PackageNotFoundError:
116
+ return "not installed"
117
+
118
+
119
+ def _rust_binary_details() -> tuple[str, bool, str]:
120
+ try:
121
+ path = find_packaged_binary("agentic-server")
122
+ except PackagedBinaryNotFoundError as error:
123
+ return ("not found", False, f"error: {error}")
124
+
125
+ executable = path.is_file() and os.access(path, os.X_OK)
126
+ try:
127
+ version = read_binary_version(path)
128
+ except PackagedBinaryVersionError as error:
129
+ version = f"error: {error}"
130
+ return (str(path), executable, version)
131
+
132
+
133
+ def _local_health(
134
+ installed_vllm_version: str,
135
+ vllm_executable_path: str,
136
+ *,
137
+ platform_name: str,
138
+ ) -> tuple[bool, str]:
139
+ if platform_name != "Linux":
140
+ return (
141
+ False,
142
+ "Local mode is currently supported only on Linux because the [local] extra installs vLLM only on Linux. "
143
+ "Use remote mode on this platform, or run local mode on Linux.",
144
+ )
145
+
146
+ install_hint = (
147
+ "Install the agentic-api wheel artifact with its local extra: "
148
+ '`uv pip install "agentic-api[local] @ file:///path/to/agentic_api-PLATFORM.whl"`.'
149
+ )
150
+ if installed_vllm_version == "not installed":
151
+ return (False, install_hint)
152
+ if installed_vllm_version != SUPPORTED_VLLM_VERSION:
153
+ return (
154
+ False,
155
+ f"Installed vLLM does not match the tested version {SUPPORTED_VLLM_VERSION}. {install_hint}",
156
+ )
157
+ if vllm_executable_path == "not found":
158
+ return (
159
+ False,
160
+ f"The installed vLLM package does not provide a `vllm` executable in the active Python environment. "
161
+ f"{install_hint}",
162
+ )
163
+ return (True, "The tested local vLLM package and executable are available.")
164
+
165
+
166
+ def _remote_health_message(rust_binary_executable: bool, rust_binary_version: str) -> str:
167
+ if not rust_binary_executable:
168
+ return "The packaged Rust gateway executable is missing or not executable."
169
+ if rust_binary_version.startswith("error:"):
170
+ return "The packaged Rust gateway executable could not report its version."
171
+ return "The packaged Rust gateway executable is available; remote mode does not require vLLM."
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import signal
5
+ import sys
6
+ from importlib.metadata import PackageNotFoundError, version
7
+ from pathlib import Path
8
+
9
+ from agentic_api.binary import (
10
+ PackagedBinaryNotFoundError,
11
+ find_active_environment_executable,
12
+ find_packaged_binary,
13
+ )
14
+ from agentic_api.cli import ServeOptions
15
+ from agentic_api.compatibility import SUPPORTED_VLLM_VERSION
16
+ from agentic_api.process import ChildResult, ProcessSupervisor, ShutdownRequested, wait_for_vllm_ready
17
+
18
+
19
+ READY_INTERVAL_S = 2.0
20
+
21
+
22
+ def run_serve(options: ServeOptions) -> int:
23
+ supervisor = ProcessSupervisor()
24
+ signal_exit_code: int | None = None
25
+ previous_handlers: list[tuple[int, object]] = []
26
+
27
+ def begin_shutdown(signum: int, _frame: object) -> None:
28
+ nonlocal signal_exit_code
29
+ if signal_exit_code is None:
30
+ signal_exit_code = _signal_exit_code(signum)
31
+ supervisor.request_shutdown(signum)
32
+
33
+ for signum in (signal.SIGINT, signal.SIGTERM):
34
+ previous_handlers.append((signum, signal.signal(signum, begin_shutdown)))
35
+
36
+ try:
37
+ if options.mode == "local":
38
+ result = _run_local_mode(supervisor, options)
39
+ elif options.mode == "remote":
40
+ result = _run_remote_mode(supervisor, options)
41
+ else:
42
+ raise ValueError(f"unsupported serve mode: {options.mode}")
43
+
44
+ if signal_exit_code is not None:
45
+ return signal_exit_code
46
+ return _normalize_exit_code(result)
47
+ except ShutdownRequested:
48
+ if signal_exit_code is not None:
49
+ return signal_exit_code
50
+ raise
51
+ except KeyboardInterrupt:
52
+ signal_exit_code = _signal_exit_code(signal.SIGINT)
53
+ supervisor.request_shutdown(signal.SIGINT)
54
+ return signal_exit_code
55
+ except (OSError, PackagedBinaryNotFoundError, RuntimeError, ValueError) as error:
56
+ if signal_exit_code is not None:
57
+ return signal_exit_code
58
+ print(str(error), file=sys.stderr)
59
+ return 1
60
+ finally:
61
+ supervisor.terminate_all(options.shutdown_timeout_s)
62
+ for signum, previous in previous_handlers:
63
+ signal.signal(signum, previous)
64
+
65
+
66
+ def _run_local_mode(supervisor: ProcessSupervisor, options: ServeOptions) -> ChildResult:
67
+ if options.model is None:
68
+ raise ValueError("--model is required in local mode")
69
+ if sys.platform != "linux":
70
+ raise RuntimeError(
71
+ "agentic-api local mode is currently supported only on Linux; "
72
+ "use remote mode on this platform"
73
+ )
74
+
75
+ rust_binary = find_packaged_binary("agentic-server")
76
+ vllm_path = find_active_environment_executable("vllm")
77
+ installed_version = _installed_vllm_version()
78
+ if installed_version != SUPPORTED_VLLM_VERSION:
79
+ raise RuntimeError(
80
+ f"agentic-api local mode requires vllm=={SUPPORTED_VLLM_VERSION}; found {installed_version}"
81
+ )
82
+
83
+ vllm_api_key = os.environ.get(options.vllm_api_key_env)
84
+ vllm_url = f"http://127.0.0.1:{options.vllm_port}"
85
+
86
+ vllm_process = supervisor.start(
87
+ [
88
+ str(vllm_path),
89
+ "serve",
90
+ options.model,
91
+ *options.vllm_args,
92
+ "--host",
93
+ "127.0.0.1",
94
+ "--port",
95
+ str(options.vllm_port),
96
+ ],
97
+ _vllm_environment(vllm_api_key),
98
+ )
99
+ wait_for_vllm_ready(
100
+ base_url=vllm_url,
101
+ api_key=vllm_api_key,
102
+ process=vllm_process,
103
+ timeout=options.startup_timeout_s,
104
+ interval=READY_INTERVAL_S,
105
+ shutdown_requested=supervisor.shutdown_requested,
106
+ )
107
+
108
+ supervisor.start(
109
+ _rust_command(options, vllm_url, binary=rust_binary),
110
+ _rust_environment(options.gateway_api_key_env, vllm_api_key),
111
+ )
112
+ return supervisor.wait_for_failure()
113
+
114
+
115
+ def _run_remote_mode(supervisor: ProcessSupervisor, options: ServeOptions) -> ChildResult:
116
+ if options.vllm_base_url is None:
117
+ raise ValueError("--vllm-base-url is required in remote mode")
118
+
119
+ supervisor.start(
120
+ _rust_command(options, options.vllm_base_url),
121
+ _rust_environment(options.gateway_api_key_env, None),
122
+ )
123
+ return supervisor.wait_for_failure()
124
+
125
+
126
+ def _rust_command(options: ServeOptions, upstream_base_url: str, *, binary: Path | None = None) -> list[str]:
127
+ binary = binary or find_packaged_binary("agentic-server")
128
+ return [
129
+ str(binary),
130
+ "--llm-api-base",
131
+ upstream_base_url,
132
+ "--llm-ready-timeout-s",
133
+ str(options.startup_timeout_s),
134
+ "--gateway-host",
135
+ options.host,
136
+ "--gateway-port",
137
+ str(options.port),
138
+ ]
139
+
140
+
141
+ def _rust_environment(gateway_api_key_env: str, api_key_override: str | None) -> dict[str, str]:
142
+ env = os.environ.copy()
143
+ gateway_api_key = api_key_override if api_key_override is not None else os.environ.get(gateway_api_key_env)
144
+ if gateway_api_key is not None:
145
+ env["OPENAI_API_KEY"] = gateway_api_key
146
+ elif gateway_api_key_env != "OPENAI_API_KEY":
147
+ env.pop("OPENAI_API_KEY", None)
148
+ return env
149
+
150
+
151
+ def _vllm_environment(api_key: str | None) -> dict[str, str]:
152
+ env = os.environ.copy()
153
+ if api_key is not None:
154
+ env["VLLM_API_KEY"] = api_key
155
+ else:
156
+ env.pop("VLLM_API_KEY", None)
157
+ return env
158
+
159
+
160
+ def _installed_vllm_version() -> str:
161
+ try:
162
+ return version("vllm")
163
+ except PackageNotFoundError as error:
164
+ raise RuntimeError(
165
+ f"agentic-api local mode requires vllm=={SUPPORTED_VLLM_VERSION}; install the [local] extra first"
166
+ ) from error
167
+
168
+
169
+ def _normalize_exit_code(result: ChildResult) -> int:
170
+ if result.returncode == 0:
171
+ print(f"{result.name} exited unexpectedly with status 0", file=sys.stderr)
172
+ return 1
173
+ if result.returncode >= 0:
174
+ return result.returncode
175
+ return _signal_exit_code(-result.returncode)
176
+
177
+
178
+ def _signal_exit_code(signum: int) -> int:
179
+ return 128 + int(signum)