Skip to content

feat(mcp): expose max_cost budget ceiling on panel-running tools#579

Merged
the-data-viking merged 1 commit into
mainfrom
feat/mcp-max-cost
Jul 17, 2026
Merged

feat(mcp): expose max_cost budget ceiling on panel-running tools#579
the-data-viking merged 1 commit into
mainfrom
feat/mcp-max-cost

Conversation

@the-data-viking

Copy link
Copy Markdown
Collaborator

Closes #576.

What

Adds an optional max_cost (USD, float > 0) argument to the three panel-running MCP tools — run_panel, run_quick_poll, and extend_panel — wired into the existing CostGate machinery (src/synth_panel/cost.py) the CLI's --max-cost uses. No parallel implementation: the gate is constructed at the tool boundary, threaded through _runners.run_panel_sync into orchestrator.run_panel_parallel, which already knew how to soft-halt on it.

Behavior on a gate trip

Same soft-halt semantics as the CLI: the in-flight panelist(s) complete, no new panelists start, and synthesis is skipped (synthesizing a deliberately-truncated panel would spend past the cap for an untrustworthy summary). The tool response is a valid partial envelope that composes with detail:"summary" and the panel_verdict contract:

  • run_invalid: true, cost_exceeded: true, abort_reason: "cost_exceeded", halted_at_panelist
  • cost_gate snapshot — spend so far, the cap, and the projection that tripped it
  • resume block — persisted partial result_id (the MCP analog of the CLI's checkpoint), completed_panelists, remaining_personas, and a how_to_resume sentence
  • a cost_exceeded: ... line in warnings[]

The completed prefix is persisted as usual, so the full partial transcript stays retrievable via get_panel_result(result_id).

Loud refusals (typed INVALID_TOOL_ARG on max_cost, before any spend)

Mirrors the CLI's --max-cost support matrix (_multi_round_flag_errors):

  • max_cost <= 0
  • sampling mode — the host agent pays; the server has no per-panelist cost accounting to enforce a USD ceiling against
  • models ensembles — the CLI doesn't gate ensembles either; run each model separately with its own cap
  • variants — the gate projection is sized to the base panel; variant expansion would skew it
  • instrument / instrument_pack — on the MCP surface these always dispatch through the multi-round engine, which the cost gate does not cover (the CLI refuses --max-cost there too)

Docs

  • docs/mcp.md — new "max_cost: hard spend ceiling" section with call + trip-envelope examples; tools table rows updated (checked by the test_doc_example_params.py / test_skill_tool_conformance.py CI guards)
  • site/mcp/index.html — "Budget-gated iteration" no longer claims max_cost is CLI-only; "Cost ceiling hit" runbook row covers both surfaces; site/mcp/index.md regenerated via scripts/render_site_markdown.py
  • docs/production-operations.md — MCP max_cost bullet under Cost controls + support-matrix note

Tests

New tests/mcp/test_max_cost.py (21 tests): param plumbing (a real CostGate with the right ceiling/panel size reaches run_panel_parallel; no gate when max_cost omitted), gate-trip envelope shape for all three tools (partial markers, snapshot math, resume block, synthesis skipped, detail composition, verdict retained), and every refusal path. Only the LLM boundary is stubbed; the tool functions, run_panel_sync, and CostGate run for real.

How verified

  • ruff check / ruff format --check — clean
  • mypy src/synth_panel/ — clean
  • Full suite: 3450 passed, 7 skipped/deselected aside; the 7 local failures (test_seed, test_structured_output escalation, test_mcp_stdio_sampling) also fail on a clean checkout of main in this environment (missing OPENROUTER_API_KEY etc.) — pre-existing, unrelated to this change
  • Doc guards (test_doc_example_params, test_skill_tool_conformance, test_site_markdown) — pass

Design decisions worth reviewing

  1. Instrument inputs refuse max_cost even for single-round instruments. The CLI supports --max-cost on single-round instruments because it flattens them onto the single-round orchestrator; MCP instruments always run through run_multi_round_panel, which has no gate wiring. Wiring the gate through the multi-round engine (per-round run_panel_parallel + inter-round halt check) is a natural follow-up if single-round-instrument coverage matters.
  2. extend_panel persists the partial extension round before attaching the halt markers (the pre-extend snapshot still allows rollback); its resume hint is honest that re-calling extend_panel re-asks every saved panelist.
  3. Panelist costs are recorded per-panelist at the orchestrator's priced rate (unchanged CostGate.record path) — the MCP layer adds zero new cost math.

🤖 Generated with Claude Code

The CLI's --max-cost projected-total spend gate had no MCP analog, so
agents could only budget-gate by piloting small and scaling manually.
run_panel / run_quick_poll / extend_panel now accept max_cost (USD),
wired into the existing CostGate machinery (cost.py) with the CLI's
soft-halt semantics: in-flight panelists finish, no new panelists
start, synthesis is skipped, and the response is a valid partial
envelope with run_invalid, cost_exceeded, abort_reason:
"cost_exceeded", halted_at_panelist, the cost_gate snapshot, and an
agent-legible resume block (persisted partial result_id, completed
panelists, remaining personas).

Unsupported combinations refuse loudly with typed INVALID_TOOL_ARG
before any spend (parity with the CLI support matrix): sampling mode,
models ensembles, variants, and instrument/instrument_pack inputs.

Closes #576

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab23f76980

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

synthesis_temperature=synthesis_temperature,
variants=variants,
sessions_out=run_sessions,
cost_gate=cost_gate,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound concurrency before arming the cost gate

When a default run_panel or run_quick_poll call supplies max_cost, this passes the gate to run_panel_parallel without limiting max_workers; that function defaults to len(personas) and submits every panelist before processing the first completion (orchestrator.py:1551,1594-1634). Consequently, by the time the gate trips, all panelists are normally already running, their futures cannot be cancelled, and executor shutdown waits for them to spend tokens even though only the first processed results and costs appear in the partial envelope. The advertised ceiling therefore neither prevents the remaining spend nor reports it; dispatch must be incremental or concurrency must be constrained while the gate is armed.

Useful? React with 👍 / 👎.

Comment on lines +794 to 798
cost_gate_halted = cost_gate is not None and cost_gate.should_halt()
if synthesis and cost_gate_halted:
logger.warning("cost gate halted the panel; skipping synthesis on the partial result")
synthesis = False
if synthesis:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Account for synthesis before spending beyond max_cost

When all panelist completions leave the projection at or below max_cost, this condition permits synthesis even though CostGate.record() is only called for panelist costs in orchestrator.py:1693-1698. For example, a panel costing $1 under a $2 cap can still issue a $5 synthesis request and return a $6 total without any cost_exceeded marker. Thus any capped call with synthesis enabled can exceed the documented hard total-spend ceiling; synthesis must be budgeted or refused when the remaining ceiling cannot cover it.

Useful? React with 👍 / 👎.

@the-data-viking
the-data-viking merged commit 3c7d82d into main Jul 17, 2026
21 checks passed
@the-data-viking
the-data-viking deleted the feat/mcp-max-cost branch July 17, 2026 05:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP: add max_cost budget ceiling to panel-running tools

2 participants