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
@@ -0,0 +1,467 @@
1
+ from __future__ import annotations
2
+
3
+ from ..catalog import PROFILE_NAME_ALIASES
4
+ from ..catalog import profile_summary
5
+ from ..config import initial_config
6
+ from ..config import load_kubeai_resource_profiles
7
+ from ..config import load_yaml
8
+ from ..config import normalized_catalogs
9
+ from ..config import save_kubeai_resource_profiles
10
+ from ..config import save_yaml
11
+ from ..contracts import load_profile_contract
12
+ from ..kubeai_ops import deploy_rendered_artifacts
13
+ from ..renderer import render_from_lock
14
+ from ..verification import verify_profile
15
+ from pathlib import Path
16
+ from typing import Any
17
+ import json
18
+ import scriptconfig as scfg
19
+
20
+ from .context import (
21
+ _apply_path_overrides,
22
+ _arg_or_env,
23
+ _as_mapping,
24
+ apply_config_overrides,
25
+ backend_name,
26
+ build_plan,
27
+ config_for_runtime,
28
+ config_path,
29
+ effective_allow_unsupported,
30
+ effective_inventory,
31
+ ensure_renderable,
32
+ generated_dir,
33
+ kubeai_generated_dir,
34
+ load_config,
35
+ models_path,
36
+ plan_path,
37
+ runtime_dir_for_config,
38
+ save_plan,
39
+ )
40
+ from .compose import _compose_up_with_router_recreate
41
+ from .options import (
42
+ _AllowUnsupportedMixin,
43
+ _BackendOverrideMixin,
44
+ _ClusterOverridesMixin,
45
+ _ComposeOverrideMixin,
46
+ _PathOverridesMixin,
47
+ _PlanOverridesCLI,
48
+ _PortOverridesMixin,
49
+ _ProfileOverrideMixin,
50
+ _SimulateHardwareMixin,
51
+ _SwitchPathOverridesCLI,
52
+ )
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Profile / config management commands
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ class InitCLI(_PathOverridesMixin):
60
+ """Write a fresh config.yaml + empty models.yaml under config_root()."""
61
+
62
+ force = scfg.Value(
63
+ False, isflag=True, help='Overwrite an existing config.yaml.'
64
+ )
65
+
66
+ @classmethod
67
+ def main(cls, argv=True, **kwargs):
68
+ config = cls.cli(argv=argv, data=kwargs)
69
+ _apply_path_overrides(config)
70
+ cfg_path = config_path()
71
+ if cfg_path.exists() and not config.force:
72
+ raise SystemExit(
73
+ 'config.yaml already exists. Use --force to overwrite.'
74
+ )
75
+ save_yaml(cfg_path, initial_config())
76
+ if not models_path().exists():
77
+ save_yaml(models_path(), {'models': {}, 'profiles': {}})
78
+ print(f'Wrote {cfg_path}')
79
+ return 0
80
+
81
+
82
+ class SetupCLI(
83
+ _PathOverridesMixin,
84
+ _ProfileOverrideMixin,
85
+ _BackendOverrideMixin,
86
+ _ComposeOverrideMixin,
87
+ _PortOverridesMixin,
88
+ _ClusterOverridesMixin,
89
+ ):
90
+ """Create / update config.yaml with the requested overrides applied."""
91
+
92
+ reset = scfg.Value(
93
+ False,
94
+ isflag=True,
95
+ help='Start from default config values before applying overrides.',
96
+ )
97
+ resource_profiles_file = scfg.Value(
98
+ None,
99
+ type=str,
100
+ help='For kubeai setups, sync a local Helm values file with resourceProfiles into kubeai-values.local.yaml.',
101
+ )
102
+
103
+ @classmethod
104
+ def main(cls, argv=True, **kwargs):
105
+ config = cls.cli(argv=argv, data=kwargs)
106
+ _apply_path_overrides(config)
107
+ cfg_path = config_path()
108
+ if cfg_path.exists() and not config.reset:
109
+ cfg = load_yaml(cfg_path)
110
+ else:
111
+ cfg = initial_config()
112
+ cfg = apply_config_overrides(cfg, config)
113
+ save_yaml(cfg_path, cfg)
114
+ if not models_path().exists():
115
+ save_yaml(models_path(), {'models': {}, 'profiles': {}})
116
+ if config.resource_profiles_file:
117
+ # User-supplied path: anchor on CWD so a typed relative path
118
+ # behaves as the user expects.
119
+ source = Path(config.resource_profiles_file)
120
+ if not source.is_absolute():
121
+ source = Path.cwd() / source
122
+ values_doc = load_yaml(source)
123
+ if 'resourceProfiles' not in values_doc:
124
+ raise SystemExit(
125
+ f'{source} is missing a top-level resourceProfiles map'
126
+ )
127
+ target = save_kubeai_resource_profiles(values_doc)
128
+ if plan_path(cfg).exists():
129
+ plan_path(cfg).unlink()
130
+ print(f'Wrote {target}')
131
+ print(f'Wrote {cfg_path}')
132
+ print(
133
+ f'Configured backend={cfg.get("backend", "compose")} '
134
+ f'active_profile={cfg.get("active_profile", "") or "<unset>"}'
135
+ )
136
+ return 0
137
+
138
+
139
+ class ResolveCLI(_PlanOverridesCLI):
140
+ """Resolve the active profile into a deployment dict and print it."""
141
+
142
+ @classmethod
143
+ def main(cls, argv=True, **kwargs):
144
+ config = cls.cli(argv=argv, data=kwargs)
145
+ _apply_path_overrides(config)
146
+ cfg = config_for_runtime(config)
147
+ plan = build_plan(
148
+ cfg,
149
+ profile_name=config.profile,
150
+ allow_unsupported=effective_allow_unsupported(config, cfg),
151
+ inventory=effective_inventory(config),
152
+ )
153
+ save_plan(plan, cfg)
154
+ print(json.dumps(plan['deployment'], indent=2))
155
+ return 0
156
+
157
+
158
+ class ValidateCLI(_PlanOverridesCLI):
159
+ """Resolve + validate the active profile; exit non-zero on validation errors."""
160
+
161
+ @classmethod
162
+ def main(cls, argv=True, **kwargs):
163
+ config = cls.cli(argv=argv, data=kwargs)
164
+ _apply_path_overrides(config)
165
+ cfg = config_for_runtime(config)
166
+ plan = build_plan(
167
+ cfg,
168
+ profile_name=config.profile,
169
+ allow_unsupported=effective_allow_unsupported(config, cfg),
170
+ inventory=effective_inventory(config),
171
+ )
172
+ save_plan(plan, cfg)
173
+ print(json.dumps(plan['validated'], indent=2))
174
+ return 0 if plan['validated']['ok'] else 2
175
+
176
+
177
+ class LockCLI(_PlanOverridesCLI):
178
+ """Write plan.yaml after resolving + validating the active profile."""
179
+
180
+ @classmethod
181
+ def main(cls, argv=True, **kwargs):
182
+ config = cls.cli(argv=argv, data=kwargs)
183
+ _apply_path_overrides(config)
184
+ cfg = config_for_runtime(config)
185
+ plan = build_plan(
186
+ cfg,
187
+ profile_name=config.profile,
188
+ allow_unsupported=effective_allow_unsupported(config, cfg),
189
+ inventory=effective_inventory(config),
190
+ )
191
+ if not plan['validated']['ok'] and not plan['allow_unsupported']:
192
+ raise SystemExit(
193
+ 'Refusing to write plan.yaml because validation failed. Use --allow-unsupported to override.'
194
+ )
195
+ save_plan(plan, cfg)
196
+ print(json.dumps(plan, indent=2))
197
+ return 0
198
+
199
+
200
+ class RenderCLI(_PlanOverridesCLI):
201
+ """Render the active profile's deployment into compose/kubeai artifacts."""
202
+
203
+ yes = scfg.Value(
204
+ False,
205
+ isflag=True,
206
+ short_alias=['y'],
207
+ help='Apply rendered changes without prompting. Without this, render shows a per-file diff and asks for confirmation.',
208
+ )
209
+
210
+ @classmethod
211
+ def main(cls, argv=True, **kwargs):
212
+ config = cls.cli(argv=argv, data=kwargs)
213
+ _apply_path_overrides(config)
214
+ cfg = config_for_runtime(config)
215
+ plan = build_plan(
216
+ cfg,
217
+ profile_name=config.profile,
218
+ allow_unsupported=effective_allow_unsupported(config, cfg),
219
+ inventory=effective_inventory(config),
220
+ )
221
+ ensure_renderable(plan)
222
+ save_plan(plan, cfg)
223
+ render_from_lock(plan, assume_yes=bool(config.yes))
224
+ print(f'Wrote {plan_path(cfg)}')
225
+ if backend_name(cfg) == 'kubeai':
226
+ print(f'Rendered KubeAI artifacts into {kubeai_generated_dir(cfg)}')
227
+ else:
228
+ print(f'Rendered Compose into {generated_dir(cfg)}')
229
+ print(
230
+ f'Rendered mounted runtime files into {runtime_dir_for_config(cfg)}'
231
+ )
232
+ return 0
233
+
234
+
235
+ class SwitchCLI(_SwitchPathOverridesCLI):
236
+ """Persist a new active_profile and re-render (optionally re-applying)."""
237
+
238
+ profile = scfg.Value(
239
+ None, type=str, position=1, help='Profile name to switch to.'
240
+ )
241
+ apply = scfg.Value(
242
+ False,
243
+ isflag=True,
244
+ help='Also apply the new profile to the running stack.',
245
+ )
246
+ yes = scfg.Value(
247
+ False,
248
+ isflag=True,
249
+ short_alias=['y'],
250
+ help='Apply rendered changes without prompting.',
251
+ )
252
+
253
+ @classmethod
254
+ def main(cls, argv=True, **kwargs):
255
+ config = cls.cli(argv=argv, data=kwargs)
256
+ _apply_path_overrides(config)
257
+ if not config.profile:
258
+ raise SystemExit('switch: missing required profile name')
259
+ persisted_cfg = load_config()
260
+ persisted_cfg['active_profile'] = config.profile
261
+ save_yaml(config_path(), persisted_cfg)
262
+ cfg = apply_config_overrides(persisted_cfg, config)
263
+
264
+ plan = build_plan(
265
+ cfg,
266
+ profile_name=config.profile,
267
+ allow_unsupported=effective_allow_unsupported(config, cfg),
268
+ inventory=effective_inventory(config),
269
+ )
270
+ ensure_renderable(plan)
271
+ save_plan(plan, cfg)
272
+ render_from_lock(plan, assume_yes=bool(config.yes))
273
+ if config.apply:
274
+ if backend_name(cfg) == 'compose':
275
+ _compose_up_with_router_recreate(cfg, detach=True)
276
+ else:
277
+ deploy_rendered_artifacts(plan['deployment'])
278
+ print(f'Switched active_profile to {config.profile}')
279
+ return 0
280
+
281
+
282
+ class ListModelsCLI(_PathOverridesMixin):
283
+ """Print every model in the merged catalog."""
284
+
285
+ __command__ = 'list-models'
286
+
287
+ @classmethod
288
+ def main(cls, argv=True, **kwargs):
289
+ config = cls.cli(argv=argv, data=kwargs)
290
+ _apply_path_overrides(config)
291
+ cfg = load_config() if config_path().exists() else initial_config()
292
+ cats = normalized_catalogs(cfg)
293
+ for name, model in cats.get('models', {}).items():
294
+ ref = model.get('hf_model_id') or model.get('url', '')
295
+ print(f'{name}: {ref}')
296
+ return 0
297
+
298
+
299
+ class ListProfilesCLI(_PathOverridesMixin):
300
+ """Print every serving profile in the merged catalog."""
301
+
302
+ __command__ = 'list-profiles'
303
+
304
+ @classmethod
305
+ def main(cls, argv=True, **kwargs):
306
+ config = cls.cli(argv=argv, data=kwargs)
307
+ _apply_path_overrides(config)
308
+ cfg = load_config() if config_path().exists() else initial_config()
309
+ cats = normalized_catalogs(cfg)
310
+ profiles = cats.get('profiles', {})
311
+ hidden_legacy = set(PROFILE_NAME_ALIASES)
312
+ for name, profile in profiles.items():
313
+ if name in hidden_legacy:
314
+ continue
315
+ if profile.get('kind') == 'invalid-profile':
316
+ continue
317
+ summary = profile_summary(profile)
318
+ providers = ','.join(summary.get('providers', [])) or 'none'
319
+ print(
320
+ f'{name}: providers={providers} gateway={summary["gateway"]} '
321
+ f'frontend={summary["frontend"]} frontend_provider={summary["frontend_provider"]} '
322
+ f'routes={summary["route_count"]}'
323
+ )
324
+ return 0
325
+
326
+
327
+ class ExplainCLI(_PathOverridesMixin):
328
+ """Pretty-print a YAML file (defaults to the current plan.yaml)."""
329
+
330
+ __command__ = 'explain'
331
+
332
+ file = scfg.Value(
333
+ None, type=str, help='Path to read (default: current plan.yaml).'
334
+ )
335
+
336
+ @classmethod
337
+ def main(cls, argv=True, **kwargs):
338
+ config = cls.cli(argv=argv, data=kwargs)
339
+ _apply_path_overrides(config)
340
+ if config.file:
341
+ target = Path(config.file)
342
+ if not target.is_absolute():
343
+ target = Path.cwd() / target
344
+ else:
345
+ target = plan_path()
346
+ if not target.exists():
347
+ raise SystemExit(f'Missing file: {target}')
348
+ print(json.dumps(load_yaml(target), indent=2))
349
+ return 0
350
+
351
+
352
+ class DescribeProfileCLI(
353
+ _PathOverridesMixin,
354
+ _BackendOverrideMixin,
355
+ _AllowUnsupportedMixin,
356
+ _SimulateHardwareMixin,
357
+ ):
358
+ """Print the profile contract for a given profile name."""
359
+
360
+ __command__ = 'describe-profile'
361
+
362
+ profile = scfg.Value(
363
+ None, type=str, position=1, help='Profile name to describe.'
364
+ )
365
+ format = scfg.Value('yaml', choices=['json', 'yaml'])
366
+ output = scfg.Value(
367
+ None, type=str, help='Write to this file instead of stdout.'
368
+ )
369
+
370
+ @classmethod
371
+ def main(cls, argv=True, **kwargs):
372
+ config = cls.cli(argv=argv, data=kwargs)
373
+ _apply_path_overrides(config)
374
+ overrides = _as_mapping(config)
375
+ if not overrides.get('profile'):
376
+ raise SystemExit('describe-profile: missing required profile name')
377
+ contract = load_profile_contract(
378
+ overrides['profile'],
379
+ backend=_arg_or_env(overrides, 'backend', 'INFER_STACK_BACKEND'),
380
+ simulate_hardware_spec=overrides.get('simulate_hardware'),
381
+ )
382
+ return _print_structured(
383
+ contract, overrides['format'], overrides.get('output')
384
+ )
385
+
386
+
387
+ def _print_structured(
388
+ data: dict[str, Any], fmt: str, output: str | None
389
+ ) -> int:
390
+ if fmt == 'yaml':
391
+ import yaml
392
+
393
+ text = yaml.safe_dump(data, sort_keys=False)
394
+ else:
395
+ text = json.dumps(data, indent=2)
396
+ if output:
397
+ target = Path(output)
398
+ target.parent.mkdir(parents=True, exist_ok=True)
399
+ target.write_text(
400
+ text + ('' if text.endswith('\n') else '\n'), encoding='utf-8'
401
+ )
402
+ print(f'Wrote {target}')
403
+ return 0
404
+ print(text)
405
+ return 0
406
+
407
+
408
+ class VerifyProfileCLI(_SwitchPathOverridesCLI):
409
+ """Sanity-check a resolved profile (post-render expectations)."""
410
+
411
+ __command__ = 'verify-profile'
412
+
413
+ profile = scfg.Value(
414
+ None, type=str, position=1, help='Profile name to verify.'
415
+ )
416
+
417
+ @classmethod
418
+ def main(cls, argv=True, **kwargs):
419
+ config = cls.cli(argv=argv, data=kwargs)
420
+ _apply_path_overrides(config)
421
+ if not config.profile:
422
+ raise SystemExit('verify-profile: missing required profile name')
423
+ cfg = config_for_runtime(config)
424
+ plan = build_plan(
425
+ cfg,
426
+ profile_name=config.profile,
427
+ allow_unsupported=effective_allow_unsupported(config, cfg),
428
+ inventory=effective_inventory(config),
429
+ )
430
+ result = verify_profile(plan['deployment'])
431
+ print(json.dumps(result, indent=2))
432
+ return 0 if result['ok'] else 2
433
+
434
+
435
+ class KubeaiSyncResourceProfilesCLI(_PathOverridesMixin):
436
+ """Pull a Helm ``resourceProfiles`` values file into kubeai-values.local.yaml."""
437
+
438
+ __command__ = 'kubeai-sync-resource-profiles'
439
+
440
+ from_file = scfg.Value(
441
+ None,
442
+ type=str,
443
+ required=True,
444
+ help='Helm values file with a top-level resourceProfiles map.',
445
+ )
446
+
447
+ @classmethod
448
+ def main(cls, argv=True, **kwargs):
449
+ config = cls.cli(argv=argv, data=kwargs)
450
+ _apply_path_overrides(config)
451
+ cfg = config_for_runtime(config, allow_missing=True)
452
+ # User-supplied path: anchor on CWD.
453
+ source = Path(config.from_file)
454
+ if not source.is_absolute():
455
+ source = Path.cwd() / source
456
+ values_doc = load_yaml(source)
457
+ if 'resourceProfiles' not in values_doc:
458
+ raise SystemExit(
459
+ f'{source} is missing a top-level resourceProfiles map'
460
+ )
461
+ target = save_kubeai_resource_profiles(values_doc)
462
+ if plan_path(cfg).exists():
463
+ plan_path(cfg).unlink()
464
+ profiles, _, _ = load_kubeai_resource_profiles()
465
+ print(f'Wrote {target}')
466
+ print(f'Synced {len(profiles)} KubeAI resource profile(s)')
467
+ return 0