Skip to content

Commit c824d73

Browse files
authored
Merge pull request #929 from Pipelex/release/v0.29.1
Release v0.29.1
2 parents 1a216ef + 7b3dd10 commit c824d73

17 files changed

Lines changed: 236 additions & 75 deletions

File tree

.badges/tests.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"schemaVersion": 1,
33
"label": "tests",
4-
"message": "6089",
4+
"message": "6095",
55
"color": "blue",
66
"cacheSeconds": 300
77
}

.pipelex/plxt.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# =============================================================================
2-
# Pipelex TOML Configuration for pipelex-demo
2+
# PLXT Configuration
33
# =============================================================================
44
# Configures MTHDS/TOML formatting and linting behaviour for this project.
55
# Powered by the Pipelex extension (plxt / taplo engine).

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [v0.29.1] - 2026-05-21
4+
5+
### Fixed
6+
7+
- **`pipelex run` now prints the aggregated cost table when `[pipelex.reporting_config].is_log_costs_to_console = true`, with `--cost-report/--no-cost-report` to override per invocation.** The `cost-tracking.md` and `reporting-config.md` docs both promised a summary cost table at the end of a CLI run, but `pipelex/cli/commands/run/_run_core.py` never called `get_report_delegate().generate_report()` — the flag only triggered `log.verbose(...)` lines per inference job, which the default `INFO` log level swallows, so the table never appeared. The CLI now calls `generate_report()` after a successful run when either `is_log_costs_to_console` or `is_generate_cost_report_file_enabled` is true, so the Rich table (one row per model, plus a totals row) prints right before the "✓ Pipeline execution completed successfully" recap — and the CSV export branch finally fires too. The new `--cost-report/--no-cost-report` tri-state flag (default unset → use config) lets you force the cost table on for a single invocation (`--cost-report`) or skip reporting entirely — no Rich table **and** no CSV file (`--no-cost-report`) — without touching `.pipelex/pipelex.toml`. Applies to `pipelex run bundle`, `pipelex run pipe`, and `pipelex run method`; works in dry-run mode as well (synthetic usage rows).
8+
39
## [v0.29.0] - 2026-05-20
410

511
### Added

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,10 +461,21 @@ The same `.mthds` file runs from multiple execution targets:
461461
|--------|-----|
462462
| **CLI** | `pipelex run bundle method.mthds --inputs inputs.json` |
463463
| **Python** | `PipelexRunner().execute_pipeline(...)` |
464+
| **TypeScript / Node** | [`mthds`](https://www.npmjs.com/package/mthds) SDK calling a Pipelex API server |
464465
| **REST API** | Self-hosted API server |
465466
| **MCP** | Model Context Protocol — agents call methods as tools |
466467
| **n8n** | Pipelex node for workflow automation |
467468

469+
## Use Pipelex from TypeScript
470+
471+
For Node, Next.js, or any TypeScript app, call a Pipelex API server via the [`mthds`](https://www.npmjs.com/package/mthds) npm SDK (source: [`mthds-js`](https://github.com/mthds-ai/mthds-js)). Self-host the open-source [`pipelex-api`](https://github.com/Pipelex/pipelex-api) and point the SDK at your instance. A Pipelex-hosted runner at `api.pipelex.com` is also available in private beta — [join the waitlist](https://go.pipelex.com/waitlist).
472+
473+
```bash
474+
npm install mthds
475+
```
476+
477+
Fastest way to get started: fork the [`pipelex-starter-js`](https://github.com/Pipelex/pipelex-starter-js) template — a Next.js 16 + TypeScript app with three working demos (text entity extraction, PDF summary, image generation). Click *Use this template* on GitHub.
478+
468479

469480
# The MTHDS Ecosystem
470481

pipelex/cli/commands/run/_run_core.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from pipelex.core.pipes.exceptions import PipeOperatorModelChoiceError
2323
from pipelex.core.stuffs.stuff_viewer import render_stuff_viewer
2424
from pipelex.graph.graph_factory import generate_graph_outputs, save_graph_outputs_to_dir
25-
from pipelex.hub import get_console, get_telemetry_manager
25+
from pipelex.hub import get_console, get_report_delegate, get_telemetry_manager
2626
from pipelex.pipe_operators.exceptions import PipeOperatorModelAvailabilityError
2727
from pipelex.pipe_run.pipe_run_mode import PipeRunMode
2828
from pipelex.pipelex import Pipelex
@@ -51,6 +51,7 @@ async def _execute_run(
5151
dry_run: bool,
5252
mock_inputs: bool,
5353
library_dir: list[str] | None,
54+
cost_report: bool | None,
5455
dynamic_output_concept_ref: str | None = None,
5556
) -> None:
5657
"""Core async execution logic for running a pipe.
@@ -212,6 +213,20 @@ async def _execute_run(
212213
save_as_json_to_path(object_to_save=working_memory_dict, path=working_memory_output_path)
213214
log.verbose(f"Working memory saved to: {working_memory_output_path}")
214215

216+
reporting_config = get_config().pipelex.reporting_config
217+
# --no-cost-report (cost_report is False) skips the report entirely: no table, no CSV.
218+
# Otherwise: console follows the flag (if given) or config; CSV follows config.
219+
if cost_report is not False:
220+
print_to_console = cost_report or reporting_config.is_log_costs_to_console
221+
if print_to_console or reporting_config.is_generate_cost_report_file_enabled:
222+
try:
223+
get_report_delegate().generate_report(
224+
pipeline_run_id=response.pipeline_run_id,
225+
print_to_console=print_to_console,
226+
)
227+
except (OSError, PipelexError) as cost_report_error:
228+
log.warning(f"Cost report generation failed (run succeeded): {cost_report_error}")
229+
215230
# Print completion recap
216231
console = get_console()
217232
if dry_run:
@@ -245,6 +260,7 @@ def execute_run(
245260
dry_run: bool,
246261
mock_inputs: bool,
247262
library_dir: list[str] | None,
263+
cost_report: bool | None = None,
248264
telemetry_command_label: str = COMMAND,
249265
temporal: bool | None = None,
250266
dynamic_output_concept_ref: str | None = None,
@@ -275,6 +291,7 @@ def execute_run(
275291
dry_run=dry_run,
276292
mock_inputs=mock_inputs,
277293
library_dir=library_dir,
294+
cost_report=cost_report,
278295
dynamic_output_concept_ref=dynamic_output_concept_ref,
279296
)
280297
)

pipelex/cli/commands/run/bundle_cmd.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ def run_bundle_cmd(
8181
help="Concept ref (e.g. 'document_qa.ReferenceCount') used to resolve a pipe whose output is declared as 'Dynamic'.",
8282
),
8383
] = None,
84+
cost_report: Annotated[
85+
bool | None,
86+
typer.Option(
87+
"--cost-report/--no-cost-report",
88+
help="Override config: --cost-report forces the cost table on; --no-cost-report skips reporting entirely (no table and no CSV file).",
89+
),
90+
] = None,
8491
) -> None:
8592
"""Run a pipeline from a bundle file (.mthds) or pipeline directory.
8693
@@ -171,6 +178,7 @@ def run_bundle_cmd(
171178
dry_run=dry_run,
172179
mock_inputs=mock_inputs,
173180
library_dir=library_dir,
181+
cost_report=cost_report,
174182
telemetry_command_label=f"{COMMAND} bundle",
175183
temporal=temporal,
176184
dynamic_output_concept_ref=dynamic_output_concept_ref,

pipelex/cli/commands/run/method_cmd.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ def run_method_cmd(
8080
help="Concept ref (e.g. 'document_qa.ReferenceCount') used to resolve a pipe whose output is declared as 'Dynamic'.",
8181
),
8282
] = None,
83+
cost_report: Annotated[
84+
bool | None,
85+
typer.Option(
86+
"--cost-report/--no-cost-report",
87+
help="Override config: --cost-report forces the cost table on; --no-cost-report skips reporting entirely (no table and no CSV file).",
88+
),
89+
] = None,
8390
) -> None:
8491
"""Run an installed method by name.
8592
@@ -141,6 +148,7 @@ def run_method_cmd(
141148
dry_run=dry_run,
142149
mock_inputs=mock_inputs,
143150
library_dir=effective_library_dir,
151+
cost_report=cost_report,
144152
telemetry_command_label=f"{COMMAND} method",
145153
temporal=temporal,
146154
dynamic_output_concept_ref=dynamic_output_concept_ref,

pipelex/cli/commands/run/pipe_cmd.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ def run_pipe_cmd(
7777
help="Concept ref (e.g. 'document_qa.ReferenceCount') used to resolve a pipe whose output is declared as 'Dynamic'.",
7878
),
7979
] = None,
80+
cost_report: Annotated[
81+
bool | None,
82+
typer.Option(
83+
"--cost-report/--no-cost-report",
84+
help="Override config: --cost-report forces the cost table on; --no-cost-report skips reporting entirely (no table and no CSV file).",
85+
),
86+
] = None,
8087
) -> None:
8188
"""Run a pipe by code.
8289
@@ -136,6 +143,7 @@ def run_pipe_cmd(
136143
dry_run=dry_run,
137144
mock_inputs=mock_inputs,
138145
library_dir=library_dir,
146+
cost_report=cost_report,
139147
telemetry_command_label=f"{COMMAND} pipe",
140148
temporal=temporal,
141149
dynamic_output_concept_ref=dynamic_output_concept_ref,

pipelex/cogt/usage/cost_registry.py

Lines changed: 60 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def generate_report(
4242
tokens_usages: Sequence[TokensUsage],
4343
unit_scale: float,
4444
cost_report_file_path: Path | None = None,
45+
print_to_console: bool = True,
4546
):
4647
if not tokens_usages:
4748
if pipeline_run_id != "untitled":
@@ -115,69 +116,70 @@ def generate_report(
115116
msg = "Empty report aggregation by model name"
116117
raise CostRegistryError(msg)
117118

118-
console = get_console()
119-
title = f"Costs by model for pipeline '{pipeline_run_id}'"
120-
table = Table(title=title, box=box.ROUNDED)
119+
if print_to_console:
120+
console = get_console()
121+
title = f"Costs by model for pipeline '{pipeline_run_id}'"
122+
table = Table(title=title, box=box.ROUNDED)
121123

122-
scale_str: str
123-
if unit_scale == 1:
124-
scale_str = ""
125-
else:
126-
scale_str = str(unit_scale)
127-
# Add columns
128-
table.add_column("Model", style="cyan", overflow="fold", width=30)
129-
table.add_column("Type", style="dim cyan", width=8)
130-
table.add_column("Input Cached", justify="right", style="green")
131-
table.add_column("Input Non Cached", justify="right", style="green")
132-
table.add_column("Input Joined", justify="right", style="green")
133-
table.add_column("Output", justify="right", style="green")
134-
table.add_column(f"Input Cached Cost ({scale_str}$)", justify="right", style="yellow")
135-
table.add_column(f"Input Non Cached Cost ({scale_str}$)", justify="right", style="yellow")
136-
table.add_column(f"Input Joined Cost ({scale_str}$)", justify="right", style="yellow")
137-
table.add_column(f"Output Cost ({scale_str}$)", justify="right", style="yellow")
138-
table.add_column(f"Total Cost ({scale_str}$)", justify="right", style="bold yellow")
124+
scale_str: str
125+
if unit_scale == 1:
126+
scale_str = ""
127+
else:
128+
scale_str = str(unit_scale)
129+
# Add columns
130+
table.add_column("Model", style="cyan", overflow="fold", width=30)
131+
table.add_column("Type", style="dim cyan", width=8)
132+
table.add_column("Input Cached", justify="right", style="green")
133+
table.add_column("Input Non Cached", justify="right", style="green")
134+
table.add_column("Input Joined", justify="right", style="green")
135+
table.add_column("Output", justify="right", style="green")
136+
table.add_column(f"Input Cached Cost ({scale_str}$)", justify="right", style="yellow")
137+
table.add_column(f"Input Non Cached Cost ({scale_str}$)", justify="right", style="yellow")
138+
table.add_column(f"Input Joined Cost ({scale_str}$)", justify="right", style="yellow")
139+
table.add_column(f"Output Cost ({scale_str}$)", justify="right", style="yellow")
140+
table.add_column(f"Total Cost ({scale_str}$)", justify="right", style="bold yellow")
139141

140-
# Add rows for each model
141-
for model_name, aggregated_data in grouped_by_model.items():
142-
row_total_cost = cls.compute_total_cost(
143-
input_non_cached_cost=aggregated_data[report_field.COST_INPUT_NON_CACHED],
144-
input_cached_cost=aggregated_data[report_field.COST_INPUT_CACHED],
145-
output_cost=aggregated_data[report_field.COST_OUTPUT],
146-
)
142+
# Add rows for each model
143+
for model_name, aggregated_data in grouped_by_model.items():
144+
row_total_cost = cls.compute_total_cost(
145+
input_non_cached_cost=aggregated_data[report_field.COST_INPUT_NON_CACHED],
146+
input_cached_cost=aggregated_data[report_field.COST_INPUT_CACHED],
147+
output_cost=aggregated_data[report_field.COST_OUTPUT],
148+
)
149+
table.add_row(
150+
model_name,
151+
model_types.get(model_name, "llm"),
152+
f"{int(aggregated_data[report_field.NB_TOKENS_INPUT_CACHED]):,}",
153+
f"{int(aggregated_data[report_field.NB_TOKENS_INPUT_NON_CACHED]):,}",
154+
f"{int(aggregated_data[report_field.NB_TOKENS_INPUT_JOINED]):,}",
155+
f"{int(aggregated_data[report_field.NB_TOKENS_OUTPUT]):,}",
156+
f"{aggregated_data[report_field.COST_INPUT_CACHED] / unit_scale:.4f}",
157+
f"{aggregated_data[report_field.COST_INPUT_NON_CACHED] / unit_scale:.4f}",
158+
f"{aggregated_data[report_field.COST_INPUT_JOINED] / unit_scale:.4f}",
159+
f"{aggregated_data[report_field.COST_OUTPUT] / unit_scale:.4f}",
160+
f"{row_total_cost / unit_scale:.4f}",
161+
)
162+
163+
# add total row
164+
footer_style = "bold"
147165
table.add_row(
148-
model_name,
149-
model_types.get(model_name, "llm"),
150-
f"{int(aggregated_data[report_field.NB_TOKENS_INPUT_CACHED]):,}",
151-
f"{int(aggregated_data[report_field.NB_TOKENS_INPUT_NON_CACHED]):,}",
152-
f"{int(aggregated_data[report_field.NB_TOKENS_INPUT_JOINED]):,}",
153-
f"{int(aggregated_data[report_field.NB_TOKENS_OUTPUT]):,}",
154-
f"{aggregated_data[report_field.COST_INPUT_CACHED] / unit_scale:.4f}",
155-
f"{aggregated_data[report_field.COST_INPUT_NON_CACHED] / unit_scale:.4f}",
156-
f"{aggregated_data[report_field.COST_INPUT_JOINED] / unit_scale:.4f}",
157-
f"{aggregated_data[report_field.COST_OUTPUT] / unit_scale:.4f}",
158-
f"{row_total_cost / unit_scale:.4f}",
166+
"Total",
167+
"",
168+
f"{total_nb_tokens_input_cached:,}",
169+
f"{total_nb_tokens_input_non_cached:,}",
170+
f"{total_nb_tokens_input_joined:,}",
171+
f"{total_nb_tokens_output:,}",
172+
f"{total_cost_input_cached / unit_scale:.4f}",
173+
f"{total_cost_input_non_cached / unit_scale:.4f}",
174+
f"{total_cost_input_joined / unit_scale:.4f}",
175+
f"{total_cost_output / unit_scale:.4f}",
176+
f"{total_cost / unit_scale:.4f}",
177+
style=footer_style,
178+
end_section=True,
159179
)
160180

161-
# add total row
162-
footer_style = "bold"
163-
table.add_row(
164-
"Total",
165-
"",
166-
f"{total_nb_tokens_input_cached:,}",
167-
f"{total_nb_tokens_input_non_cached:,}",
168-
f"{total_nb_tokens_input_joined:,}",
169-
f"{total_nb_tokens_output:,}",
170-
f"{total_cost_input_cached / unit_scale:.4f}",
171-
f"{total_cost_input_non_cached / unit_scale:.4f}",
172-
f"{total_cost_input_joined / unit_scale:.4f}",
173-
f"{total_cost_output / unit_scale:.4f}",
174-
f"{total_cost / unit_scale:.4f}",
175-
style=footer_style,
176-
end_section=True,
177-
)
178-
179-
console.print(table)
180-
console.print(" [dim]Note: some costs might be missing or not up-to-date.[/dim]")
181+
console.print(table)
182+
console.print(" [dim]Note: some costs might be missing or not up-to-date.[/dim]")
181183

182184
if cost_report_file_path:
183185
cls.save_to_csv(records, cost_report_file_path)

pipelex/kit/configs/plxt.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# =============================================================================
2-
# Pipelex TOML Configuration for pipelex-demo
2+
# PLXT Configuration
33
# =============================================================================
44
# Configures MTHDS/TOML formatting and linting behaviour for this project.
55
# Powered by the Pipelex extension (plxt / taplo engine).

0 commit comments

Comments
 (0)