Skip to content

AI assist

Optional, bring-your-own-key AI assistance: title and meta-description suggestions, plain-language explanations of scan issues, a one-click description rewrite, and a structured-data (schema.org) suggestion. It is off by default — with the flag off, no AI code path runs at all.

Three things define the design:

  • Your key, your provider. Requests go from your server directly to the provider you configure — Anthropic, OpenAI, Google, or a local / OpenAI-compatible server — billed to your account (or free). Nothing is proxied, metered, or resold, and the package sends no telemetry anywhere.
  • Suggestions, never silent changes. The model proposes; you pick. A picked suggestion or fix is only ever applied by an explicit action — it fills a form field, or writes one reviewed value when you click Apply — and the regular validation (60/160 counters, evaluator warnings, the schema validator) applies to it like any hand-typed value.
  • Always non-fatal. A missing key, an invalid key, an exhausted account, a rate limit, or a timeout produces an inline message. It can never block saving, rendering, or scanning.

Providers at a glance

Pick a provider by what you already have a key for, and how much you want to spend. All four run every surface identically — the choice only changes cost, speed, and where the request goes.

ProviderDefault modelStructured outputMarginal costBest for
Local (Ollama / LM Studio / vLLM)llama3.1 (set your own)Best-effort (response_format)$0 (self-hosted)Privacy — nothing leaves your network
OpenAIgpt-5.5Native (Structured Outputs, strict)~$0.005 / suggestionAn existing OpenAI account
Anthropicclaude-opus-4-8Native (output_config.format)~$0.015 / suggestionHighest-quality copy
Googlegemini-2.5-flashNative (responseSchema)~$0.0005 / suggestion (paid)Lowest paid cost per call — but its free tier is too rate-limited for real use (see below)

Notes confirmed against a live run of every provider:

  • Structured output is enforced by the API on OpenAI, Google, and Anthropic — each is constrained to the exact JSON schema, so no surface regex-parses model text on the happy path. A local server is asked for it best-effort (a server that ignores response_format still returns usable text, which is tolerantly parsed as the fallback). Either way a caller gets a clean list or a clear failure — never a half-parsed reply.
  • Google and Anthropic are the two extremes on token spend. Gemini 2.5 and Gemma are thinking models: they burn hidden reasoning tokens before any visible output (measured: a three-line description cost Gemini ~500 hidden reasoning tokens on top of ~100 visible ones). Those tokens are billed as output — still cheap on Gemini's paid rates, but the reason the reasoning floor below exists. Anthropic used no hidden reasoning tokens in the same calls.
  • The default model is a starting point, not a lock-in: any model your key or server can reach works (SEO_PRO_AI_MODEL), and a smaller model (claude-haiku-4-5, gpt-5.4-mini, gemma-3-12b-it) cuts cost.

Setup

Enable the feature and put your provider key in the environment. Switch providers by changing exactly two lines — the provider and the key.

dotenv
SEO_PRO_AI_ENABLED=true
SEO_PRO_AI_PROVIDER=local
SEO_PRO_AI_MODEL=llama3.1          # a model the server has pulled
SEO_PRO_AI_LOCAL_BASE_URL=http://localhost:11434/v1
SEO_PRO_AI_LOCAL_ALLOW_PRIVATE=true   # required for a localhost server
# no API key needed for a local server
dotenv
SEO_PRO_AI_ENABLED=true
SEO_PRO_AI_PROVIDER=openai
SEO_PRO_AI_API_KEY=sk-...
# optional: SEO_PRO_AI_MODEL=gpt-5.4-mini  (default: gpt-5.5)
dotenv
SEO_PRO_AI_ENABLED=true
SEO_PRO_AI_PROVIDER=anthropic
SEO_PRO_AI_API_KEY=sk-ant-...
# optional: SEO_PRO_AI_MODEL=claude-haiku-4-5  (default: claude-opus-4-8)
dotenv
SEO_PRO_AI_ENABLED=true
SEO_PRO_AI_PROVIDER=google
SEO_PRO_AI_API_KEY=AIza...        # AI Studio key: aistudio.google.com/apikey
# use a paid (billing-enabled) key for real use — the free tier is heavily rate-limited
# optional: SEO_PRO_AI_MODEL=gemma-3-12b-it  (default: gemini-2.5-flash)

A provider API key is separate from a Claude / ChatGPT subscription

A Claude Code, Claude.ai, or ChatGPT subscription does not fund the API. SEO_PRO_AI_API_KEY must be a pay-as-you-go API key from the provider's developer console (or a Google AI Studio key), with its own credit balance. A subscription-only or unfunded account will authenticate but return an out of credit / quota error — see Troubleshooting.

The config file (config/seo-pro.php, ai block) exposes timeout, max_input_chars, max_output_tokens, token_budgets, reasoning_models

  • reasoning_min_output_tokens, suggestion_count, bulk_model (the cheap tier for bulk-fill — see Cost), retry, the pricing table, and the local sub-block — all covered under Limits and tuning.

Key handling with a cached config

The config stores only the name of the environment variable (api_key_env), never the key — so php artisan config:cache never writes your key to bootstrap/cache/config.php. The flip side: with a cached config, .env is not loaded, so set SEO_PRO_AI_API_KEY as a real environment variable on the server.

Running at $0, and the cheapest paid option

  • Local / OpenAI-compatible is the true $0 option. A model you host runs every surface at no marginal cost and nothing leaves your network — see below.
  • Google is the cheapest paid provider, not a free one. A free AI Studio key (aistudio.google.com/apikey, format AIza…) authenticates and is fine for a quick trial, but its free tier is heavily rate-limited — enough to make real or bulk use impractical (you'll hit quota_exceeded / rate_limited quickly). Enable billing on the key for production; even then gemini-2.5-flash is the cheapest cloud option per call. The default model is gemini-2.5-flash; the open gemma-3-* models work the same way. Both are thinking models, so they are listed in reasoning_models and get the higher output-token floor automatically.

For a genuinely free, private setup, use a local server instead:

  • Local / OpenAI-compatible. Point provider=local at any server speaking the OpenAI Chat Completions API: Ollama, LM Studio, vLLM, LocalAI (all local, all free), or a remote gateway like OpenRouter. Set SEO_PRO_AI_LOCAL_BASE_URL to the server's API root (/chat/completions is appended) and SEO_PRO_AI_MODEL to a model the server has loaded. Nothing leaves your network.

Local base_url is validated — opt in for localhost

The base_url is a privileged setting, validated through the same SsrfGuard every other outbound fetch uses: http/https only, no userinfo, and — by default — it must resolve to a public address, so a stray or hostile base_url can't be turned into a probe of internal services. A genuinely local server lives on 127.0.0.1 (a private address), so a local provider needs the explicit opt-in seo-pro.ai.local.allow_local_addresses (SEO_PRO_AI_LOCAL_ALLOW_PRIVATE=true). Leave it off for a public gateway (OpenRouter). The request path is fixed and redirects are never followed, so the key can't be bounced to another host.

Ollama thinking models are slow by default — turn reasoning off

A local reasoning model (e.g. Gemma via Ollama) spends seconds on hidden reasoning before it answers, which can exceed the short default timeout. Add ['think' => false] to seo-pro.ai.local.extra_body to skip that reasoning on the strict-JSON suggestion calls — it cuts latency sharply. If calls still time out, raise seo-pro.ai.timeout (see below). No temperature is ever sent: some reasoning models reject it, so there is intentionally no knob for it.

Cost

There is no package markup — you pay the provider directly, or nothing on a local provider. Two numbers matter: the per-suggestion cost for interactive use, and the bulk-fill cost for a whole collection.

The seo-pro.ai.pricing table (USD per 1,000,000 tokens) turns a token estimate into the dollar figure the bulk-fill confirm prompt shows. The shipped defaults are approximate public prices — override them with your provider's current published prices for an accurate estimate:

Model patternInput $/1MOutput $/1M
claude-opus-*15.0075.00
claude-sonnet-*3.0015.00
claude-haiku-*1.005.00
gpt-5*mini*0.501.50
gpt-5*5.0015.00
gemini-2.5-pro*1.2510.00
gemini-*flash*0.150.60

Measured against a real page (one title + one description suggestion, the default models), at those prices. The last column is the same 1,000-record run through --batch, which bills at half the per-token rate on the two providers that offer a batch endpoint:

Provider / model~ per suggestion pair~ per 1,000 records (bulk-fill)~ per 1,000 records (--batch)
Local gemma/llama (Ollama)$0$0n/a (no batch endpoint)
Google gemini-2.5-flash (paid)~$0.001~$0.40n/a (no batch endpoint)
OpenAI gpt-5.5~$0.008~$5.25~$2.63 (50% off)
Anthropic claude-opus-4-8~$0.03~$20~$10 (50% off)

Bulk-fill figures are close to the pre-run estimate the command prints (see Bulk-fill); it is deliberately honest to about ±50%, since real input size and output length vary per page. One caveat for thinking models: the estimate counts only visible output, so a model that spends heavy hidden reasoning (Gemini 2.5, Gemma — see above) runs somewhat higher than the printed figure. A model with no pricing entry (any local model) shows a token estimate only, with no invented dollar figure.

Cheaper bulk generation

Interactive single suggestions want your best model; a bulk-fill of hundreds–thousands of short title/description gaps rarely does — the quality edge of a top model is seldom worth its per-call cost at that volume. Set seo-pro.ai.bulk_model (SEO_PRO_AI_BULK_MODEL) to your provider's cheap tier and only the bulk-fill pass (seo-pro:ai-fill / SeoPro::aiFill()) uses it — the Filament and seo-pro:ai-suggest single-suggestion paths keep the quality model. Left unset (null), bulk-fill uses the same model — no silent downgrade. The pre-run cost estimate prices whichever model the run will actually bill.

dotenv
SEO_PRO_AI_MODEL=claude-opus-4-8        # interactive: highest quality
SEO_PRO_AI_BULK_MODEL=claude-haiku-4-5  # bulk-fill: cheap tier

Recommended cheap tier per provider (each matches the pricing table, so the estimate stays priced): anthropic claude-haiku-4-5 · openaigpt-5.5-mini · google gemini-2.5-flash (already the default) · local a smaller local model.

A 100-page fill (each page missing both title and description = 200 provider calls), priced from the shipped pricing defaults and the estimator's per-call token model (600 input + 150 output tokens/call), quality model vs its bulk_model cheap tier:

ProviderQuality model — 100 pagesbulk_model cheap tier — 100 pages
Anthropicclaude-opus-4-8$4.05claude-haiku-4-5$0.27
OpenAIgpt-5.5$1.05gpt-5.5-mini$0.11
Googlegemini-2.5-pro$0.45gemini-2.5-flash$0.04
Local (Ollama / vLLM)any model — $0any model — $0

Same ±50% honesty caveat as the printed estimate — real input size and output length vary per page, and a thinking model's hidden reasoning tokens aren't in the visible-output figure. Override seo-pro.ai.pricing with your provider's current published prices for an accurate number.

Limits and tuning

Every knob lives in the config/seo-pro.php ai block:

  • timeout (default 15 seconds, env SEO_PRO_AI_TIMEOUT) — also the cap on the synchronous call the Filament suggestion modal makes while it opens, so it is kept short for UX. A slow reasoning or local model can exceed 15s and time out; raise it via SEO_PRO_AI_TIMEOUT (and see the Ollama think => false tip above) if you run one. A timeout is always non-fatal: it produces an inline error, never a blocked save.
  • max_input_chars (default 6000) — the cost + privacy cap on how much page content (plain text, HTML stripped) is sent per request.
  • max_output_tokens (default 1000) — base cap on generated tokens. A reply that hits it returns a distinct truncated failure, never a silent half-answer.
  • token_budgets — per-task output caps (suggestions 800, explanation 600, rewrite 300, schema_suggestion 700). None needs the full default, but the reasoning floor is applied on top.
  • reasoning_models + reasoning_min_output_tokens (default 2000) — any model whose name matches a pattern (*gemma*, gemini-2.5-*, o1*/o3*/o4*) has its output budget raised to the floor, because a thinking model spends hidden tokens before producing any visible output and a small budget would truncate it.
  • suggestion_count (default 3) — how many title/description alternatives to request.
  • retry — automatic retry for transient failures only; see How replies are handled.

In Filament

With the optional Filament packages installed (rankbeam/laravel-seo-filament >= 1.1), enabling AI assist adds:

  • Suggest with AI on the SEO title and description fields of every resource using the SEO section (on edit pages). The modal shows the generated alternatives with character counts; picking one fills the field for review.
  • Explain (AI) on the dashboard issue table: a short plain-language explanation of the issue and the concrete fix.
  • Rewrite description (AI) on the dashboard issue table (next to Explain): proposes one improved meta description, always within the 160-character limit. Review it in the modal; clicking Apply rewrite writes it to the page's seo_meta record. Nothing is written until you apply it.
  • Suggest structured data (AI) on the dashboard issue table: proposes the best schema.org rich-result type (Product, Article, or Breadcrumb) and shows the JSON-LD built for it. Clicking Apply structured data adds it to the page's seo_meta.schema_jsonld — the same column the optional structured-data editor manages, so it round-trips and stays editable there. An incomplete suggestion (e.g. an Article with no author or image) is shown with the missing fields and is not applied.

The last two are bounded fixes: see Bounded fixes.

Bounded fixes (propose, never auto-apply)

Two assist actions go one step beyond a suggestion — they produce a single, constrained value you can apply in one click. Both still propose only: nothing is persisted until you explicitly accept.

  • Rewrite description (SeoSuggestionService::rewriteDescription($model, $issue?)) returns one meta description always within the core DESCRIPTION_MAX_LENGTH (160). If the model overshoots, the text is trimmed deterministically at a sentence (then word) boundary, so an accepted rewrite can never itself trip the description_too_long warning. Passing the scan issue steers the rewrite (e.g. too long vs missing).
  • Suggest structured data (SeoSuggestionService::suggestSchemaType($model)) asks the model only for a type recommendation and leaf field values — never raw JSON-LD. Deterministic code then assembles the document with the core schema builders (ProductSchema / ArticleSchema / BreadcrumbSchema) and validates it with the core SchemaValidator, so a hallucinated @type, @context, or structure can never reach the page. If the assembled document is missing a required field, it is surfaced as incomplete and withheld — in a live test, one provider proposed an Article for a thin page and it was correctly withheld for a missing author and image, while others declined to suggest a type at all.

Headless

The same capability as JSON, for scripts and non-Filament apps:

bash
# title + description suggestions for a model
php artisan seo-pro:ai-suggest "App\Models\Post" 42

# one field only
php artisan seo-pro:ai-suggest "App\Models\Post" 42 --field=description

# explain a scan issue (IDs from seo-pro:scan-status)
php artisan seo-pro:ai-suggest --issue=17

# suggest a schema.org type + built, validated JSON-LD for a model
php artisan seo-pro:suggest-schema "App\Models\Post" 42

Output includes the suggestions (or the recommended type, the built JSON-LD, and whether it validates), the model used, and the token usage per request (input / output / reasoning) — so you can see exactly what each call cost. The command exits non-zero on any failure, with the error in the JSON envelope. Like every assist surface, seo-pro:suggest-schema is propose-only — it prints the document and writes nothing.

Bulk-fill missing metadata

Everything above is propose-only. The one batch surface that writes is seo-pro:ai-fill (and SeoPro::aiFill()): the "fill everything" pass for a whole model collection.

bash
# preview what would be written (no changes saved)
php artisan seo-pro:ai-fill "App\Models\Post" --dry-run

# fill missing descriptions across all configured models
php artisan seo-pro:ai-fill --field=description

# all configured (seo.audit.models / seo.sitemap.models) models, all fields
php artisan seo-pro:ai-fill --force

It iterates the records, finds the ones whose title or description is missing — no explicit value and no computed fallback, the same definition the audit uses — generates one with the suggester, and saves it.

It is deliberately conservative:

  • Only gaps are filled. A record that already has (or can derive) the field is skipped; an existing value is never overwritten.
  • --dry-run generates and prints the values without saving, so you can review the spend and the output first.
  • It writes, so in production it asks for confirmation unless you pass --force. --field (title | description | all) and --limit scope the run.
  • Each filled field is one suggester call billed to your key, and it runs only when AI assist is enabled.

At scale: pacing, a cost estimate, and crash-resume

Filling hundreds or thousands of models is a long, paid operation, so the command is built to be safe against a large collection:

  • Paced calls. seo-pro.ai.fill.throttle_ms (default 200) inserts a delay between provider calls so a big run doesn't burst into the provider's rate limit. Set it to 0 for a local or free provider that wants maximum speed, or raise it on a low tier.

  • A cost estimate you confirm first. A run that will touch at least seo-pro.ai.fill.confirm_over records (default 100) prints an estimate and asks before making a single call:

    text
    About to fill ~890 missing fields via anthropic (claude-opus-4-8) across 948 records.
    Estimated ~667,500 tokens ≈ $18.02 (rough, ±50%).
    Continue? (yes/no) [no]

    The dollar figure comes from the seo-pro.ai.pricing table (see Cost). A model with no pricing entry (e.g. a local model) shows the token estimate with no invented dollar figure. --force skips the prompt for automation; a dry run confirms too, because it makes the same paid calls.

  • Resumable — an interrupted run never re-pays. Progress is checkpointed to storage after every record, so if the run is killed (a worker timeout, a deploy, Ctrl-C) the next run resumes: records already filled are skipped, not billed again. A transient failure (rate limit, timeout) leaves its record un-checkpointed so the resume retries exactly it. A clean run clears its checkpoint; pass --fresh to ignore a prior run's checkpoint and start over.

php
use Rankbeam\Seo\Pro\Facades\SeoPro;

$summary = SeoPro::aiFill()->fill([\App\Models\Post::class], 'all', limit: 50, apply: true);
// ['processed' => 120, 'filled' => 18, 'skipped' => 102, 'failed' => 0, 'resumed' => 0, 'errors' => [], 'records' => [...]]
// On a provider failure, 'errors' maps each distinct error code to its human
// message (e.g. 'quota_exceeded' => 'OpenAI: the provider account is out of
// credit or quota…'), and the seo-pro:ai-fill command prints those reasons —
// so a run never fails silently.

Batch mode (50% cheaper)

For a large fill where you don't need the results in the next minute, --batch routes the whole run through the provider's asynchronous batch endpointAnthropic Message Batches or the OpenAI Batch API — which bill at half the per-token price. Google and local providers have no batch endpoint, so --batch prints a notice and runs the sequential fill instead.

A batch is submit-now, collect-later, split across two runs of the same command so the process can be closed in between:

bash
# 1) Submit: builds one request per missing field, sends the whole batch in a
#    single call, prints the discounted estimate, and exits. Nothing is written yet.
php artisan seo-pro:ai-fill "App\Models\Post" --field=description --batch

#    → Batch submitted: msgbatch_01Hkc… — 890 requests across 890 records via anthropic.
#      Most batches finish within an hour (max 24h, then they expire).
#      Re-run the SAME command to poll and apply the results:
#        php artisan seo-pro:ai-fill "App\Models\Post" --field=description --batch

# 2) Collect: re-run the same command. While the batch is still processing it
#    just says so and exits; once results are ready it writes them and prints
#    the usual summary.
php artisan seo-pro:ai-fill "App\Models\Post" --field=description --batch
  • Same requests, half the price. Each batched item is byte-identical to the synchronous call — same prompts, same structured-output schema, same model (the cheap bulk_model when set). The only thing that changes is when the model runs.
  • The pre-run estimate is the discounted one. Over the seo-pro.ai.fill.confirm_over threshold, submit prints the 50%-off figure and asks before sending anything (--force skips it for automation).
  • Survives being closed. The provider batch id and the request→record map are persisted with the same crash-resume bookkeeping as a sequential run, so the collect step works from a fresh process (a later cron tick, a redeploy). If the submitting run is killed in the split second between the provider accepting the batch and its id being written, the next run fails closed — it tells you a batch may be in flight and to check the provider dashboard, rather than silently re-submitting the whole (paid) batch.
  • Never overwrites. The no-overwrite rule holds across the entire batch window: at collect time each record is re-checked, and only fields still missing are written — a title or description you added by hand while the batch was running is never clobbered.
  • Partial results are safe. Results are matched back to records by id, in any order. A record whose field succeeded is written and marked done. A record that failed transiently (rate limit, timeout, provider error, or an expired item) is left open, so running --batch again submits a fresh, smaller batch for exactly those. A record the provider rejected outright (content filter, invalid request) is recorded and not retried — a doomed record can't loop and re-bill.
  • Switching provider mid-batch is refused, not silently mis-handled. If you change SEO_PRO_AI_PROVIDER between submit and collect, the collect step stops with a clear message (switch back to collect, or --fresh to discard) instead of polling the wrong API. Run a single --batch submit at a time (don't fire two concurrent submits for the same models/field).
  • One knob: seo-pro.ai.fill.batch.request_timeout (default 120s, env SEO_PRO_AI_FILL_BATCH_TIMEOUT) — the HTTP timeout for the submit / poll / collect calls (its own, longer value than the short synchronous timeout, because a submit uploads every request and a collect streams the whole result file). Raise it for very large runs.

Schedule the collect

Because submit and collect are independent, a natural pattern is to submit from a deploy hook or a one-off command and let a scheduled seo-pro:ai-fill … --batch (every 15–30 min) poll and apply the moment the batch finishes — no long-running process babysitting the run.

How replies are handled

Every call returns one provider-neutral envelope, so the behaviour is the same across providers (and across the ones added later):

  • Structured output where the provider supports it. OpenAI (native Structured Outputs), Google (Gemini responseSchema), and Anthropic (output_config.format) all have the JSON shape enforced by the API — a reply that is not valid JSON is a clean failure, never text-scraped. A local / OpenAI-compatible server is asked for it too (response_format), best-effort: a server that ignores the field still returns usable text, which is tolerantly parsed as the fallback. Either way you get a clean list or a clear failure — never a half-parsed reply.
  • Truncation is an explicit, actionable error. If a reply is cut off at the output-token cap, you get a truncated error that says to raise seo-pro.ai.max_output_tokens — not a silently shortened title. This is most common with reasoning / thinking models; those models get the higher reasoning_min_output_tokens floor automatically.
  • Transient failures are retried automatically. A 429 rate limit or a 5xx is retried with capped exponential backoff, honoring a Retry-After header when present (bounded, so a hostile value can't stall the request). Deterministic failures are not retried — a bad key, a malformed request, an oversized payload, a timeout, or an out-of-credit / quota account (retrying an unfunded account only burns backoff). Tune or disable retrying via the retry block; set max_attempts to 0 to turn it off.
  • Errors are typed and sanitized. Each failure carries a stable code (unauthorized, quota_exceeded, rate_limited, timeout, content_too_large, bad_request, truncated, content_filtered, provider_error, …) and a retryable flag for the transient ones. The message is a short, sanitized string — the raw provider response body is never surfaced or logged. In Filament a failure renders in the styled modal partial with a tailored next-step hint for the common modes (see Troubleshooting).

Troubleshooting

Every failure is inline and non-fatal, with a typed code and a sanitized message. The common ones and their one real fix:

Symptom (error code)What it meansFix
quota_exceeded"the provider account is out of credit or quota"The key is valid but the API account has no credit / quota. Not a rate limit — retrying won't help. Distinct per provider: Anthropic returns "credit balance is too low", OpenAI "exceeded your current quota… check your plan and billing" (insufficient_quota), Google "prepayment credits are depleted".Add credit / enable billing in the provider's console — or switch to a local model for $0. Remember a Claude/ChatGPT subscription does not fund the API.
unauthorized"authentication failed"The key is missing, wrong, or not valid for the configured provider.Check the key in the env var named by seo-pro.ai.api_key_env (default SEO_PRO_AI_API_KEY) — set, current, and matching SEO_PRO_AI_PROVIDER.
rate_limited"the provider rate limit was reached"A genuine, transient rate limit (retried automatically first).Wait and retry, or move to a local model ($0, no limits) to avoid it. Raise seo-pro.ai.fill.throttle_ms for bulk runs on a low tier.
timeout"the request timed out"The provider didn't respond within seo-pro.ai.timeout (default 15s). Common with a slow local reasoning model.Raise it with SEO_PRO_AI_TIMEOUT; for Ollama also set ['think' => false] in seo-pro.ai.local.extra_body.
truncated"hit the max_output_tokens limit"A thinking model spent the budget on hidden reasoning before finishing.Raise seo-pro.ai.max_output_tokens (reasoning models can need 2000+), or confirm the model matches a reasoning_models pattern so the floor applies.
content_too_large (HTTP 413)The page content sent exceeded the provider limit.Lower seo-pro.ai.max_input_chars to send a shorter excerpt.
bad_requestA malformed request — usually a model name the account can't access, or an unsupported parameter.Check SEO_PRO_AI_MODEL is a model your key/server can reach for the configured provider.
content_filteredThe provider's safety filter declined to answer.Rare for SEO copy; try a different provider or model.

The fastest way out of a paid-provider problem is a local one

A missing key, an exhausted account, or a rate limit all disappear on SEO_PRO_AI_PROVIDER=local against Ollama — genuinely $0, no rate limits, and the surfaces behave identically. (A billing-enabled Google key is the cheapest cloud fallback, but its free tier will rate-limit you.)

What leaves your server

Exactly this, only to your configured provider, only on explicit action (a clicked action or an invoked command):

  • Suggestions: the model's class basename and key (e.g. "Post #3"), the currently resolved title and description, the canonical URL, and a plain-text content excerpt (HTML stripped) capped at max_input_chars (default 6000 characters).
  • Issue explanations: the issue's type, severity, field, message, and target URL, plus the affected model's resolved title/description.
  • Description rewrite: the same minimal page context as a suggestion, plus — when given — the scan issue's type and message.
  • Structured-data suggestion: the same minimal page context as a suggestion. The model returns only a type and leaf field values; the JSON-LD is assembled locally.

Never: visitor data, IP addresses, request headers, credentials, or full HTML. The Pro repository's SECURITY.md carries the authoritative version of this list.

rankbeam/laravel-seo is released under the MIT License.