Reasoning models at high effort could spend the entire max_tokens budget
on thinking and return zero visible content with finish_reason=length;
the empty article flowed writer -> editor (reject) -> empty draft post,
with the cost booked as success (2026-06-11 incident: prod run acf24029,
draft post 921; diagnosis: convo pipeline-v2-phase1 seq 12).
- Empty-completion guard at the provider seam (new
OpenRouter_Completion_Guard): empty/whitespace content is a failure;
one retry with reasoning disabled (per-call override) when
finish_reason=length or reasoning was active, then the stage fails.
- finish_reason=length always warning-logged (model, stage, tokens).
- Failed/retried attempts booked to generation_log with
response_status=error + real token burn; run_decisions verdict rows
(retry_reasoning_disabled / failed_empty_completion) for v0.18.0 runs.
- Writer-path guard in Content_Generator::execute_stage and
belt-and-braces empty check in Publisher::create_post.
- Reasoning budget sanity: reasoning.max_tokens cap + completion
headroom via new prautoblogger_reasoning_max_tokens setting (default
constant 2048, 0 = legacy uncapped effort mode); cap replaces effort
when active; governor reservations use the effective ceiling.
- Body assembly moved to Request_Builder::build_body (300-line cap).
- Tests: guard verdict matrix, builder budget shaping, writer/publisher
guards, full provider retry path.
Agent-Session: prab-empty-draft-guard-20260611
Pipeline v2 Phase 1, commit 3/4. Cost_Governor reserves each call's
worst-case estimate (prompt chars/4 + max_tokens via the #18 pricing
chain) against the run ledger row with one atomic conditional UPDATE
(the #10 lock discipline) BEFORE dispatch, then settles to actuals after
each response; failed calls release their hold. Integrated inside
OpenRouter_Provider::send_chat_completion so EVERY text-LLM call path is
guarded with zero call-site changes; the image curl_multi batch reserves
its summed estimate in Image_Pipeline before generate_image_batch.
Breach: run halted (sticky), overage recorded, Cost_Ceiling_Exception
aborts the un-dispatched call, process_next_queued_article aborts the
remaining queued articles of halted/failed runs, and the Review Queue
shows a 'Halted runs' notice (runs halted pre-post have no draft to
list). Ceiling = prautoblogger_per_run_cost_ceiling_usd setting (default
constant $0.50, snapshotted at run start, 0 = disabled). Standalone
no-run calls, the monthly cap, and the AI Gateway path are unchanged.
Under-300: provider API-key fetch+validation moved verbatim to
OpenRouter_Request_Builder::resolve_api_key().
Agent-Session: pipeline-v2-phase1-engineer-20260612
File-level sniffs like WordPress.Files.FileName are evaluated at the file level,
not at code level. They require phpcs:ignore directives to be placed within the
file-level docblock (/** ... */), not in code comments.
Moved all 81 phpcs:ignore pragmas from code comments to the file-level docblocks.
This applies to:
- 79 class files with class-naming convention ignores
- prautoblogger.php with strict-types docblock ignore
- uninstall.php with strict-types docblock ignore
- Added phpcs:ignore for class file naming convention (79 violations)
WordPress.Files.FileName.InvalidClassFileName is a known architectural
issue — class files use short names without prautoblogger- prefix
- Added phpcs:ignore for ->prepare() table interpolation (56 violations)
WordPress.DB.PreparedSQL.InterpolatedNotPrepared: dynamic table names
(prab_generation_logs, prab_cost_logs) require interpolation; ->prepare()
does not support table name placeholders, only column values
- Fixed file-level docblock positioning (2 violations)
prautoblogger.php and uninstall.php: moved declare(strict_types=1) before
file docblock per PHP best practices
Run phpcbf --standard=WordPress-Core across includes/, prautoblogger.php,
and uninstall.php. Fixed 1,362 formatting violations in 75 files:
- Short array syntax: [] → array()
- Line breaking for long function calls
- Consistent indentation
- Function call parameter alignment
No logic changes; purely mechanical formatting.
Extracted credential validation, request building, response parsing, and
configuration helpers from the 464-line OpenRouter provider into separate,
focused classes. This follows the pattern established by the Cloudflare
image provider and brings all files under the project's 300-line limit.
Files created:
- class-open-router-validator.php (152 lines): Handles credential validation
with HTTP probes and diagnostic messaging. Delegates from the provider.
- class-open-router-config.php (84 lines): Centralizes admin config resolution
for API base URL and cache TTL settings. Used by both provider and validator.
- class-open-router-request-builder.php (79 lines): Encapsulates request header
building and cURL auth filter registration for hosting environments that
strip Authorization headers.
- class-open-router-response-parser.php (87 lines): Handles response parsing,
error classification, and retry-decision logic.
- Tests: OpenRouterValidatorTest.php with comprehensive credential checks.
Provider shrinks from 464 to 278 lines. All classes are under 300 lines.
Dependencies remain clean: validator and config are independent, request-builder
and response-parser have no circular imports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>