infer-stack 0.6.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 (44) hide show
  1. infer_stack/__init__.py +2 -0
  2. infer_stack/backends/__init__.py +7 -0
  3. infer_stack/backends/compose_renderer.py +243 -0
  4. infer_stack/backends/kubeai_renderer.py +202 -0
  5. infer_stack/benchmark.py +38 -0
  6. infer_stack/catalog.py +438 -0
  7. infer_stack/cli/__init__.py +169 -0
  8. infer_stack/cli/__main__.py +4 -0
  9. infer_stack/cli/commands_profile.py +467 -0
  10. infer_stack/cli/commands_runtime.py +719 -0
  11. infer_stack/cli/commands_smoke.py +691 -0
  12. infer_stack/cli/compose.py +755 -0
  13. infer_stack/cli/context.py +471 -0
  14. infer_stack/cli/options.py +134 -0
  15. infer_stack/cli/probes.py +178 -0
  16. infer_stack/config.py +450 -0
  17. infer_stack/contracts.py +223 -0
  18. infer_stack/diff_prompt.py +117 -0
  19. infer_stack/docker_utils.py +230 -0
  20. infer_stack/env_utils.py +97 -0
  21. infer_stack/experimental/model_catalog_discover.py +1155 -0
  22. infer_stack/experimental/model_memory_estimator.py +1264 -0
  23. infer_stack/experimental/stress_test_long_context.py +397 -0
  24. infer_stack/hardware.py +70 -0
  25. infer_stack/kubeai_ops.py +76 -0
  26. infer_stack/paths.py +87 -0
  27. infer_stack/profile_runtime.py +46 -0
  28. infer_stack/renderer.py +19 -0
  29. infer_stack/resolver.py +1092 -0
  30. infer_stack/templates/default-models.yaml +674 -0
  31. infer_stack/templates/default-ollama-models.yaml +31 -0
  32. infer_stack/templates/default-profiles.yaml +1731 -0
  33. infer_stack/templates/default-vllm-models.yaml +714 -0
  34. infer_stack/templates/docker-compose.yml.j2 +430 -0
  35. infer_stack/templates/litellm_config.yaml.j2 +44 -0
  36. infer_stack/templates/nginx.conf.j2 +84 -0
  37. infer_stack/tuning.py +3 -0
  38. infer_stack/validator.py +314 -0
  39. infer_stack/verification.py +46 -0
  40. infer_stack-0.6.0.dist-info/METADATA +1034 -0
  41. infer_stack-0.6.0.dist-info/RECORD +44 -0
  42. infer_stack-0.6.0.dist-info/WHEEL +5 -0
  43. infer_stack-0.6.0.dist-info/entry_points.txt +2 -0
  44. infer_stack-0.6.0.dist-info/top_level.txt +1 -0
infer_stack/catalog.py ADDED
@@ -0,0 +1,438 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from typing import Any
5
+
6
+
7
+ PROFILE_NAME_ALIASES = {
8
+ 'helm-qwen2-72b-instruct': 'qwen2-72b-instruct-tp2-balanced',
9
+ 'helm-qwen2.5-7b-instruct': 'qwen2-5-7b-instruct-turbo-default',
10
+ 'helm-qwen2.5-72b-instruct': 'qwen2-5-72b-instruct-tp2-balanced',
11
+ 'helm-gpt-oss-20b': 'gpt-oss-20b-completions',
12
+ 'helm-vicuna-7b-v1.3': 'vicuna-7b-v1-3-no-chat-template',
13
+ }
14
+
15
+
16
+ def sanitize_name(value: str) -> str:
17
+ value = str(value).strip().lower()
18
+ out: list[str] = []
19
+ prev_dash = False
20
+ for char in value:
21
+ if char.isalnum():
22
+ out.append(char)
23
+ prev_dash = False
24
+ continue
25
+ if not prev_dash:
26
+ out.append('-')
27
+ prev_dash = True
28
+ sanitized = ''.join(out).strip('-')
29
+ return sanitized or 'profile'
30
+
31
+
32
+ def canonical_profile_name(name: str | None) -> str | None:
33
+ if name is None:
34
+ return None
35
+ return PROFILE_NAME_ALIASES.get(name, name)
36
+
37
+
38
+ def _list(value: Any, default: list[Any] | None = None) -> list[Any]:
39
+ if value is None:
40
+ return list(default or [])
41
+ if isinstance(value, list):
42
+ return deepcopy(value)
43
+ if isinstance(value, tuple):
44
+ return list(value)
45
+ return [value]
46
+
47
+
48
+ def normalize_vllm_models(catalog: dict[str, Any]) -> dict[str, Any]:
49
+ normalized: dict[str, Any] = {}
50
+ for key, raw in (catalog or {}).items():
51
+ entry = deepcopy(raw) or {}
52
+ hf_model_id = entry.get('hf_model_id', '')
53
+ url = entry.get('url') or (f'hf://{hf_model_id}' if hf_model_id else '')
54
+ defaults = deepcopy(entry.get('defaults', {}))
55
+ supported_protocols = entry.get('supported_protocols')
56
+ if supported_protocols is None:
57
+ supported_protocols = ['chat', 'completions']
58
+ normalized[key] = {
59
+ 'key': key,
60
+ 'provider': 'vllm',
61
+ 'canonical_key': sanitize_name(entry.get('canonical_key', key)),
62
+ 'hf_model_id': hf_model_id,
63
+ 'url': url,
64
+ 'family': entry.get('family', ''),
65
+ 'modalities': _list(entry.get('modalities'), ['text']),
66
+ 'supported_protocols': [str(p) for p in supported_protocols],
67
+ 'reasoning': deepcopy(entry.get('reasoning', {})),
68
+ 'tokenizer_name': entry.get('tokenizer_name')
69
+ or entry.get('tokenizer')
70
+ or entry.get('served_model_name')
71
+ or key,
72
+ 'logical_model_name': entry.get('logical_model_name')
73
+ or entry.get('served_model_name')
74
+ or key,
75
+ 'served_model_name': entry.get('served_model_name')
76
+ or entry.get('logical_model_name')
77
+ or key,
78
+ 'memory_class_gib': entry.get('memory_class_gib'),
79
+ 'min_vram_gib_per_replica': entry.get(
80
+ 'min_vram_gib_per_replica', 0
81
+ ),
82
+ 'preferred_gpu_count': entry.get('preferred_gpu_count', 1),
83
+ 'context_window': entry.get('context_window'),
84
+ 'defaults': defaults,
85
+ 'engine': 'VLLM',
86
+ 'resource_profile': entry.get('resource_profile', ''),
87
+ 'priority_class_name': entry.get('priority_class_name'),
88
+ 'tool_calling': deepcopy(entry.get('tool_calling', {})),
89
+ 'thinking_history_policy': entry.get(
90
+ 'thinking_history_policy', 'keep_final_only'
91
+ ),
92
+ 'features': deepcopy(entry.get('features', ['TextGeneration'])),
93
+ 'safe_defaults': deepcopy(entry.get('safe_defaults', defaults)),
94
+ 'notes': _list(entry.get('notes')),
95
+ 'caveats': _list(entry.get('caveats')),
96
+ }
97
+ return normalized
98
+
99
+
100
+ def normalize_ollama_models(catalog: dict[str, Any]) -> dict[str, Any]:
101
+ normalized: dict[str, Any] = {}
102
+ for key, raw in (catalog or {}).items():
103
+ entry = deepcopy(raw) or {}
104
+ tag = (
105
+ entry.get('tag')
106
+ or entry.get('ollama_model')
107
+ or entry.get('model')
108
+ or key
109
+ )
110
+ normalized[key] = {
111
+ 'key': key,
112
+ 'provider': 'ollama',
113
+ 'tag': str(tag),
114
+ 'served_model_name': entry.get('served_model_name')
115
+ or sanitize_name(str(tag)),
116
+ 'logical_model_name': entry.get('logical_model_name')
117
+ or entry.get('served_model_name')
118
+ or sanitize_name(str(tag)),
119
+ 'modalities': _list(entry.get('modalities'), ['text']),
120
+ 'supported_protocols': [
121
+ str(p)
122
+ for p in _list(entry.get('supported_protocols'), ['chat'])
123
+ ],
124
+ 'context_window': entry.get('context_window'),
125
+ 'defaults': deepcopy(entry.get('defaults', {})),
126
+ 'notes': _list(entry.get('notes')),
127
+ 'caveats': _list(entry.get('caveats')),
128
+ }
129
+ return normalized
130
+
131
+
132
+ # Compatibility view used by older helper commands/tests. Prefer provider-specific
133
+ # catalogs in new code.
134
+ def normalize_model_catalog(catalog: dict[str, Any]) -> dict[str, Any]:
135
+ return normalize_vllm_models(catalog)
136
+
137
+
138
+ def _infer_protocol_mode(
139
+ profile_name: str, logical_model_name: str, raw: dict[str, Any]
140
+ ) -> str:
141
+ explicit = raw.get('protocol_mode') or raw.get('protocol')
142
+ if explicit:
143
+ return str(explicit)
144
+ tags = {str(tag) for tag in raw.get('tags', [])}
145
+ if 'completions' in tags:
146
+ return 'completions'
147
+ if 'chat' in tags:
148
+ return 'chat'
149
+ name = f'{profile_name} {logical_model_name}'.lower()
150
+ if 'completion' in name:
151
+ return 'completions'
152
+ return 'chat'
153
+
154
+
155
+ def _normalize_bool_map(
156
+ raw: Any, default_enabled: bool = False
157
+ ) -> dict[str, Any]:
158
+ if isinstance(raw, dict):
159
+ return deepcopy(raw)
160
+ if raw in (None, 'auto'):
161
+ return {'enabled': 'auto'}
162
+ return {'enabled': bool(raw) if raw is not None else default_enabled}
163
+
164
+
165
+ def _normalize_components(
166
+ profile: dict[str, Any],
167
+ ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
168
+ providers = deepcopy(profile.get('providers', {}))
169
+ gateways = deepcopy(profile.get('gateways', {}))
170
+ frontends = deepcopy(profile.get('frontends', {}))
171
+
172
+ # Convenience/older shape: components: {ollama: true, litellm: false, ...}
173
+ components = deepcopy(profile.get('components', {}) or {})
174
+ for name in ['ollama', 'vllm']:
175
+ if name in components and name not in providers:
176
+ providers[name] = _normalize_bool_map(components[name])
177
+ if 'litellm' in components and 'litellm' not in gateways:
178
+ gateways['litellm'] = _normalize_bool_map(components['litellm'])
179
+ if 'open_webui' in components and 'open_webui' not in frontends:
180
+ frontends['open_webui'] = _normalize_bool_map(components['open_webui'])
181
+
182
+ providers.setdefault('vllm', {})
183
+ providers.setdefault('ollama', {'enabled': 'auto'})
184
+ gateways.setdefault('litellm', {'enabled': 'auto'})
185
+ frontends.setdefault('open_webui', {'enabled': 'auto', 'provider': 'auto'})
186
+ return providers, gateways, frontends
187
+
188
+
189
+ def _route_aliases(route_name: str, raw: dict[str, Any]) -> list[str]:
190
+ aliases = raw.get('aliases')
191
+ if aliases is None:
192
+ aliases = raw.get('served_aliases')
193
+ if aliases is None:
194
+ aliases = [route_name]
195
+ elif isinstance(aliases, str):
196
+ aliases = [aliases]
197
+ else:
198
+ aliases = list(aliases)
199
+ if route_name not in aliases:
200
+ aliases.insert(0, route_name)
201
+ ordered: list[str] = []
202
+ for alias in aliases:
203
+ if alias and alias not in ordered:
204
+ ordered.append(str(alias))
205
+ return ordered
206
+
207
+
208
+ def _legacy_profile_to_stack(
209
+ name: str, raw: dict[str, Any], vllm_models: dict[str, Any]
210
+ ) -> dict[str, Any]:
211
+ services = deepcopy(raw.get('services', []))
212
+ if not services and (raw.get('model') or raw.get('base_model')):
213
+ services = [deepcopy(raw)]
214
+ aliases = deepcopy(raw.get('router', {}).get('aliases', {}))
215
+ runtimes: dict[str, Any] = {}
216
+ routes: dict[str, Any] = {}
217
+ for index, service in enumerate(services):
218
+ runtime_name = sanitize_name(
219
+ service.get('service_name')
220
+ or service.get('runtime')
221
+ or service.get('name')
222
+ or (name if len(services) == 1 else f'runtime-{index + 1}')
223
+ )
224
+ model_key = service.get('base_model') or service.get('model')
225
+ if model_key not in vllm_models:
226
+ raise KeyError(f'Unknown vLLM model: {model_key}')
227
+ model = vllm_models[model_key]
228
+ public_name = sanitize_name(
229
+ service.get('public_name')
230
+ or (name if len(services) == 1 else f'{name}-{runtime_name}')
231
+ )
232
+ logical_model_name = (
233
+ service.get('logical_model_name')
234
+ or model.get('logical_model_name')
235
+ or model.get('served_model_name')
236
+ or model_key
237
+ )
238
+ served_model_name = (
239
+ service.get('served_model_name')
240
+ or model.get('served_model_name')
241
+ or logical_model_name
242
+ )
243
+ protocol_mode = _infer_protocol_mode(
244
+ public_name, logical_model_name, service
245
+ )
246
+ runtimes[runtime_name] = {
247
+ 'model': model_key,
248
+ 'public_name': public_name,
249
+ 'logical_model_name': logical_model_name,
250
+ 'served_model_name': served_model_name,
251
+ 'protocol_mode': protocol_mode,
252
+ 'placement': deepcopy(service.get('placement', {})),
253
+ 'topology': deepcopy(service.get('topology', {})),
254
+ 'runtime': deepcopy(service.get('runtime', {})),
255
+ 'extra_args': deepcopy(service.get('extra_args', [])),
256
+ 'reasoning': deepcopy(service.get('reasoning', {})),
257
+ 'tool_calling': deepcopy(service.get('tool_calling', {})),
258
+ 'chat_compat': deepcopy(service.get('chat_compat', {})),
259
+ 'resource_profile': service.get(
260
+ 'resource_profile', model.get('resource_profile', '')
261
+ ),
262
+ 'min_replicas': int(
263
+ service.get(
264
+ 'min_replicas',
265
+ model.get('defaults', {}).get('min_replicas', 0),
266
+ )
267
+ ),
268
+ 'max_replicas': int(
269
+ service.get(
270
+ 'max_replicas',
271
+ model.get('defaults', {}).get('max_replicas', 1),
272
+ )
273
+ ),
274
+ 'priority_class_name': service.get(
275
+ 'priority_class_name', model.get('priority_class_name')
276
+ ),
277
+ 'tags': list(service.get('tags', raw.get('tags', [])) or []),
278
+ 'audit_notes': list(
279
+ service.get('audit_notes', raw.get('audit_notes', [])) or []
280
+ ),
281
+ 'notes': list(service.get('notes', raw.get('notes', [])) or []),
282
+ 'benchmark_transport': deepcopy(
283
+ service.get(
284
+ 'benchmark_transport',
285
+ service.get(
286
+ 'transport',
287
+ raw.get(
288
+ 'benchmark_transport', raw.get('transport', {})
289
+ ),
290
+ ),
291
+ )
292
+ ),
293
+ 'publish_port': bool(service.get('publish_port', False)),
294
+ }
295
+ # Convert old router alias map to route map.
296
+ if aliases:
297
+ for alias, target in aliases.items():
298
+ routes[str(alias)] = {
299
+ 'provider': 'vllm',
300
+ 'runtime': sanitize_name(str(target)),
301
+ }
302
+ else:
303
+ for runtime_name, rt in runtimes.items():
304
+ route_name = str(
305
+ rt.get('public_name')
306
+ or rt.get('logical_model_name')
307
+ or rt.get('served_model_name')
308
+ or runtime_name
309
+ )
310
+ routes[route_name] = {'provider': 'vllm', 'runtime': runtime_name}
311
+ return {
312
+ 'name': name,
313
+ 'description': raw.get('description', ''),
314
+ 'kind': 'stack',
315
+ 'providers': {
316
+ 'vllm': {'enabled': True, 'runtimes': runtimes},
317
+ 'ollama': {'enabled': False},
318
+ },
319
+ 'gateways': {'litellm': {'enabled': True}},
320
+ 'frontends': {'open_webui': {'enabled': True, 'provider': 'litellm'}},
321
+ 'routes': routes,
322
+ 'policy': deepcopy(raw.get('policy', {})),
323
+ 'vllm': deepcopy(raw.get('vllm', {})),
324
+ 'tags': list(raw.get('tags', []) or []),
325
+ 'audit_notes': list(raw.get('audit_notes', []) or []),
326
+ 'notes': list(raw.get('notes', []) or []),
327
+ }
328
+
329
+
330
+ def _normalize_route_map(routes_raw: Any) -> dict[str, Any]:
331
+ if routes_raw is None:
332
+ return {}
333
+ if isinstance(routes_raw, list):
334
+ out: dict[str, Any] = {}
335
+ for item in routes_raw:
336
+ raw = deepcopy(item)
337
+ name = str(
338
+ raw.pop('name', raw.get('alias', raw.get('model', 'route')))
339
+ )
340
+ out[name] = raw
341
+ return out
342
+ return deepcopy(routes_raw)
343
+
344
+
345
+ def normalize_stack_profiles(
346
+ catalog: dict[str, Any],
347
+ vllm_models: dict[str, Any],
348
+ ollama_models: dict[str, Any],
349
+ ) -> dict[str, Any]:
350
+ normalized: dict[str, Any] = {}
351
+ for original_name, raw in (catalog or {}).items():
352
+ name = original_name
353
+ profile = deepcopy(raw) or {}
354
+ try:
355
+ if (
356
+ 'providers' not in profile
357
+ and 'routes' not in profile
358
+ and 'components' not in profile
359
+ ):
360
+ stack = _legacy_profile_to_stack(name, profile, vllm_models)
361
+ else:
362
+ providers, gateways, frontends = _normalize_components(profile)
363
+ stack = {
364
+ 'name': name,
365
+ 'description': profile.get('description', ''),
366
+ 'kind': profile.get('kind', 'stack'),
367
+ 'providers': providers,
368
+ 'gateways': gateways,
369
+ 'frontends': frontends,
370
+ 'routes': _normalize_route_map(profile.get('routes', {})),
371
+ 'policy': deepcopy(profile.get('policy', {})),
372
+ 'vllm': deepcopy(profile.get('vllm', {})),
373
+ 'tags': list(profile.get('tags', []) or []),
374
+ 'audit_notes': list(profile.get('audit_notes', []) or []),
375
+ 'notes': list(profile.get('notes', []) or []),
376
+ }
377
+ normalized[name] = stack
378
+ except KeyError as ex:
379
+ normalized[name] = {
380
+ 'name': name,
381
+ 'description': profile.get('description', ''),
382
+ 'kind': 'invalid-profile',
383
+ 'catalog_error': str(ex),
384
+ 'providers': {},
385
+ 'gateways': {},
386
+ 'frontends': {},
387
+ 'routes': {},
388
+ 'policy': deepcopy(profile.get('policy', {})),
389
+ 'vllm': deepcopy(profile.get('vllm', {})),
390
+ 'tags': list(profile.get('tags', []) or []),
391
+ 'audit_notes': list(profile.get('audit_notes', []) or []),
392
+ 'notes': list(profile.get('notes', []) or []),
393
+ }
394
+ return normalized
395
+
396
+
397
+ # Compatibility function name used in a few imports.
398
+ def normalize_profile_catalog(
399
+ catalog: dict[str, Any], models: dict[str, Any]
400
+ ) -> dict[str, Any]:
401
+ return normalize_stack_profiles(catalog, normalize_vllm_models(models), {})
402
+
403
+
404
+ def profile_summary(profile: dict[str, Any]) -> dict[str, Any]:
405
+ providers = []
406
+ p = profile.get('providers', {}) or {}
407
+ if (p.get('ollama') or {}).get('enabled') not in (False, 'false', None):
408
+ providers.append('ollama')
409
+ vllm_runtimes = (p.get('vllm') or {}).get('runtimes') or {}
410
+ if vllm_runtimes or (p.get('vllm') or {}).get('enabled') is True:
411
+ providers.append('vllm')
412
+ litellm = (profile.get('gateways', {}).get('litellm') or {}).get(
413
+ 'enabled', 'auto'
414
+ )
415
+ open_webui = profile.get('frontends', {}).get('open_webui', {}) or {}
416
+ return {
417
+ 'name': profile['name'],
418
+ 'public_name': profile.get('name', ''),
419
+ 'kind': profile.get('kind', 'stack'),
420
+ 'providers': providers,
421
+ 'gateway': 'litellm'
422
+ if litellm is True
423
+ else ('auto' if litellm == 'auto' else 'none'),
424
+ 'frontend': 'open_webui'
425
+ if open_webui.get('enabled', 'auto') not in (False, 'false')
426
+ else 'none',
427
+ 'frontend_provider': open_webui.get('provider', 'auto'),
428
+ 'route_count': len(profile.get('routes', {}) or {}),
429
+ 'description': profile.get('description', ''),
430
+ # Old fields kept so older list formatting doesn't crash.
431
+ 'base_model': '',
432
+ 'logical_model_name': '',
433
+ 'served_model_name': '',
434
+ 'protocol_mode': 'stack',
435
+ 'engine': ','.join(providers) or 'none',
436
+ 'resource_profile': '',
437
+ 'tags': profile.get('tags', []),
438
+ }
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env python3
2
+ # PYTHON_ARGCOMPLETE_OK
3
+ """scriptconfig-based CLI for infer-stack.
4
+
5
+ Each subcommand is a ``scfg.DataConfig`` subclass; ``ManageCLI`` composes
6
+ them into a single ``scfg.ModalCLI`` exposed as the ``infer-stack`` entry
7
+ point. Because every subcommand is a ``DataConfig``, the same class can
8
+ be invoked from the shell (``infer-stack render --profile X``) or from
9
+ Python (``RenderCLI.main(argv=False, profile='X')``).
10
+
11
+ This package was split out of a single ``cli.py`` module. The layers are:
12
+
13
+ * ``context`` — path/config/env/override/plan helpers (no other cli deps).
14
+ * ``probes`` — pure readiness/model-selection helpers over a deployment.
15
+ * ``compose`` — compose + LiteLLM + preflight + diagnostics helpers.
16
+ * ``options`` — shared ``DataConfig`` mixins for override flags.
17
+ * ``commands_profile`` — profile/config management subcommands.
18
+ * ``commands_runtime`` — up/down/deploy/status/env + day-2-ops wrappers.
19
+ * ``commands_smoke`` — diagnose/wait/smoke-test/benchmark subcommands.
20
+
21
+ ``infer_stack.cli`` re-exports the previously top-level names so existing
22
+ ``from infer_stack.cli import ...`` imports keep working.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import requests # noqa: F401 (cli_mod.requests is patched in tests)
28
+ import scriptconfig as scfg
29
+
30
+ # Keep submodules importable as attributes (e.g. ``infer_stack.cli.commands_runtime``)
31
+ # so tests can patch seams where they are actually looked up.
32
+ from . import ( # noqa: F401
33
+ commands_profile,
34
+ commands_runtime,
35
+ commands_smoke,
36
+ compose,
37
+ context,
38
+ options,
39
+ probes,
40
+ )
41
+ from ..kubeai_ops import ( # noqa: F401
42
+ CommandError,
43
+ deploy_rendered_artifacts,
44
+ print_status as kubeai_print_status,
45
+ )
46
+ from .commands_profile import (
47
+ DescribeProfileCLI,
48
+ ExplainCLI,
49
+ InitCLI,
50
+ KubeaiSyncResourceProfilesCLI,
51
+ ListModelsCLI,
52
+ ListProfilesCLI,
53
+ LockCLI,
54
+ RenderCLI,
55
+ ResolveCLI,
56
+ SetupCLI,
57
+ SwitchCLI,
58
+ ValidateCLI,
59
+ VerifyProfileCLI,
60
+ )
61
+ from .commands_runtime import (
62
+ DeployCLI,
63
+ DownCLI,
64
+ EnvCLI,
65
+ LogsCLI,
66
+ OllamaListCLI,
67
+ OllamaPsCLI,
68
+ OllamaPullCLI,
69
+ PsCLI,
70
+ PullCLI,
71
+ PurgeCLI,
72
+ RestartCLI,
73
+ StartCLI,
74
+ StatusCLI,
75
+ StopCLI,
76
+ UpCLI,
77
+ )
78
+ from .commands_smoke import (
79
+ BenchmarkCLI,
80
+ DiagnoseCLI,
81
+ SmokeTestCLI,
82
+ WaitReadyCLI,
83
+ )
84
+ from .compose import ( # noqa: F401
85
+ _compose_has_service,
86
+ _litellm_delete_missed_config_model,
87
+ )
88
+ from .context import ( # noqa: F401
89
+ apply_config_overrides,
90
+ backend_name,
91
+ build_plan,
92
+ config_for_runtime,
93
+ config_path,
94
+ effective_allow_unsupported,
95
+ effective_inventory,
96
+ ensure_renderable,
97
+ generated_dir,
98
+ has_runtime_overrides,
99
+ kubeai_generated_dir,
100
+ load_config,
101
+ models_path,
102
+ plan_path,
103
+ render_is_stale,
104
+ runtime_dir_for_config,
105
+ save_plan,
106
+ )
107
+ from .probes import ( # noqa: F401
108
+ _default_model_for_deployment,
109
+ _resolve_smoke_protocol_from_deployment,
110
+ )
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Modal CLI + entry point
115
+ # ---------------------------------------------------------------------------
116
+
117
+
118
+ class ManageCLI(scfg.ModalCLI):
119
+ description = (
120
+ 'Render and run vLLM serving profiles through the Compose or KubeAI '
121
+ 'backends. Primary workflow: setup -> render -> up (or deploy).'
122
+ )
123
+
124
+ # Config / profile management
125
+ setup = SetupCLI
126
+ init = InitCLI
127
+ resolve = ResolveCLI
128
+ validate = ValidateCLI
129
+ lock = LockCLI
130
+ render = RenderCLI
131
+ switch = SwitchCLI
132
+ list_models = ListModelsCLI
133
+ list_profiles = ListProfilesCLI
134
+ explain = ExplainCLI
135
+ describe_profile = DescribeProfileCLI
136
+ verify_profile = VerifyProfileCLI
137
+ kubeai_sync_resource_profiles = KubeaiSyncResourceProfilesCLI
138
+
139
+ # Runtime
140
+ up = UpCLI
141
+ down = DownCLI
142
+ purge = PurgeCLI
143
+ deploy = DeployCLI
144
+ status = StatusCLI
145
+ env = EnvCLI
146
+ diagnose = DiagnoseCLI
147
+ wait_ready = WaitReadyCLI
148
+ smoke_test = SmokeTestCLI
149
+ benchmark = BenchmarkCLI
150
+ ollama_pull = OllamaPullCLI
151
+ ollama_list = OllamaListCLI
152
+ ollama_ps = OllamaPsCLI
153
+
154
+ # Compose day-2-ops wrappers
155
+ logs = LogsCLI
156
+ ps = PsCLI
157
+ restart = RestartCLI
158
+ pull = PullCLI
159
+ start = StartCLI
160
+ stop = StopCLI
161
+
162
+
163
+ def main(argv=None) -> int:
164
+ rv = ManageCLI.main(argv=argv)
165
+ return int(rv) if rv is not None else 0
166
+
167
+
168
+ if __name__ == '__main__':
169
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ from . import main
2
+
3
+ if __name__ == '__main__':
4
+ raise SystemExit(main())