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.
- infer_stack/__init__.py +2 -0
- infer_stack/backends/__init__.py +7 -0
- infer_stack/backends/compose_renderer.py +243 -0
- infer_stack/backends/kubeai_renderer.py +202 -0
- infer_stack/benchmark.py +38 -0
- infer_stack/catalog.py +438 -0
- infer_stack/cli/__init__.py +169 -0
- infer_stack/cli/__main__.py +4 -0
- infer_stack/cli/commands_profile.py +467 -0
- infer_stack/cli/commands_runtime.py +719 -0
- infer_stack/cli/commands_smoke.py +691 -0
- infer_stack/cli/compose.py +755 -0
- infer_stack/cli/context.py +471 -0
- infer_stack/cli/options.py +134 -0
- infer_stack/cli/probes.py +178 -0
- infer_stack/config.py +450 -0
- infer_stack/contracts.py +223 -0
- infer_stack/diff_prompt.py +117 -0
- infer_stack/docker_utils.py +230 -0
- infer_stack/env_utils.py +97 -0
- infer_stack/experimental/model_catalog_discover.py +1155 -0
- infer_stack/experimental/model_memory_estimator.py +1264 -0
- infer_stack/experimental/stress_test_long_context.py +397 -0
- infer_stack/hardware.py +70 -0
- infer_stack/kubeai_ops.py +76 -0
- infer_stack/paths.py +87 -0
- infer_stack/profile_runtime.py +46 -0
- infer_stack/renderer.py +19 -0
- infer_stack/resolver.py +1092 -0
- infer_stack/templates/default-models.yaml +674 -0
- infer_stack/templates/default-ollama-models.yaml +31 -0
- infer_stack/templates/default-profiles.yaml +1731 -0
- infer_stack/templates/default-vllm-models.yaml +714 -0
- infer_stack/templates/docker-compose.yml.j2 +430 -0
- infer_stack/templates/litellm_config.yaml.j2 +44 -0
- infer_stack/templates/nginx.conf.j2 +84 -0
- infer_stack/tuning.py +3 -0
- infer_stack/validator.py +314 -0
- infer_stack/verification.py +46 -0
- infer_stack-0.6.0.dist-info/METADATA +1034 -0
- infer_stack-0.6.0.dist-info/RECORD +44 -0
- infer_stack-0.6.0.dist-info/WHEEL +5 -0
- infer_stack-0.6.0.dist-info/entry_points.txt +2 -0
- 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())
|