feat(models): OpenRouter model registry + interface + cron wiring (model-picker commit 1) (#13)

Foundation for the smart model picker. Fetches, normalizes, and caches
the OpenRouter model list (/api/v1/models — free, unauthenticated).
Daily refresh via the existing prautoblogger_daily_generation cron with
12h idempotency. 3-retry exponential backoff on transient failures;
serves last-good option payload when stale-and-fetch-fails.

New files:
- interface-model-registry.php — Phase 3-aware contract (94 lines)
- class-openrouter-model-registry.php — fetch + cache + query (226 lines)
- class-openrouter-model-normalizer.php — raw → standardized (114 lines)

Zero-coupling: no PRAUTOBLOGGER_* constants inside the service classes.
Config injected via constructor. Phase 2 lift into a shared Composer
package requires only namespace rename, zero internal edits.

Cost impact: $0/month (free endpoint, no LLM tokens).

Agent-Session: cowork-2026-04-16-model-picker-commit1

Co-authored-by: peptiderepo <peptiderepo@users.noreply.github.com>
This commit is contained in:
peptiderepo
2026-04-16 16:50:12 +07:00
committed by GitHub
parent 33413cf678
commit be5c659999
8 changed files with 741 additions and 3 deletions

View File

@@ -18,11 +18,11 @@ declare(strict_types=1);
*/
class PRAutoBlogger {
/**
* Whether the plugin has been initialized.
*/
private bool $initialized = false;
/** @var PRAutoBlogger_OpenRouter_Model_Registry|null Lazy-loaded singleton. */
private ?PRAutoBlogger_OpenRouter_Model_Registry $model_registry = null;
/**
* Register all hooks and initialize the plugin.
*
@@ -118,6 +118,11 @@ class PRAutoBlogger {
private function register_cron_hooks(): void {
add_action( 'prautoblogger_daily_generation', [ $this, 'on_daily_generation' ] );
add_action( 'prautoblogger_collect_metrics', [ $this, 'on_collect_metrics' ] );
// Model registry refresh — fires inside on_daily_generation() before the
// generation lock, so a stuck lock doesn't freeze the registry too.
$registry = $this->get_model_registry();
add_action( 'prautoblogger_refresh_model_registry', [ $registry, 'refresh' ] );
}
/** Register AJAX handlers for admin actions. */
@@ -218,6 +223,10 @@ class PRAutoBlogger {
* @return void
*/
public function on_daily_generation(): void {
// Refresh the OpenRouter model registry (idempotent, skips if <12h old).
// Fires BEFORE the generation lock so a stuck lock doesn't block the refresh.
do_action( 'prautoblogger_refresh_model_registry', false );
if ( ! $this->acquire_generation_lock() ) {
PRAutoBlogger_Logger::instance()->info( 'Daily generation skipped: already running (lock held).', 'scheduler' );
return;
@@ -361,6 +370,24 @@ class PRAutoBlogger {
return $result > 0;
}
/**
* Lazy-load the OpenRouter model registry singleton.
*
* Config (option names, transient names) is injected here — the registry
* class itself has no knowledge of PRAUTOBLOGGER_* constants.
*
* @return PRAutoBlogger_OpenRouter_Model_Registry
*/
public function get_model_registry(): PRAutoBlogger_OpenRouter_Model_Registry {
if ( null === $this->model_registry ) {
$this->model_registry = new PRAutoBlogger_OpenRouter_Model_Registry(
'prautoblogger_openrouter_model_registry',
'prautoblogger_openrouter_model_registry_cache'
);
}
return $this->model_registry;
}
/** Release the generation mutex. */
private function release_generation_lock(): void {
global $wpdb;