freesolo-flash-dev 0.2.25__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 (111) hide show
  1. flash/__init__.py +29 -0
  2. flash/_channel.py +23 -0
  3. flash/_fileio.py +35 -0
  4. flash/_logging.py +49 -0
  5. flash/_update_check.py +266 -0
  6. flash/catalog.py +253 -0
  7. flash/cli/__init__.py +1 -0
  8. flash/cli/main/__init__.py +227 -0
  9. flash/cli/main/__main__.py +6 -0
  10. flash/cli/main/commands.py +636 -0
  11. flash/cli/main/envpush.py +317 -0
  12. flash/cli/main/render.py +599 -0
  13. flash/cli/main/training_doc.py +455 -0
  14. flash/client/__init__.py +14 -0
  15. flash/client/config.py +70 -0
  16. flash/client/http.py +372 -0
  17. flash/client/runtime_secrets.py +69 -0
  18. flash/client/specs.py +20 -0
  19. flash/cost/__init__.py +16 -0
  20. flash/cost/analytical.py +175 -0
  21. flash/cost/facts.py +114 -0
  22. flash/cost/spec.py +113 -0
  23. flash/cost/types.py +158 -0
  24. flash/engine/__init__.py +6 -0
  25. flash/engine/accounting.py +36 -0
  26. flash/engine/chalk_kernels.py +116 -0
  27. flash/engine/multiturn_rollout.py +780 -0
  28. flash/engine/recipe.py +86 -0
  29. flash/engine/vram.py +603 -0
  30. flash/engine/worker/__init__.py +2916 -0
  31. flash/engine/worker/__main__.py +4 -0
  32. flash/engine/worker/kernel_warmup.py +400 -0
  33. flash/engine/worker/lora.py +796 -0
  34. flash/engine/worker/packing.py +366 -0
  35. flash/engine/worker/perf.py +1048 -0
  36. flash/envs/__init__.py +10 -0
  37. flash/envs/adapter/__init__.py +883 -0
  38. flash/envs/adapter/rubric.py +222 -0
  39. flash/envs/base.py +52 -0
  40. flash/envs/registry.py +62 -0
  41. flash/mcp/__init__.py +1 -0
  42. flash/mcp/server.py +85 -0
  43. flash/providers/__init__.py +59 -0
  44. flash/providers/_auth.py +24 -0
  45. flash/providers/_http.py +230 -0
  46. flash/providers/_instance.py +416 -0
  47. flash/providers/_instance_bootstrap.py +517 -0
  48. flash/providers/_poll.py +311 -0
  49. flash/providers/allocator.py +193 -0
  50. flash/providers/base.py +431 -0
  51. flash/providers/hyperstack/__init__.py +127 -0
  52. flash/providers/hyperstack/api.py +522 -0
  53. flash/providers/hyperstack/auth.py +17 -0
  54. flash/providers/hyperstack/gpus.py +29 -0
  55. flash/providers/hyperstack/jobs/__init__.py +632 -0
  56. flash/providers/hyperstack/jobs/builders.py +122 -0
  57. flash/providers/hyperstack/preflight.py +23 -0
  58. flash/providers/hyperstack/pricing.py +26 -0
  59. flash/providers/hyperstack/train.py +25 -0
  60. flash/providers/lambdalabs/__init__.py +139 -0
  61. flash/providers/lambdalabs/api.py +261 -0
  62. flash/providers/lambdalabs/auth.py +18 -0
  63. flash/providers/lambdalabs/gpus.py +29 -0
  64. flash/providers/lambdalabs/jobs/__init__.py +724 -0
  65. flash/providers/lambdalabs/jobs/builders.py +118 -0
  66. flash/providers/lambdalabs/preflight.py +27 -0
  67. flash/providers/lambdalabs/pricing.py +51 -0
  68. flash/providers/lambdalabs/train.py +27 -0
  69. flash/providers/preflight.py +55 -0
  70. flash/providers/realized.py +80 -0
  71. flash/providers/runpod/__init__.py +130 -0
  72. flash/providers/runpod/api.py +186 -0
  73. flash/providers/runpod/auth.py +37 -0
  74. flash/providers/runpod/cost.py +57 -0
  75. flash/providers/runpod/gpus.py +46 -0
  76. flash/providers/runpod/jobs.py +956 -0
  77. flash/providers/runpod/keys.py +139 -0
  78. flash/providers/runpod/preflight.py +30 -0
  79. flash/providers/runpod/preload.py +915 -0
  80. flash/providers/runpod/pricing.py +18 -0
  81. flash/providers/runpod/slots.py +79 -0
  82. flash/providers/runpod/train/__init__.py +150 -0
  83. flash/providers/runpod/train/deps.py +395 -0
  84. flash/providers/runpod/train/endpoints.py +820 -0
  85. flash/py.typed +0 -0
  86. flash/runner/__init__.py +686 -0
  87. flash/runner/checkpoints.py +82 -0
  88. flash/runner/deploy.py +422 -0
  89. flash/runner/lifecycle.py +672 -0
  90. flash/schema/__init__.py +375 -0
  91. flash/schema/fields.py +331 -0
  92. flash/serve/__init__.py +1 -0
  93. flash/serve/deploy.py +326 -0
  94. flash/serve/pricing.py +60 -0
  95. flash/server/__init__.py +1 -0
  96. flash/server/__main__.py +20 -0
  97. flash/server/app.py +961 -0
  98. flash/server/auth.py +263 -0
  99. flash/server/billing.py +124 -0
  100. flash/server/checkpoints.py +110 -0
  101. flash/server/db.py +160 -0
  102. flash/server/environment_registry.py +102 -0
  103. flash/server/envs.py +360 -0
  104. flash/server/reconcile.py +163 -0
  105. flash/server/run_registry.py +150 -0
  106. flash/spec.py +333 -0
  107. freesolo_flash_dev-0.2.25.dist-info/METADATA +192 -0
  108. freesolo_flash_dev-0.2.25.dist-info/RECORD +111 -0
  109. freesolo_flash_dev-0.2.25.dist-info/WHEEL +4 -0
  110. freesolo_flash_dev-0.2.25.dist-info/entry_points.txt +3 -0
  111. freesolo_flash_dev-0.2.25.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,599 @@
1
+ """Human-facing rendering for `flash login` / `flash whoami` (stdlib only).
2
+
3
+ The identity behind a stored key is shown as a small aligned card instead of raw
4
+ ``json.dumps`` output. ANSI styling and unicode glyphs are applied only when stdout is
5
+ an interactive terminal that can encode them; piped, captured, or ASCII-locale output
6
+ stays plain so it can be grepped or parsed and never raises ``UnicodeEncodeError``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import sys
13
+
14
+ # Identity fields the control plane may return (flash/server/app.py `/v1/me`), in the
15
+ # order we show them. ``key_prefix``/``kind`` render separately on the key line.
16
+ _ROWS = (
17
+ ("email", "account"),
18
+ ("org_id", "org"),
19
+ ("user_id", "user"),
20
+ ("project_id", "project"),
21
+ ("training_agent_job_id", "job"),
22
+ )
23
+
24
+ _KIND_LABEL = {
25
+ "internal": "internal key",
26
+ "freesolo_api_key": "freesolo key",
27
+ }
28
+
29
+
30
+ def _flag(name: str) -> bool | None:
31
+ """Tri-state read of an env flag: True/False when set to a recognizable value, else None."""
32
+ raw = os.environ.get(name)
33
+ if raw is None:
34
+ return None
35
+ val = raw.strip().lower()
36
+ if val in {"1", "true", "yes", "on"}:
37
+ return True
38
+ if val in {"", "0", "false", "no", "off"}:
39
+ return False
40
+ # Unrecognized value (e.g. a typo): stay tri-state None so styled() falls back to isatty
41
+ # rather than silently forcing the theme on.
42
+ return None
43
+
44
+
45
+ def styled() -> bool:
46
+ """Whether to render the themed human layout (tables, badges, panels) instead of the plain
47
+ machine form. True on an interactive stdout; ``FLASH_STYLE=1``/``0`` forces it on or off
48
+ (used to render the docs preview, and to keep output deterministic in scripts)."""
49
+ forced = _flag("FLASH_STYLE")
50
+ if forced is not None:
51
+ return forced
52
+ return sys.stdout.isatty()
53
+
54
+
55
+ def _color() -> bool:
56
+ """Whether ANSI color may be emitted: the themed layout, minus a NO_COLOR / dumb-terminal opt-out."""
57
+ return styled() and "NO_COLOR" not in os.environ and os.environ.get("TERM") != "dumb"
58
+
59
+
60
+ def _style(code: str, text: str) -> str:
61
+ return f"\x1b[{code}m{text}\x1b[0m" if _color() else text
62
+
63
+
64
+ def _glyph(unicode_glyph: str, ascii_fallback: str) -> str:
65
+ """Use the unicode glyph only if stdout can encode it (else an ASCII stand-in)."""
66
+ enc = sys.stdout.encoding or "ascii"
67
+ try:
68
+ unicode_glyph.encode(enc)
69
+ except (UnicodeError, LookupError):
70
+ return ascii_fallback
71
+ return unicode_glyph
72
+
73
+
74
+ # Common unicode punctuation we'd rather downgrade to a readable ASCII stand-in than to an
75
+ # escape sequence when stdout can't encode it (e.g. an ASCII/non-UTF-8 locale). Written as
76
+ # escapes so the source stays free of confusable characters.
77
+ _ASCII_PUNCT = {
78
+ 0x2014: "-", # em dash
79
+ 0x2013: "-", # en dash
80
+ 0x2026: "...", # ellipsis
81
+ 0x2019: "'", # right single quote
82
+ 0x201C: '"', # left double quote
83
+ 0x201D: '"', # right double quote
84
+ }
85
+
86
+
87
+ def _safe(text: str) -> str:
88
+ """Guarantee ``text`` can be printed under the current stdout encoding.
89
+
90
+ Identity values from ``/v1/me`` (e.g. an internationalized email) or punctuation in our
91
+ own copy must never make ``print()`` raise ``UnicodeEncodeError`` after a login has
92
+ already succeeded. On a normal UTF-8 terminal the text is returned unchanged; only when
93
+ the active encoding can't represent it do we downgrade punctuation and escape the rest.
94
+ """
95
+ enc = sys.stdout.encoding or "ascii"
96
+ try:
97
+ text.encode(enc)
98
+ except UnicodeEncodeError:
99
+ return text.translate(_ASCII_PUNCT).encode(enc, "backslashreplace").decode(enc)
100
+ return text
101
+
102
+
103
+ def _bold(s: str) -> str:
104
+ return _style("1", s)
105
+
106
+
107
+ def _dim(s: str) -> str:
108
+ return _style("2", s)
109
+
110
+
111
+ def format_identity(me: dict) -> str:
112
+ """Render the stored key's identity as an aligned card (not raw JSON)."""
113
+ rows = [(label, str(me[field])) for field, label in _ROWS if me.get(field)]
114
+ prefix = me.get("key_prefix")
115
+ kind = _KIND_LABEL.get(me.get("kind", ""), me.get("kind") or "api key")
116
+ rows.append(("key", f"{prefix}{_glyph('…', '...')} {_dim(f'({kind})')}" if prefix else kind))
117
+ width = max(len(label) for label, _ in rows)
118
+ return "\n".join(f" {_dim(label.ljust(width))} {value}" for label, value in rows)
119
+
120
+
121
+ def login_ok(me: dict | None) -> str:
122
+ head = f"{_paint(_glyph('✓', 'ok:'), _GREEN, '1')} {_bold('logged in to flash')}"
123
+ if not me:
124
+ # The key is verified and stored; we just couldn't fetch the account card right now.
125
+ return _safe(
126
+ f"{head}\n{_dim(' account details unavailable right now — run `flash whoami` later')}"
127
+ )
128
+ return _safe(f"{head}\n\n{format_identity(me)}")
129
+
130
+
131
+ def whoami(me: dict) -> str:
132
+ return _safe(f"{_bold('logged in to flash')}\n\n{format_identity(me)}")
133
+
134
+
135
+ def login_failed(reason: str) -> str:
136
+ return _safe(
137
+ f"{_paint(_glyph('✗', 'x:'), _RED, '1')} {_bold('login failed')}\n"
138
+ f" {reason}\n"
139
+ f" {_dim('then run `flash login --api-key <key>` to try again')}\n"
140
+ f" {_dim('if it keeps failing, email founders@freesolo.co')}"
141
+ )
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Theme — one visual language shared by every styled command.
146
+ #
147
+ # These renderers are only ever called when `styled()` is true (an interactive stdout, or
148
+ # FLASH_STYLE=1). Each command keeps its exact plain/JSON output on the machine path, so
149
+ # `jq`, scripts, and the agent contract are untouched; this is purely the human view.
150
+ # 256-color SGR keeps the palette readable on essentially every modern terminal.
151
+ # ---------------------------------------------------------------------------
152
+
153
+ # The Freesolo brand palette, pulled from the website (frontend/app/globals.css): navy
154
+ # `--special` #1b1b4b, periwinkle `--periwinkle` #5f72ff (the accent/ring), green `--green`
155
+ # #57ff8f, and the deep teal `--green-deep` #00695c. The site ships a light and a dark theme,
156
+ # so the CLI mirrors that: each semantic color carries a (hex, xterm-256 fallback) for each
157
+ # mode. On a dark terminal periwinkle accents and the bright green read well; on a light
158
+ # terminal those wash out, so — exactly as the website does — green falls back to the deep
159
+ # teal and the accents deepen. Truecolor terminals (and the docs gallery) get the exact hex;
160
+ # everything else the nearest 256-color approximation.
161
+ _PALETTE: dict[str, dict[str, tuple[str, int]]] = {
162
+ # role dark (on navy) light (on white)
163
+ "accent": {"dark": ("5f72ff", 63), "light": ("5f72ff", 63)}, # periwinkle — brand accent
164
+ "accent2": {"dark": ("97a3ff", 111), "light": ("3a46c8", 62)}, # keys, ids, links, snippets
165
+ "green": {"dark": ("57ff8f", 84), "light": ("00695c", 23)}, # success (deep teal on light)
166
+ "teal": {"dark": ("24c2a8", 43), "light": ("0e7490", 30)}, # amounts / numbers
167
+ "red": {"dark": ("ff6b6b", 203), "light": ("cc3b3b", 160)}, # destructive
168
+ "violet": {"dark": ("9a8cff", 105), "light": ("6d28d9", 92)}, # JSON literals
169
+ "gray": {"dark": ("8a93a8", 245), "light": ("5b6472", 242)}, # neutral
170
+ "faint": {"dark": ("4d5470", 240), "light": ("9aa1b5", 248)}, # rules, punctuation
171
+ }
172
+ # semantic handles used throughout the renderers (resolved per mode at paint time)
173
+ _ACCENT, _ACCENT2, _GREEN, _TEAL, _RED, _VIOLET, _GRAY, _FAINT = (
174
+ "accent",
175
+ "accent2",
176
+ "green",
177
+ "teal",
178
+ "red",
179
+ "violet",
180
+ "gray",
181
+ "faint",
182
+ )
183
+
184
+
185
+ def _truecolor() -> bool:
186
+ """Whether the terminal advertises 24-bit color (so we can emit exact brand hex)."""
187
+ return os.environ.get("COLORTERM", "").lower() in {"truecolor", "24bit"}
188
+
189
+
190
+ def _theme() -> str:
191
+ """Active theme: ``light`` or ``dark``. ``FLASH_THEME`` wins; otherwise a light terminal
192
+ background (via the de-facto ``COLORFGBG`` env) selects light. Defaults to ``dark`` (the
193
+ safe default for the colors, and unchanged behavior when nothing is set)."""
194
+ forced = os.environ.get("FLASH_THEME", "").strip().lower()
195
+ if forced in {"light", "dark"}:
196
+ return forced
197
+ fgbg = os.environ.get("COLORFGBG", "")
198
+ if fgbg:
199
+ try:
200
+ bg = int(fgbg.split(";")[-1])
201
+ except ValueError:
202
+ return "dark"
203
+ # ANSI bg 7 (light grey) and 11-15 (bright/white) mean a light terminal background
204
+ return "light" if bg == 7 or bg >= 11 else "dark"
205
+ return "dark"
206
+
207
+
208
+ def _sgr(part: str) -> str:
209
+ """Resolve one style token to an SGR parameter: a brand color name resolves to a truecolor
210
+ or 256-color foreground for the active theme; any other code (e.g. ``"1"``) passes through."""
211
+ color = _PALETTE.get(part)
212
+ if color is None:
213
+ return part
214
+ hex6, fallback = color[_theme()]
215
+ if _truecolor():
216
+ return f"38;2;{int(hex6[0:2], 16)};{int(hex6[2:4], 16)};{int(hex6[4:6], 16)}"
217
+ return f"38;5;{fallback}"
218
+
219
+
220
+ def _paint(text: str, *codes: str) -> str:
221
+ """Apply one or more style tokens (raw SGR codes and/or brand color names), honoring the gate."""
222
+ if not codes or not _color():
223
+ return text
224
+ return f"\x1b[{';'.join(_sgr(c) for c in codes)}m{text}\x1b[0m"
225
+
226
+
227
+ # state -> (color, unicode dot, ascii dot) for status badges
228
+ _STATE_STYLE: dict[str, tuple[str, str, str]] = {
229
+ "queued": (_GRAY, "○", "o"),
230
+ "provisioning": (_TEAL, "◐", "o"),
231
+ "running": (_ACCENT, "●", "*"),
232
+ "done": (_GREEN, "●", "*"),
233
+ "deployed": (_VIOLET, "●", "*"),
234
+ "ready": (_GREEN, "●", "*"),
235
+ "failed": (_RED, "●", "*"),
236
+ "error": (_RED, "●", "*"),
237
+ "cancelled": (_GRAY, "●", "*"),
238
+ "canceled": (_GRAY, "●", "*"),
239
+ "dry_run": (_ACCENT2, "○", "o"),
240
+ }
241
+
242
+
243
+ def _term_width(cap: int = 80) -> int:
244
+ import shutil
245
+
246
+ cols = shutil.get_terminal_size((80, 24)).columns
247
+ return max(24, min(cols, cap))
248
+
249
+
250
+ def _rule(width: int | None = None) -> str:
251
+ return _paint(_glyph("─", "-") * (width or _term_width()), _FAINT)
252
+
253
+
254
+ def header(cmd: str, desc: str | None = None) -> str:
255
+ """Brand header line + a faint rule: the wordmark, the command, and an optional descriptor."""
256
+ mark = _paint("flash", _ACCENT, "1")
257
+ sep = _paint(_glyph("›", ">"), _FAINT) # noqa: RUF001 (the glyph is the point)
258
+ line = f"{mark} {sep} {_bold(cmd)}"
259
+ if desc:
260
+ line += " " + _paint(desc, _GRAY)
261
+ return _safe(f"{line}\n{_rule()}")
262
+
263
+
264
+ def badge(state: str) -> str:
265
+ """A colored status dot + label, e.g. a green ``● done``."""
266
+ color, uni, ascii_dot = _STATE_STYLE.get((state or "").lower(), (_GRAY, "•", "-"))
267
+ return _paint(f"{_glyph(uni, ascii_dot)} {state}", color)
268
+
269
+
270
+ def ok(msg: str) -> str:
271
+ return _safe(f"{_paint(_glyph('✓', 'ok:'), _GREEN, '1')} {msg}")
272
+
273
+
274
+ def arrow(msg: str) -> str:
275
+ """A de-emphasized pointer / next-step line."""
276
+ return _safe(f"{_paint(_glyph('→', '->'), _ACCENT2)} {_dim(msg)}")
277
+
278
+
279
+ def money(value: float, decimals: int = 4) -> str:
280
+ return _paint(f"${value:.{decimals}f}", _TEAL)
281
+
282
+
283
+ def _kv(pairs: list[tuple[str, str | None]], indent: int = 2) -> str:
284
+ """Aligned ``key · value`` panel; keys dimmed and padded to a common width."""
285
+ rows = [(k, v) for k, v in pairs if v is not None]
286
+ if not rows:
287
+ return ""
288
+ keyw = max(len(k) for k, _ in rows)
289
+ pad = " " * indent
290
+ sep = _paint(_glyph("·", "-"), _FAINT)
291
+ return "\n".join(f"{pad}{_paint(k.ljust(keyw), _GRAY)} {sep} {v}" for k, v in rows)
292
+
293
+
294
+ def _table(headers: list[str], rows: list[list], aligns: list[str] | None = None) -> str:
295
+ """Aligned table with a dim header and faint underline. Each cell is a plain string or a
296
+ ``(text, *sgr_codes)`` tuple; widths come from the plain text so color never skews them."""
297
+ aligns = aligns or ["l"] * len(headers)
298
+ cols = len(headers)
299
+
300
+ def plain(cell) -> str:
301
+ return cell[0] if isinstance(cell, tuple) else str(cell)
302
+
303
+ widths = [len(h) for h in headers]
304
+ for row in rows:
305
+ for i in range(cols):
306
+ widths[i] = max(widths[i], len(plain(row[i])))
307
+
308
+ def fmt(cell, w: int, align: str) -> str:
309
+ text = plain(cell)
310
+ padded = f"{text:>{w}}" if align == "r" else f"{text:<{w}}"
311
+ if isinstance(cell, tuple) and len(cell) > 1:
312
+ return _paint(padded, *cell[1:])
313
+ return padded
314
+
315
+ head = " ".join(
316
+ _paint(f"{h:>{widths[i]}}" if aligns[i] == "r" else f"{h:<{widths[i]}}", _GRAY, "1")
317
+ for i, h in enumerate(headers)
318
+ )
319
+ underline = _paint(" ".join(_glyph("─", "-") * w for w in widths), _FAINT)
320
+ # rstrip each row so an uncolored trailing column leaves no dangling whitespace.
321
+ body = [
322
+ " ".join(fmt(row[i], widths[i], aligns[i]) for i in range(cols)).rstrip() for row in rows
323
+ ]
324
+ return _safe("\n".join([head.rstrip(), underline, *body]))
325
+
326
+
327
+ def _json(obj) -> str:
328
+ """Pretty JSON: plain ``json.dumps(indent=2)`` when color is off (byte-identical to the
329
+ legacy output), syntax-highlighted when it's on. No data is ever dropped."""
330
+ import json
331
+
332
+ if not _color():
333
+ return json.dumps(obj, indent=2)
334
+ return _safe(_color_json(obj, 0))
335
+
336
+
337
+ def _color_json(obj, depth: int) -> str:
338
+ import json
339
+
340
+ pad_in = " " * (depth + 1)
341
+ pad_out = " " * depth
342
+ colon = _paint(":", _FAINT)
343
+ if isinstance(obj, dict):
344
+ if not obj:
345
+ return "{}"
346
+ items = [
347
+ f"{pad_in}{_paint(json.dumps(k), _ACCENT2)}{colon} {_color_json(v, depth + 1)}"
348
+ for k, v in obj.items()
349
+ ]
350
+ return "{\n" + ",\n".join(items) + f"\n{pad_out}}}"
351
+ if isinstance(obj, list):
352
+ if not obj:
353
+ return "[]"
354
+ items = [f"{pad_in}{_color_json(v, depth + 1)}" for v in obj]
355
+ return "[\n" + ",\n".join(items) + f"\n{pad_out}]"
356
+ if obj is None:
357
+ return _paint("null", _VIOLET)
358
+ if isinstance(obj, bool):
359
+ return _paint("true" if obj else "false", _VIOLET)
360
+ if isinstance(obj, (int, float)):
361
+ return _paint(json.dumps(obj), _TEAL)
362
+ return _paint(json.dumps(obj), _GREEN)
363
+
364
+
365
+ # ---------------------------------------------------------------------------
366
+ # Per-command renderers (themed view only; the command supplies the data).
367
+ # ---------------------------------------------------------------------------
368
+
369
+
370
+ def version(value: str) -> str:
371
+ """The wordmark + version."""
372
+ mark = _paint("flash", _ACCENT, "1")
373
+ return _safe(f"{mark} {_dim('v' + value)}")
374
+
375
+
376
+ def submitted(run_id: str) -> str:
377
+ """The `flash train` hand-off note (printed to stderr before logs start streaming)."""
378
+ head = ok(f"run {_paint(run_id, _ACCENT2)} submitted")
379
+ hint = _dim(f"following logs — Ctrl-C detaches; resume with `flash status {run_id} --follow`")
380
+ return _safe(f"{head}\n{hint}")
381
+
382
+
383
+ def models_table(rows: list[dict]) -> str:
384
+ """Supported base models — a clean themed list of ids (the CLI lists ids only)."""
385
+ dot = _glyph("•", "-")
386
+ ids = "\n".join(f" {_paint(dot, _FAINT)} {_paint(r['id'], _ACCENT2)}" for r in rows)
387
+ foot = arrow("train one with: flash train configs/rl.toml")
388
+ return _safe(f"{header('models', 'supported base models')}\n{ids}\n\n{foot}")
389
+
390
+
391
+ def gpus_table(rows: list[tuple[str, int, float | None]], tip: str) -> str:
392
+ """GPU classes: (name, vram_gb, $/hr or None)."""
393
+ body = []
394
+ for name, vram, rate in rows:
395
+ rate_cell = (f"${rate:.2f}", _TEAL) if rate else ("-", _FAINT)
396
+ body.append([(name, _ACCENT2), (f"{vram} GB", _GRAY), rate_cell])
397
+ table = _table(["GPU", "VRAM", "$/HR"], body, aligns=["l", "r", "r"])
398
+ return _safe(f"{header('gpus', 'managed GPU classes')}\n{table}\n\n{_dim(tip)}")
399
+
400
+
401
+ def runs_table(runs: list[dict]) -> str:
402
+ """Runs list: state badges + cost, newest first."""
403
+ body = []
404
+ for r in sorted(runs, key=lambda r: r.get("updated_at", 0), reverse=True):
405
+ spec = r.get("spec") or {}
406
+ model = spec.get("model", "")
407
+ algorithm = str(spec.get("algorithm") or "-").upper()
408
+ remote = r.get("remote") or {}
409
+ provider = remote.get("provider") or (
410
+ "runpod" if remote else (spec.get("gpu") or {}).get("provider", "")
411
+ )
412
+ gpu = remote.get("gpu") or (spec.get("gpu") or {}).get("type", "")
413
+ where = f"{gpu}@{provider}" if provider else gpu
414
+ color, uni, ascii_dot = _STATE_STYLE.get(str(r.get("state", "")).lower(), (_GRAY, "•", "-"))
415
+ body.append(
416
+ [
417
+ (r["run_id"], _ACCENT2),
418
+ (f"{_glyph(uni, ascii_dot)} {r.get('state', '')}", color),
419
+ (algorithm, _GRAY),
420
+ (f"${r.get('cost_usd', 0.0):.4f}", _TEAL),
421
+ (where, _GRAY),
422
+ model,
423
+ ]
424
+ )
425
+ table = _table(
426
+ ["RUN ID", "STATE", "ALGO", "COST", "GPU", "MODEL"],
427
+ body,
428
+ aligns=["l", "l", "l", "r", "l", "l"],
429
+ )
430
+ return _safe(f"{header('runs', f'{len(runs)} run(s)')}\n{table}")
431
+
432
+
433
+ def deployments_table(rows: list[dict]) -> str:
434
+ body = []
435
+ for r in rows:
436
+ d = r.get("deployment") or {}
437
+ body.append(
438
+ [
439
+ (r["run_id"], _ACCENT2),
440
+ (d.get("gpu", "?"), _GRAY),
441
+ (d.get("endpoint_name", ""), _GREEN),
442
+ ]
443
+ )
444
+ table = _table(["RUN ID", "GPU", "ENDPOINT"], body)
445
+ return _safe(f"{header('deployments', f'{len(rows)} active')}\n{table}")
446
+
447
+
448
+ def empty(cmd: str, desc: str, message: str) -> str:
449
+ """A styled empty state (e.g. no runs yet)."""
450
+ return _safe(f"{header(cmd, desc)}\n{_dim(' ' + message)}")
451
+
452
+
453
+ def _humanize_ts(value) -> str | None:
454
+ """Format an epoch seconds value as a compact UTC timestamp, leaving non-numbers alone."""
455
+ if not isinstance(value, (int, float)) or value <= 0:
456
+ return None
457
+ import datetime
458
+
459
+ return datetime.datetime.fromtimestamp(value, datetime.UTC).strftime("%Y-%m-%d %H:%M UTC")
460
+
461
+
462
+ def run_status(obj: dict) -> str:
463
+ """A curated status panel for `flash status`, with the full JSON below for completeness."""
464
+ spec = obj.get("spec") or {}
465
+ remote = obj.get("remote") or {}
466
+ gpu = remote.get("gpu") or (spec.get("gpu") or {}).get("type")
467
+ provider = remote.get("provider")
468
+ where = f"{gpu} @ {provider}" if gpu and provider else gpu
469
+ pairs = [
470
+ ("run id", _paint(obj.get("run_id", ""), _ACCENT2)),
471
+ ("model", spec.get("model")),
472
+ ("algorithm", (spec.get("algorithm") or "").upper() or None),
473
+ ("gpu", where),
474
+ ("cost", money(obj.get("cost_usd", 0.0))),
475
+ ]
476
+ realized = obj.get("realized_cost_usd")
477
+ if realized is not None:
478
+ pairs.append(("realized", money(realized)))
479
+ pairs += [
480
+ ("created", _humanize_ts(obj.get("created_at"))),
481
+ ("updated", _humanize_ts(obj.get("updated_at"))),
482
+ ]
483
+ if obj.get("artifacts_dir"):
484
+ pairs.append(("artifacts", obj["artifacts_dir"]))
485
+ if obj.get("error"):
486
+ pairs.append(("error", _paint(str(obj["error"]), _RED)))
487
+ head = f"{header('status')}\n {badge(obj.get('state', 'unknown'))}\n\n{_kv(pairs)}"
488
+ raw = f"{_dim('details')}\n{_json(obj)}"
489
+ return _safe(f"{head}\n\n{raw}")
490
+
491
+
492
+ def object_panel(cmd: str, obj: dict, desc: str | None = None) -> str:
493
+ """Header (+ state badge when present) over syntax-highlighted JSON. Lossless."""
494
+ parts = [header(cmd, desc)]
495
+ if isinstance(obj, dict) and obj.get("state"):
496
+ rid = obj.get("run_id")
497
+ line = " " + badge(obj["state"])
498
+ if rid:
499
+ line += " " + _paint(rid, _ACCENT2)
500
+ parts.append(line + "\n")
501
+ parts.append(_json(obj))
502
+ return _safe("\n".join(parts))
503
+
504
+
505
+ def cost_panel(est) -> str:
506
+ """Pre-flight cost estimate (the themed twin of CostEstimate.breakdown())."""
507
+ setup_extra = " + vLLM init" if est.method == "grpo" else ""
508
+ pairs = [
509
+ (
510
+ "run",
511
+ f"{_paint(est.model_id, _ACCENT2)} {_dim(f'[{est.method.upper()}, {est.steps} steps]')}",
512
+ ),
513
+ (
514
+ "gpu",
515
+ f"{est.gpu} on {est.provider} "
516
+ f"{_dim(f'({est.gpu_vram_gb} GB; needs >= {est.required_vram_gb} GB)')} "
517
+ f"@ {money(est.gpu_hourly_usd, 2)}/hr",
518
+ ),
519
+ (
520
+ "setup",
521
+ f"{est.setup_seconds / 60:.1f} min {_dim(f'(cold start: boot + deps + model load{setup_extra})')}",
522
+ ),
523
+ ("per step", f"{est.seconds_per_step:.2f} s"),
524
+ (
525
+ "train",
526
+ f"{est.train_seconds / 60:.1f} min"
527
+ + (_paint(" [capped at wall-clock limit]", _RED) if est.wall_capped else ""),
528
+ ),
529
+ ("wall clock", f"{est.wall_clock_hours:.2f} h"),
530
+ ]
531
+ panel = _kv(pairs)
532
+ total = f" {_paint('TOTAL'.ljust(10), _GRAY, '1')} {_paint(_glyph('·', '-'), _FAINT)} {_paint(f'${est.total_usd:.2f}', _TEAL, '1')}"
533
+ out = f"{header('train', 'pre-flight cost estimate')}\n{panel}\n{_rule()}\n{total}"
534
+ if est.notes:
535
+ notes = "\n".join(f" {_paint(_glyph('·', '-'), _FAINT)} {_dim(n)}" for n in est.notes)
536
+ out += f"\n\n{_dim('notes')}\n{notes}"
537
+ return _safe(out)
538
+
539
+
540
+ def env_setup(paths: list[str]) -> str:
541
+ """Confirmation + file tree for `flash env setup`."""
542
+ labels = {
543
+ "environment.py": "env entrypoint — edit the reward + prompt",
544
+ "datasets/train.jsonl": "starter training rows",
545
+ "configs/rl.toml": "GRPO run config",
546
+ "configs/sft.toml": "SFT run config",
547
+ "TRAINING.md": "how to train well — read this first",
548
+ }
549
+ keyw = max(len(p) for p in paths)
550
+ tree = "\n".join(
551
+ f" {_paint(p.ljust(keyw), _ACCENT2)} {_dim(labels.get(p, ''))}" for p in paths
552
+ )
553
+ head = f"{header('env setup', 'starter Freesolo environment')}\n{ok('scaffold ready')}\n"
554
+ nxt = arrow("publish it: flash env push --name my-env .")
555
+ return _safe(f"{head}\n{tree}\n\n{nxt}")
556
+
557
+
558
+ def env_list(installed: list[str], local: list[str]) -> str:
559
+ parts = [header("env list", "installed + local environments")]
560
+ if installed:
561
+ parts.append(_paint("installed", _GRAY, "1"))
562
+ parts.extend(
563
+ f" {_paint(_glyph('·', '-'), _FAINT)} {_paint(e, _ACCENT2)}" for e in installed
564
+ )
565
+ if local:
566
+ if installed:
567
+ parts.append("")
568
+ parts.append(
569
+ _paint("local sources", _GRAY, "1")
570
+ + _dim(" (publish with flash env push --name <name> <path>)")
571
+ )
572
+ parts.extend(f" {_paint(_glyph('·', '-'), _FAINT)} {_paint(p, _ACCENT2)}" for p in local)
573
+ if not installed and not local:
574
+ parts.append(_dim(" no environments yet — scaffold one with `flash env setup`"))
575
+ return _safe("\n".join(parts))
576
+
577
+
578
+ def env_installed(env_id: str, manifest: str) -> str:
579
+ snippet = f'[environment]\nid = "{env_id}"'
580
+ body = "\n".join(f" {_paint(line, _ACCENT2)}" for line in snippet.splitlines())
581
+ return _safe(
582
+ f"{ok(f'recorded {_bold(env_id)}')}\n"
583
+ f"{_dim(f' manifest: {manifest}')}\n\n"
584
+ f"{_dim('use it in your config:')}\n{body}"
585
+ )
586
+
587
+
588
+ def chat_label() -> str:
589
+ """A faint speaker label printed above a styled chat reply (the reply itself stays plain
590
+ text so a piped transcript is unchanged)."""
591
+ return _paint("assistant", _ACCENT2, "1")
592
+
593
+
594
+ def env_published(slug: str) -> str:
595
+ snippet = f'[environment]\nid = "{slug}"'
596
+ body = "\n".join(f" {_paint(line, _ACCENT2)}" for line in snippet.splitlines())
597
+ return _safe(
598
+ f"{ok(f'published {_bold(slug)}')}\n\n{_dim('reference it in your config:')}\n{body}"
599
+ )