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:
peptiderepo
2026-06-11 04:20:30 +00:00
committed by peptiderepo
parent c723af927b
commit c6d051c87a
13 changed files with 667 additions and 47 deletions

View File

@@ -78,4 +78,43 @@ class PRAutoBlogger_OpenRouter_Request_Builder {
add_action( 'http_api_curl', $curl_auth_filter, 99, 3 );
return $curl_auth_filter;
}
/**
* Fetch, decrypt, and format-validate the OpenRouter API key.
*
* Moved from OpenRouter_Provider in v0.18.0 (300-line cap); the
* messages and behavior are unchanged.
*
* @return string Validated plaintext API key.
* @throws \RuntimeException When the key is missing or has an
* unexpected format (corrupted decryption).
*/
public function resolve_api_key(): string {
$encrypted = get_option( 'prautoblogger_openrouter_api_key', '' );
$api_key = '' === $encrypted ? '' : PRAutoBlogger_Encryption::decrypt( $encrypted );
if ( '' === $api_key ) {
throw new \RuntimeException(
__( 'OpenRouter API key is not configured. Go to PRAutoBlogger → Settings.', 'prautoblogger' )
);
}
// Validate key format — OpenRouter keys start with "sk-or-".
// A key that decrypts to garbage (e.g. after a salt change) won't match.
if ( 0 !== strpos( $api_key, 'sk-or-' ) ) {
PRAutoBlogger_Logger::instance()->error(
sprintf(
'Decrypted API key has unexpected format (prefix="%s", len=%d). Re-enter your key in settings.',
substr( $api_key, 0, 6 ),
strlen( $api_key )
),
'openrouter'
);
throw new \RuntimeException(
__( 'OpenRouter API key appears corrupted (unexpected format). Please re-enter your key in PRAutoBlogger → Settings.', 'prautoblogger' )
);
}
return $api_key;
}
}