# AI assistant (M5)

An opt-in writing helper in the composer. Its suggestions only fill the editor: the person edits, saves and submits the post through the normal approval flow. Nothing in `app/Domain/Ai` can save or publish a post.

## Flow

`Editor::assist()` → `AiAssistant::generate()` runs these checks in order:

1. `ai.use` on the selected client (`ClientAccess`). Owner, admin, manager and creator have it; approver, client and viewer do not.
2. Workspace consent: `AiSettings` (tenant setting `ai`) must be enabled with the current `ai.consent_version`. Only `ai.settings.manage` (owner, admin) can enable it, and they must accept the data-processing notice. Bumping the version turns the assistant off until it is accepted again.
3. Provider ready: `AI_PROVIDER=anthropic` with `ANTHROPIC_API_KEY`, `fake` (local/testing only), or `none`. When the provider is not ready, the assistant shows as unavailable. It is never simulated in production.
4. Rate limit: 20 requests per minute per user.
5. Budget: the worst case (estimated input + `max_tokens` output) must fit the remaining `ai.credits.monthly` for the UTC month. A plan without AI credits is blocked.

Every attempt writes an `ai_requests` row: status (`succeeded`, `blocked`, `refused`, `failed`), template version, prompt hash, names of the client fields sent, tokens, credits and suggestions. Usage is aggregated in `ai_usage` (per month) and metered as `ai.credits` usage events.

Credits: `ceil((input + 5 × output tokens) / 1000)`, at least 1. Only successful requests are charged.

## What is sent

`AiContext` reads only the selected client: name, industry, brand voice, the first brand's guidelines and default hashtags, plus what the user typed (text, instructions, language) and the names of the target platforms. User text is passed through `Redactor` and a filter that masks credential-shaped strings (bearer tokens, `sk-…`, Meta/Google token prefixes, long opaque strings). Account ids, tokens, contacts, analytics and other clients are never read. Tests capture the exact payload to prove this.

## Prompts

Version 1 of each task (caption, variants, hashtags, translate, rewrite, ideas) is in `config/ai.php` and is installed into `ai_templates` on first use. Admins create new versions in Settings → AI. Saving a version deactivates the previous one, which is kept for audit, and each request records the version it used.

## Anthropic integration

`AnthropicProvider` uses the official PHP SDK (`anthropic-ai/sdk`) and passes the API key explicitly:

- Model `AI_MODEL` (default `claude-opus-5-5`; `claude-sonnet-5-5` is a cheaper option) with an explicit effort level (`AI_EFFORT`, default `medium`). Thinking is left at the model default (adaptive).
- Structured output (`output_config.format` JSON schema) for `{suggestions: [{text, platform, note}]}`.
- Server-side refusal fallback (`fallbacks: "default"`, beta `server-side-fallback-2026-07-01`). If the whole chain refuses, the request is logged as `refused` with no charge, and the user is asked to rephrase.
- SDK errors map to user-safe messages: rate limit, auth, bad request, connection, other. `max_tokens` truncation is reported, not shown as partial text.
