How AI site prose is actually produced — the two provider paths, model config, the 90s timeout, the anti-boilerplate footer, and every failure mode that bit us.
This is the engine behind Content Generation and Healing. It produces businesses.ai_generated_content (headline, about, services, why_choose_us, faq, meta_description, suggested_colors). Hard-won operational truths live here — read before touching AI content, model config, or regeneration.
The governing principle (why site voice varies so much place to place) lives in Per-Location Data Enrichment: a site's voice is signal-in, signal-out. The lever for a thin site is more/better input data, never a forced tone.
generateBusinessContent($businessRow) in includes/ai-content.php is the entry. It takes the business row (from getBusinessById()), NOT a Google-Places shape. There is no generateContent() — calling that name silently broke every content regen with "Content generator unavailable" (fixed v2.2.235).
It tries two paths, and a change to ANY of prompt / timeout / model must cover BOTH:
generateContentFlexible() inincludes/ai-provider-system.php. Reads the per-content-type model config, builds the prompt from the DB template, calls callAnthropic() / callOpenAICompatible().
ai-content.php's own callAnthropic/callOpenAI. Has its ownmodel resolution + the historical default claude-3-5-sonnet-20241022.
DURABLE RULE: the flexible path is what runs ~always. If you only patch ai-content.php you patched the wrong path. (This exact trap meant the anti-boilerplate footer didn't apply for a full cycle — see below.)
Per-content-type provider/model lives in the ai_content_configs table (rows: business_website, photo_vision), joined to ai_models + ai_providers. Set it via Admin → AI Management (it's under the Infrastructure menu, ?page=admin§ion=ai → Content Configuration → Configure), NOT a migration. Class aliases resolve via aiModelResolve() over the includes/ai-models.php catalog.
DURABLE RULE: the content primary model must be a FAST quality model — claude-sonnet-4-6. Do NOT set a reasoning model (gpt-5, o-series) as the synchronous content primary: they take far longer than the timeout for a 2048-token generation and every call aborts with HTTP 0. (gpt-5 as a primary is what caused the June 2026 network-wide content outage.)
Content asks for up to 2048 output tokens; a quality model needs 25-60s for that length. The historical 30s cURL timeout aborted generation mid-stream → cURL returns HTTP 0. Fixed: AI_TIMEOUT_SECONDS = 90 in config/app.php (legacy path) AND callAnthropic/callOpenAICompatible in ai-provider-system.php default their cURL timeout to AI_TIMEOUT_SECONDS (v2.2.238). Both paths, or it regresses. Safe because content gen runs via the queue worker (post-response) or CLI batches, and admin synchronous regen is rare/one-at-a-time — not a visitor FPM hot path.
_aiContentProseQualityFooter() (ai-content.php) prepends the shared Design Standards rules + an ORIGINALITY block that BANS templated openers ("nestled", "in the heart of", "stands as a beacon", "more than just a", "steeped in tradition") and em-dashes, and requires a page-specific opener built from real data. It is appended in BOTH paths (legacy always; flexible since v2.2.239). Without it, similar businesses get near-identical prose — the near-duplicate cluster that triggered the [elks.club scaled-content demotion]. See the project_seo_cliff_recovery memory + site-quality scoring for measuring boilerplate.
dz ai_pingshows providers 200 in <1s, it's the model/timeout, not the network.
This stalled a batch mid-run (~1,158 calls drained Anthropic). It IS logged (callAnthropic logs 4xx) but to error_logs in UTC while batch logs print America/New_York — a ~4h offset that makes it look unlogged. Check instantly with dz ai_status (flags billing/credit/auth).
generateBusinessContent returned null (both pathsfailed). The regenerationRun result carries a detail / $GLOBALS['__lastContentGenError'] with the real per-provider cause; surface that, don't guess.
regenerationRun($businessId, 'content') ([regeneration.php]) wraps generateBusinessContent with a readability quality gate (keeps prior content if the new draft fails) + manifest stamp. Fleet rollout = worst-first over site-quality-score boilerplate-flagged pages, rate-limited, resumable (re-run skips now-clean). ALWAYS dz ai_status first — a big batch on an empty credit balance fails silently. Operator preference: drive batches via the Dev API, not raw php -r pasted into a root shell.
dz ai_status — live provider health + billing/credit alert + recent provider errors (UTC-correct).dz ai_ping — minimal keyed probes to both providers (egress vs auth vs timeout).dz quality root=<root> — boilerplate/flag rollup to measure de-boilerplate progress.See also hosting infrastructure (the queue + drain) and design standards (the prose rules enforced at the source).