smartpipe-cli 1.3.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 (126) hide show
  1. smartpipe/__init__.py +6 -0
  2. smartpipe/__main__.py +8 -0
  3. smartpipe/assets/probe.png +0 -0
  4. smartpipe/assets/probe.txt +1 -0
  5. smartpipe/assets/probe.wav +0 -0
  6. smartpipe/cli/__init__.py +5 -0
  7. smartpipe/cli/auth_cmd.py +78 -0
  8. smartpipe/cli/cache_cmd.py +60 -0
  9. smartpipe/cli/chart_cmd.py +60 -0
  10. smartpipe/cli/cite_cmd.py +26 -0
  11. smartpipe/cli/cluster_cmd.py +102 -0
  12. smartpipe/cli/completions.py +91 -0
  13. smartpipe/cli/config_cmd.py +234 -0
  14. smartpipe/cli/diff_cmd.py +100 -0
  15. smartpipe/cli/distinct_cmd.py +94 -0
  16. smartpipe/cli/doctor_cmd.py +207 -0
  17. smartpipe/cli/echo_cmd.py +44 -0
  18. smartpipe/cli/embed_cmd.py +80 -0
  19. smartpipe/cli/extend_cmd.py +138 -0
  20. smartpipe/cli/filter_cmd.py +87 -0
  21. smartpipe/cli/getschema_cmd.py +31 -0
  22. smartpipe/cli/input_options.py +113 -0
  23. smartpipe/cli/interrupts.py +92 -0
  24. smartpipe/cli/join_cmd.py +162 -0
  25. smartpipe/cli/map_cmd.py +150 -0
  26. smartpipe/cli/outliers_cmd.py +82 -0
  27. smartpipe/cli/probe_cmd.py +223 -0
  28. smartpipe/cli/reduce_cmd.py +129 -0
  29. smartpipe/cli/root.py +281 -0
  30. smartpipe/cli/run_cmd.py +136 -0
  31. smartpipe/cli/sample_cmd.py +35 -0
  32. smartpipe/cli/schema_cmd.py +75 -0
  33. smartpipe/cli/screens.py +231 -0
  34. smartpipe/cli/sem_file.py +453 -0
  35. smartpipe/cli/sort_cmd.py +33 -0
  36. smartpipe/cli/split_cmd.py +76 -0
  37. smartpipe/cli/summarize_cmd.py +37 -0
  38. smartpipe/cli/top_k_cmd.py +97 -0
  39. smartpipe/cli/usage_cmd.py +66 -0
  40. smartpipe/cli/where_cmd.py +36 -0
  41. smartpipe/config/__init__.py +5 -0
  42. smartpipe/config/credentials.py +118 -0
  43. smartpipe/config/display.py +70 -0
  44. smartpipe/config/doctor.py +58 -0
  45. smartpipe/config/paths.py +38 -0
  46. smartpipe/config/store.py +252 -0
  47. smartpipe/container.py +439 -0
  48. smartpipe/core/__init__.py +5 -0
  49. smartpipe/core/errors.py +57 -0
  50. smartpipe/core/jsontools.py +56 -0
  51. smartpipe/engine/__init__.py +9 -0
  52. smartpipe/engine/aggregate.py +234 -0
  53. smartpipe/engine/blocking.py +44 -0
  54. smartpipe/engine/chart.py +143 -0
  55. smartpipe/engine/chunking.py +161 -0
  56. smartpipe/engine/clustering.py +94 -0
  57. smartpipe/engine/predicate.py +330 -0
  58. smartpipe/engine/prompts.py +601 -0
  59. smartpipe/engine/ranking.py +62 -0
  60. smartpipe/engine/runner.py +175 -0
  61. smartpipe/engine/schema.py +208 -0
  62. smartpipe/engine/schema_dsl.py +144 -0
  63. smartpipe/engine/tally.py +53 -0
  64. smartpipe/engine/timebin.py +67 -0
  65. smartpipe/engine/units.py +43 -0
  66. smartpipe/engine/windows.py +78 -0
  67. smartpipe/io/__init__.py +9 -0
  68. smartpipe/io/diagnostics.py +148 -0
  69. smartpipe/io/inputs.py +44 -0
  70. smartpipe/io/items.py +149 -0
  71. smartpipe/io/leaderboard.py +52 -0
  72. smartpipe/io/metering.py +180 -0
  73. smartpipe/io/progress.py +140 -0
  74. smartpipe/io/readers.py +455 -0
  75. smartpipe/io/text.py +40 -0
  76. smartpipe/io/tty.py +88 -0
  77. smartpipe/io/usage.py +214 -0
  78. smartpipe/io/writers.py +340 -0
  79. smartpipe/models/__init__.py +5 -0
  80. smartpipe/models/anthropic_adapter.py +149 -0
  81. smartpipe/models/base.py +170 -0
  82. smartpipe/models/budget.py +94 -0
  83. smartpipe/models/cache.py +132 -0
  84. smartpipe/models/gemini_native.py +196 -0
  85. smartpipe/models/http_support.py +77 -0
  86. smartpipe/models/jina.py +98 -0
  87. smartpipe/models/local_embed.py +76 -0
  88. smartpipe/models/ollama.py +204 -0
  89. smartpipe/models/openai_codex.py +237 -0
  90. smartpipe/models/openai_compat.py +328 -0
  91. smartpipe/models/openai_oauth.py +366 -0
  92. smartpipe/models/resolve.py +78 -0
  93. smartpipe/models/retry.py +69 -0
  94. smartpipe/models/stt.py +80 -0
  95. smartpipe/models/windows.py +116 -0
  96. smartpipe/parsing/__init__.py +10 -0
  97. smartpipe/parsing/detect.py +178 -0
  98. smartpipe/parsing/extract.py +582 -0
  99. smartpipe/py.typed +0 -0
  100. smartpipe/verbs/__init__.py +5 -0
  101. smartpipe/verbs/chart.py +153 -0
  102. smartpipe/verbs/cluster.py +220 -0
  103. smartpipe/verbs/common.py +468 -0
  104. smartpipe/verbs/convert.py +251 -0
  105. smartpipe/verbs/diff.py +206 -0
  106. smartpipe/verbs/distinct.py +164 -0
  107. smartpipe/verbs/embed.py +166 -0
  108. smartpipe/verbs/extend.py +180 -0
  109. smartpipe/verbs/filter.py +191 -0
  110. smartpipe/verbs/getschema.py +135 -0
  111. smartpipe/verbs/join.py +413 -0
  112. smartpipe/verbs/map.py +315 -0
  113. smartpipe/verbs/outliers.py +119 -0
  114. smartpipe/verbs/reduce.py +428 -0
  115. smartpipe/verbs/sample.py +52 -0
  116. smartpipe/verbs/sortverb.py +63 -0
  117. smartpipe/verbs/split.py +333 -0
  118. smartpipe/verbs/summarize.py +60 -0
  119. smartpipe/verbs/top_k.py +318 -0
  120. smartpipe/verbs/where.py +47 -0
  121. smartpipe_cli-1.3.0.dist-info/METADATA +192 -0
  122. smartpipe_cli-1.3.0.dist-info/RECORD +126 -0
  123. smartpipe_cli-1.3.0.dist-info/WHEEL +4 -0
  124. smartpipe_cli-1.3.0.dist-info/entry_points.txt +3 -0
  125. smartpipe_cli-1.3.0.dist-info/licenses/LICENSE +201 -0
  126. smartpipe_cli-1.3.0.dist-info/licenses/NOTICE +5 -0
@@ -0,0 +1,82 @@
1
+ """``smartpipe outliers`` — the items least like the rest."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import sys
8
+
9
+ import click
10
+
11
+ from smartpipe.cli.completions import complete_embed_models
12
+ from smartpipe.cli.input_options import input_options, input_spec
13
+ from smartpipe.cli.interrupts import graceful_interrupts, settle_budget
14
+ from smartpipe.core.errors import ExitCode
15
+ from smartpipe.verbs.outliers import OutliersRequest, run_outliers
16
+
17
+ __all__ = ["outliers_command"]
18
+
19
+
20
+ @click.command(name="outliers")
21
+ @click.argument("count", type=int, default=5)
22
+ @click.option(
23
+ "--embed-model",
24
+ "model_flag",
25
+ shell_complete=complete_embed_models,
26
+ help="Embedding model for this run.",
27
+ )
28
+ @click.option("--concurrency", "concurrency_flag", type=int, help="Max parallel model calls.")
29
+ @click.option("--max-calls", "max_calls", type=int, help="Stop after N model calls (cost cap).")
30
+ @click.option(
31
+ "--allow-captions",
32
+ "allow_captions",
33
+ is_flag=True,
34
+ help="Let a CLOUD model convert images/audio to text (paid; local models do it free).",
35
+ )
36
+ @input_options
37
+ def outliers_command(
38
+ count: int,
39
+ model_flag: str | None,
40
+ concurrency_flag: int | None,
41
+ max_calls: int | None,
42
+ allow_captions: bool,
43
+ in_patterns: tuple[str, ...],
44
+ from_files: bool,
45
+ ) -> None:
46
+ """Rank the N items least like the rest — novelty, surfaced.
47
+
48
+ \b
49
+ Examples:
50
+ cat today.log | smartpipe outliers 5 # the failure shapes you HAVEN'T seen
51
+ cat train.jsonl | smartpipe outliers 20 # label noise and template glitches
52
+
53
+ top_k's mirror: farthest from everything instead of nearest to a query.
54
+ Embeddings only — no chat calls. Each row carries _distance, and the
55
+ stderr line anchors it ("median neighbor distance 0.21 — these are
56
+ 3.1x-3.9x out") so the score means something.
57
+ """
58
+ request = OutliersRequest(
59
+ count=count,
60
+ model_flag=model_flag,
61
+ concurrency_flag=concurrency_flag,
62
+ allow_captions=allow_captions,
63
+ input=input_spec(in_patterns, from_files=from_files),
64
+ )
65
+ code = asyncio.run(_run(request, max_calls))
66
+ if code is not ExitCode.OK:
67
+ raise SystemExit(int(code))
68
+
69
+
70
+ async def _run(request: OutliersRequest, max_calls: int | None) -> ExitCode:
71
+ from dataclasses import replace
72
+
73
+ from smartpipe.container import build_container
74
+
75
+ async with (
76
+ graceful_interrupts() as stop,
77
+ build_container(os.environ, max_calls=max_calls, stop=stop) as container,
78
+ ):
79
+ if not request.allow_captions and container.config.allow_captions:
80
+ request = replace(request, allow_captions=True) # profile consent (D35)
81
+ code = await run_outliers(request, container, stdin=sys.stdin, stdout=sys.stdout, stop=stop)
82
+ return settle_budget(container.budget, code)
@@ -0,0 +1,223 @@
1
+ """``doctor --probe`` (D31/D42): the modality matrix, with real tiny calls.
2
+
3
+ ``doctor`` alone never spends a cent (D18); this flag is the explicit opt-in
4
+ that answers what the docs can only claim: which modalities *actually* reach
5
+ your configured models. Marks: check = native; dash+star = works via a
6
+ footnote names it); cross = no path. Four tiny paid calls, announced first.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from importlib import resources
13
+ from typing import TYPE_CHECKING
14
+
15
+ from smartpipe.core.errors import SempipeError
16
+ from smartpipe.io import diagnostics
17
+ from smartpipe.models.base import (
18
+ AudioData,
19
+ CompletionRequest,
20
+ ImageData,
21
+ supports_media_embedding,
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Mapping
26
+
27
+ from smartpipe.models.base import ChatModel, EmbeddingModel
28
+
29
+ __all__ = ["render_matrix", "run_probe"]
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class Cell:
34
+ verdict: str # "ok" | "no" | "via" (fallback) | "na"
35
+ detail: str
36
+ footnote: str | None = None # what the * means, when verdict == "via"
37
+
38
+
39
+ def _asset(name: str) -> bytes:
40
+ return (resources.files("smartpipe.assets") / name).read_bytes()
41
+
42
+
43
+ async def run_probe(env: Mapping[str, str]) -> str:
44
+ """Build the matrix against the configured chat + embed models."""
45
+ import os
46
+
47
+ from smartpipe.container import build_container
48
+
49
+ del env # the container reads the process environment itself
50
+ async with build_container(os.environ) as container:
51
+ chat = await container.chat_model()
52
+ embed = await container.embedding_model()
53
+ diagnostics.note(
54
+ "probing modalities with 4 tiny calls "
55
+ f"(chat: {chat.ref.name} · embed: {embed.ref.name})"
56
+ )
57
+ stt = _stt_path(os.environ, container.config.stt_model)
58
+ chat_image = await _chat_image(chat)
59
+ chat_audio = await _chat_audio(chat, stt)
60
+ rows = {
61
+ "text": (await _chat_text(chat), await _embed_text(embed)),
62
+ "image": (chat_image, _embed_image(embed, chat_image)),
63
+ "audio": (chat_audio, _embed_audio(chat_audio, stt)),
64
+ "video": (_chat_video(chat), _embed_video()),
65
+ "document": (
66
+ Cell("ok", "parsed locally (no call)"),
67
+ Cell("ok", "as extracted text"),
68
+ ),
69
+ }
70
+ return render_matrix(rows)
71
+
72
+
73
+ def _stt_path(env: Mapping[str, str], configured: str | None) -> str | None:
74
+ """The transcription path the ladder would take, if any (D39/05)."""
75
+ named = env.get("SMARTPIPE_STT_MODEL", "").strip() or (configured or "")
76
+ if named:
77
+ return named
78
+ if env.get("OPENAI_API_KEY", "").strip():
79
+ return "openai/whisper-1 (auto)"
80
+ from importlib.util import find_spec
81
+
82
+ if find_spec("faster_whisper") is not None:
83
+ return "local whisper"
84
+ return None
85
+
86
+
87
+ async def _chat_text(chat: ChatModel) -> Cell:
88
+ try:
89
+ reply = await chat.complete(
90
+ CompletionRequest(system=None, user="Reply with exactly: OK", max_tokens=8)
91
+ )
92
+ return Cell("ok", f"replied {reply.strip()[:16]!r}")
93
+ except SempipeError as exc:
94
+ return Cell("no", _first_line(exc))
95
+
96
+
97
+ async def _chat_image(chat: ChatModel) -> Cell:
98
+ try:
99
+ reply = await chat.complete(
100
+ CompletionRequest(
101
+ system=None,
102
+ user="One word: what color dominates this image?",
103
+ media=(ImageData(_asset("probe.png"), "image/png"),),
104
+ max_tokens=8,
105
+ )
106
+ )
107
+ return Cell("ok", f"saw it — {reply.strip()[:16]!r}")
108
+ except SempipeError:
109
+ return Cell("no", "this model can't see images")
110
+
111
+
112
+ async def _chat_audio(chat: ChatModel, stt: str | None) -> Cell:
113
+ try:
114
+ reply = await chat.complete(
115
+ CompletionRequest(
116
+ system=None,
117
+ user="One word: is this sound a tone or speech?",
118
+ media=(AudioData(_asset("probe.wav"), "audio/wav"),),
119
+ max_tokens=8,
120
+ )
121
+ )
122
+ return Cell("ok", f"heard it — {reply.strip()[:16]!r}")
123
+ except SempipeError:
124
+ if stt is not None:
125
+ return Cell("via", "transcribed, then chat", footnote=f"audio → {stt}")
126
+ return Cell("no", "no transcription path — reinstall smartpipe")
127
+
128
+
129
+ async def _embed_text(embed: EmbeddingModel) -> Cell:
130
+ try:
131
+ vectors = await embed.embed([_asset("probe.txt").decode("utf-8").strip()])
132
+ return Cell("ok", f"{len(vectors[0])}-dim vector")
133
+ except SempipeError as exc:
134
+ return Cell("no", _first_line(exc))
135
+
136
+
137
+ def _embed_image(embed: EmbeddingModel, chat_image: Cell) -> Cell:
138
+ if supports_media_embedding(embed):
139
+ return Cell("ok", "embedded as pixels")
140
+ if chat_image.verdict == "ok":
141
+ return Cell("via", "caption, then embed", footnote="image → caption pivot (D33)")
142
+ return Cell("no", "needs a vision chat model to caption")
143
+
144
+
145
+ def _embed_audio(chat_audio: Cell, stt: str | None) -> Cell:
146
+ if chat_audio.verdict == "ok" or stt is not None:
147
+ return Cell("via", "transcript, then embed", footnote="audio → transcript pivot")
148
+ return Cell("no", "no transcription path")
149
+
150
+
151
+ def _chat_video(chat: ChatModel) -> Cell:
152
+ if chat.ref.provider == "gemini":
153
+ return Cell("ok", "watched natively")
154
+ try:
155
+ from smartpipe.parsing.extract import ffmpeg_exe
156
+
157
+ ffmpeg_exe()
158
+ return Cell("via", "frames + audio track", footnote="video → 1 fps frames + track")
159
+ except SempipeError:
160
+ return Cell("no", "ffmpeg unavailable — reinstall smartpipe")
161
+
162
+
163
+ def _embed_video() -> Cell:
164
+ return Cell("via", "halves, then embed", footnote="video → visual+speech halves (D36)")
165
+
166
+
167
+ def _first_line(exc: SempipeError) -> str:
168
+ return str(exc).splitlines()[0].removeprefix("error: ")
169
+
170
+
171
+ _MARKS = {"ok": "✓ ", "no": "✗ ", "via": "–*", "na": "– "} # noqa: RUF001 — pinned marks
172
+
173
+
174
+ def render_matrix(rows: Mapping[str, tuple[Cell, Cell]]) -> str:
175
+ """Aligned by VISIBLE width, cells truncated to the grid — overflow never
176
+ smashes columns (D42; live-caught by the owner's screenshot)."""
177
+ from smartpipe.cli.screens import bad, good, heading, tint
178
+
179
+ label_width = max(len(name) for name in rows) + 2
180
+ cap = 30
181
+
182
+ def cell_text(cell: Cell) -> str:
183
+ detail = cell.detail if len(cell.detail) <= cap else cell.detail[: cap - 1] + "…"
184
+ return f"{_MARKS[cell.verdict]} {detail}"
185
+
186
+ def paint(cell: Cell, text: str) -> str:
187
+ mark_len = 2
188
+ mark, rest = text[:mark_len], text[mark_len:]
189
+ match cell.verdict:
190
+ case "ok":
191
+ return good(mark) + rest
192
+ case "no":
193
+ return bad(mark) + rest
194
+ case _:
195
+ return tint(mark, "2") + rest
196
+
197
+ chat_width = max(len(cell_text(chat)) for chat, _embed in rows.values()) + 3
198
+ header = f" {' ' * label_width}{heading(_pad_plain('chat', chat_width))}{heading('embed')}"
199
+ lines = [header]
200
+ footnotes: list[str] = []
201
+ for modality, (chat_cell, embed_cell) in rows.items():
202
+ left = paint(chat_cell, cell_text(chat_cell))
203
+ right = paint(embed_cell, cell_text(embed_cell))
204
+ label = tint(modality.ljust(label_width), "2")
205
+ lines.append(f" {label}{_pad_ansi(left, chat_width)}{right}")
206
+ for cell in (chat_cell, embed_cell):
207
+ if cell.footnote and cell.footnote not in footnotes:
208
+ footnotes.append(cell.footnote)
209
+ if footnotes:
210
+ lines.append(tint(" * fallback paths: " + " · ".join(footnotes), "2"))
211
+ return "\n".join(lines)
212
+
213
+
214
+ def _pad_plain(text: str, width: int) -> str:
215
+ return f"{text:{width}s}"
216
+
217
+
218
+ def _pad_ansi(text: str, width: int) -> str:
219
+ """Pad by VISIBLE length — ANSI escapes are zero-width."""
220
+ import re
221
+
222
+ visible = len(re.sub(r"\x1b\[[0-9;]*m", "", text))
223
+ return text + " " * max(0, width - visible)
@@ -0,0 +1,129 @@
1
+ """``smartpipe reduce`` — synthesize many items into one."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import click
11
+
12
+ from smartpipe.cli.completions import complete_chat_models
13
+ from smartpipe.cli.input_options import (
14
+ fields_option,
15
+ input_options,
16
+ input_spec,
17
+ resolve_prompt,
18
+ )
19
+ from smartpipe.cli.interrupts import graceful_interrupts, settle_budget
20
+ from smartpipe.core.errors import ExitCode
21
+ from smartpipe.verbs.reduce import ReduceRequest, run_reduce
22
+
23
+ __all__ = ["reduce_command"]
24
+
25
+
26
+ @click.command(name="reduce")
27
+ @click.argument("prompt", required=False)
28
+ @click.option(
29
+ "--prompt-file",
30
+ "prompt_file",
31
+ type=click.Path(path_type=Path),
32
+ help="Read the prompt from a file (the @file shorthand does the same).",
33
+ )
34
+ @click.option(
35
+ "--schema-from",
36
+ "schema_dsl",
37
+ metavar="DSL",
38
+ help='Build the schema from a short DSL: "vendor string; total number >= 0".',
39
+ )
40
+ @click.option(
41
+ "--schema",
42
+ "schema_path",
43
+ type=click.Path(path_type=Path),
44
+ help="Shape the final result with a JSON Schema.",
45
+ )
46
+ @click.option("--group-by", "group_by", help="Reduce per group (by an input JSON field).")
47
+ @click.option(
48
+ "--model", "model_flag", shell_complete=complete_chat_models, help="Model for this run."
49
+ )
50
+ @click.option("--concurrency", "concurrency_flag", type=int, help="Max parallel model calls.")
51
+ @click.option("--max-calls", "max_calls", type=int, help="Stop after N model calls (cost cap).")
52
+ @click.option("--verbose", is_flag=True, help="Show the chunking tree on stderr.")
53
+ @click.option("--window", type=int, help="Stream mode: reduce every N lines (tumbling).")
54
+ @click.option("--every", type=int, help="With --window: slide, reducing after every M lines.")
55
+ @fields_option
56
+ @click.option(
57
+ "--allow-captions",
58
+ "allow_captions",
59
+ is_flag=True,
60
+ help="Let a CLOUD model convert images/audio to text (paid; local models do it free).",
61
+ )
62
+ @input_options
63
+ def reduce_command(
64
+ prompt: str | None,
65
+ prompt_file: Path | None,
66
+ schema_path: Path | None,
67
+ schema_dsl: str | None,
68
+ group_by: str | None,
69
+ model_flag: str | None,
70
+ concurrency_flag: int | None,
71
+ max_calls: int | None,
72
+ allow_captions: bool,
73
+ verbose: bool,
74
+ window: int | None,
75
+ every: int | None,
76
+ fields: tuple[str, ...] | None,
77
+ in_patterns: tuple[str, ...],
78
+ from_files: bool,
79
+ ) -> None:
80
+ """Synthesize all input items into a single result.
81
+
82
+ \b
83
+ Examples:
84
+ smartpipe reduce "Write a one-page executive summary" --in 'notes/*.md'
85
+ cat reports.jsonl | smartpipe reduce "Write a root-cause analysis" --schema rca.json
86
+ cat feedback.jsonl | smartpipe reduce "Summarize sentiment" --group-by product
87
+
88
+ When the input is too large for the model, smartpipe chunks it and recursively
89
+ summarizes — automatically. Add --verbose to see the chunking tree.
90
+ """
91
+ request = ReduceRequest(
92
+ allow_captions=allow_captions,
93
+ prompt=resolve_prompt(prompt, prompt_file),
94
+ schema_path=schema_path,
95
+ schema_dsl=schema_dsl,
96
+ group_by=group_by,
97
+ model_flag=model_flag,
98
+ concurrency_flag=concurrency_flag,
99
+ verbose=verbose,
100
+ window=window,
101
+ every=every,
102
+ input=input_spec(in_patterns, from_files=from_files),
103
+ fields=fields,
104
+ )
105
+ code = asyncio.run(_run(request, max_calls))
106
+ if code is not ExitCode.OK:
107
+ raise SystemExit(int(code))
108
+
109
+
110
+ async def _run(request: ReduceRequest, max_calls: int | None) -> ExitCode:
111
+ from smartpipe.container import build_container
112
+
113
+ if request.window is None: # whole-set mode: ^C exits immediately; budget is fatal (D18)
114
+ async with build_container(os.environ, max_calls=max_calls) as container:
115
+ if not request.allow_captions and container.config.allow_captions:
116
+ from dataclasses import replace as _replace
117
+
118
+ request = _replace(request, allow_captions=True) # profile consent (D35)
119
+ return await run_reduce(request, container, stdin=sys.stdin, stdout=sys.stdout)
120
+ async with ( # stream mode drains + flushes partial
121
+ graceful_interrupts() as stop,
122
+ build_container(os.environ, max_calls=max_calls, stop=stop) as container,
123
+ ):
124
+ if not request.allow_captions and container.config.allow_captions:
125
+ from dataclasses import replace as _replace
126
+
127
+ request = _replace(request, allow_captions=True) # profile consent (D35)
128
+ code = await run_reduce(request, container, stdin=sys.stdin, stdout=sys.stdout, stop=stop)
129
+ return settle_budget(container.budget, code)