> ## Documentation Index
> Fetch the complete documentation index at: https://docs.theaitracker.com/llms.txt
> Use this file to discover all available pages before exploring further.

# AI providers

> Provider configuration, model defaults, rate limits and cost tracking.

All provider configuration lives in `config/ai.php`. Each provider has one class in
`app/Services/LLM`, and each entry in the config carries a base URL, a default model, a
`batch_supported` flag and a model catalogue with cost metadata.

## Providers

| Provider   | Key                  | Base URL                                   | Batch                 |
| ---------- | -------------------- | ------------------------------------------ | --------------------- |
| OpenAI     | `OPENAI_API_KEY`     | `api.openai.com/v1`                        | Disabled deliberately |
| Gemini     | `GOOGLE_AI_API_KEY`  | `generativelanguage.googleapis.com/v1beta` | No native batch API   |
| Claude     | `ANTHROPIC_API_KEY`  | `api.anthropic.com`                        | Supported             |
| Perplexity | `PERPLEXITY_API_KEY` | `api.perplexity.ai`                        | No                    |
| Grok (xAI) | `GROK_API_KEY`       | `api.x.ai/v1`                              | No                    |
| SerpApi    | `SERPAPI_API_KEY`    | `serpapi.com`                              | No                    |

Claude is the only provider with `batch_supported => true`. OpenAI's is set to `false` with a
comment noting batch was turned off in favour of realtime processing — so enabling
`AI_BATCH_ENABLED` does not change OpenAI's behaviour.

<Note>
  SerpApi is not an LLM — it is how the app reads Google's **AI Overview** and **AI Mode**
  surfaces, billed per search (`cost_per_search` of `0.01` for both) rather than per token.
</Note>

Semrush also sits in the providers list (`SEMRUSH_API_KEY`, `SEMRUSH_BASE_URL`) but is a
keyword-data source, not an LLM. The Suggest Prompts flow falls back to it when a campaign has
no Search Console connection. Neither variable is in `.env.example` — add them by hand if you
need that fallback path.

## Model defaults

```ini theme={null}
AI_DEFAULT_PROVIDER=openai
AI_DEFAULT_OPENAI_MODEL=gpt-4o-search-preview
AI_DEFAULT_GEMINI_MODEL=gemini-2.5-flash
AI_DEFAULT_CLAUDE_MODEL=claude-sonnet-5
AI_DEFAULT_PERPLEXITY_MODEL=sonar
AI_DEFAULT_GROK_MODEL=grok-3
ONBOARDING_MODEL=gemini-3.1-flash-lite
```

<Warning>
  The Grok default differs by source: `config/ai.php` falls back to `grok-4`, while
  `.env.example` ships `grok-3`. Whichever is in your `.env` wins, so set it explicitly rather
  than relying on the fallback.
</Warning>

`ONBOARDING_MODEL` is separate on purpose. The whole setup wizard — brand name, business
description, topics, prompts, variants — runs on the cheapest fast Gemini model so onboarding
stays snappy and does not consume the OpenAI quota.

Adding a model means adding it to that provider's `models` array with a `name`,
`context_window` and `cost_per_1k_tokens`. The cost figure is what `TokenUsageTracker` uses to
derive spend, so an omitted or stale value silently produces wrong cost reporting.

## Rate limits

Configured per provider under `rate_limits`, as requests and tokens per minute:

| Provider   | Requests/min | Tokens/min |
| ---------- | ------------ | ---------- |
| OpenAI     | 500          | 30,000     |
| Gemini     | 300          | 20,000     |
| Claude     | 200          | 15,000     |
| Grok       | 200          | 15,000     |
| Perplexity | 100          | 10,000     |
| SerpApi    | 60           | —          |

These are the app's own ceilings, not the providers' published limits. Raise them only after
confirming your account's actual quota.

## Batch settings

```php theme={null}
'batch' => [
    'enabled'                => env('AI_BATCH_ENABLED', true),
    'max_requests_per_batch' => env('AI_BATCH_MAX_REQUESTS_PER_BATCH', 1000),
    'timeout_hours'          => env('AI_BATCH_TIMEOUT_HOURS', 24),
    'check_interval_minutes' => 60,
    'retry_attempts'         => 3,
],
```

`check_interval_minutes` and `retry_attempts` are hardcoded — change them in
`config/ai.php` rather than looking for an environment variable.

## Prompt configuration

| Setting           | Value                                | Meaning                                |
| ----------------- | ------------------------------------ | -------------------------------------- |
| `active_limit`    | `PROMPTS_ACTIVE_LIMIT`, default `10` | Active prompts per campaign            |
| `auto_seed`       | `PROMPTS_AUTO_SEED`, default `true`  | Seed starter prompts for new campaigns |
| `max_text_length` | `2000`                               | Maximum prompt length in characters    |

Ten default prompt categories drive generation: `alternatives`, `comparison`, `reviews`,
`tutorials`, `pricing`, `updates`, `troubleshooting`, `integrations`, `plans` and `security`.

## Visibility detection

The `visibility` block tunes how `VisibilityDetector` reads answers:

```php theme={null}
'visibility' => [
    'domain_prefixes_to_strip' => ['www.', 'm.', 'blog.', 'store.', 'amp.'],
    'url_extraction_pattern'   => '/https?:\/\/[^\s\)\]\}>\"]+/i',
    'mention_word_boundary'    => true,
    'case_sensitive_mentions'  => false,
],
```

Mention matching is case-insensitive and respects word boundaries, so a brand name will not
match inside a longer word. Prefix stripping is what makes `blog.example.com` and
`www.example.com` count as the same domain.
