feat(cost): per-run cost governor — reserve-before-call enforcement
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
This commit is contained in:
@@ -125,6 +125,16 @@ class PRAutoBlogger_Image_Pipeline {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// v0.18.0 — per-run governor: reserve the curl_multi batch's SUMMED
|
||||
// worst-case estimate before dispatch (no-op outside a governed
|
||||
// run). A breach halts the run and skips the whole batch.
|
||||
try {
|
||||
$batch_reservation = PRAutoBlogger_Cost_Governor::open_amount_reservation( $estimated_cost, 'image_batch' );
|
||||
} catch ( PRAutoBlogger_Cost_Ceiling_Exception $e ) {
|
||||
$result['errors'][] = $e->getMessage();
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Build all prompts upfront (LLM calls happen here, sequentially).
|
||||
$article_prompt = $this->prompt_builder->build_article_prompt( $article_data );
|
||||
$batch_requests = array(
|
||||
@@ -151,11 +161,19 @@ class PRAutoBlogger_Image_Pipeline {
|
||||
try {
|
||||
$batch_results = $this->provider->generate_image_batch( $batch_requests );
|
||||
} catch ( \Throwable $e ) {
|
||||
PRAutoBlogger_Cost_Governor::release( $batch_reservation );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Batch generation failed: ' . $e->getMessage(), 'image_pipeline' );
|
||||
$result['errors'][] = $e->getMessage();
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Settle the batch reservation to the actual per-image costs.
|
||||
$actual_batch_cost = 0.0;
|
||||
foreach ( $batch_results as $batch_entry ) {
|
||||
$actual_batch_cost += (float) ( $batch_entry['cost_usd'] ?? 0 );
|
||||
}
|
||||
PRAutoBlogger_Cost_Governor::settle( $batch_reservation, $actual_batch_cost );
|
||||
|
||||
// Single-shot retry for any slot the provider flagged as NSFW-blocked.
|
||||
if ( PRAutoBlogger_Image_NSFW_Retry::is_enabled() ) {
|
||||
( new PRAutoBlogger_Image_NSFW_Retry( $this->provider, $this->prompt_builder ) )
|
||||
|
||||
Reference in New Issue
Block a user