{"id":50744516,"url":"https://github.com/b13/aim","last_synced_at":"2026-06-10T19:32:10.765Z","repository":{"id":351276765,"uuid":"1209911701","full_name":"b13/aim","owner":"b13","description":"Intelligent AI proxy for TYPO3","archived":false,"fork":false,"pushed_at":"2026-05-26T08:08:09.000Z","size":864,"stargazers_count":18,"open_issues_count":2,"forks_count":6,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-26T09:36:07.409Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/b13.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-13T22:55:22.000Z","updated_at":"2026-05-26T08:08:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/b13/aim","commit_stats":null,"previous_names":["b13/aim"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/b13/aim","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/b13%2Faim","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/b13%2Faim/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/b13%2Faim/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/b13%2Faim/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/b13","download_url":"https://codeload.github.com/b13/aim/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/b13%2Faim/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34168086,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-10T19:32:09.910Z","updated_at":"2026-06-10T19:32:10.743Z","avatar_url":"https://github.com/b13.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AiM - Intelligent AI Proxy for TYPO3\n\nAiM is the central AI layer for TYPO3. Extensions describe what they need. AiM decides which provider and model to use, routes through a middleware pipeline, and returns the result. Built for TYPO3 v12, v13, and v14.\n\n\u003e **New to AiM?** Read the [Introduction](Documentation/Introduction.md) for a non-technical overview of what AiM does, why it exists, and how it works for administrators and extension developers.\n\n\u003e **Alpha state.** AiM is under active development. The API is functional but may change before 1.0. We'd love your feedback: [open an issue](https://github.com/b13/aim/issues) or reach out at [b13.com](https://b13.com).\n\n![AiM Request Log](Documentation/Images/request-log.png)\n\n## Quick start\n\n```php\nuse B13\\Aim\\Ai;\n\npublic function __construct(private readonly Ai $ai) {}\n\n$response = $this-\u003eai-\u003evision(\n    imageData: base64_encode($fileContent),\n    mimeType: 'image/jpeg',\n    prompt: 'Generate alt text for this image',\n    extensionKey: 'my_extension',\n);\necho $response-\u003econtent; // \"A golden retriever playing fetch in a sunny park\"\n```\n\nA few lines to add AI to any TYPO3 extension. No API keys in your code, no provider lock-in, full logging and cost tracking out of the box.\n\n## Key features\n\n**For extension developers:**\n- Simple proxy API (`$ai-\u003evision()`, `$ai-\u003etext()`, `$ai-\u003etranslate()`, `$ai-\u003eembed()`)\n- Fluent builder for advanced parameters\n- Direct pipeline access for full control\n- Structured output (JSON Schema), tool calling, streaming\n\n**For administrators:**\n- Backend modules for provider management and request monitoring\n- Disable specific models per provider via clickable badges\n- Budget limits and rate limiting per user (including admins as a safety net)\n- Privacy levels (standard / reduced / none) per provider\n- Provider group restrictions and capability permissions via native TYPO3 mechanisms\n- LLM grading: score response quality with a second model acting as a judge\n\n**Under the hood:**\n- Zero provider dependencies. Install Symfony AI bridge packages as needed.\n- Auto-discovery of installed bridges (OpenAI, Anthropic, Gemini, Mistral, Ollama, etc.)\n- Capability-based routing with model-level awareness\n- Auto model switch: one config covers all capabilities\n- Smart routing: routes simple prompts to cheaper models based on historical cost, reliability, and (with grading) quality data\n- Fallback chains: automatic retry with alternative providers on failure\n- 9-layer middleware pipeline: retry, access control, smart routing, capability validation, grading, logging, cost tracking, events, dispatch\n\n## Installation\n\n```bash\ncomposer require b13/aim\n```\n\nAiM has **zero AI provider dependencies**. Install provider bridges as needed:\n\n```bash\n# For OpenAI\ncomposer require symfony/ai-open-ai-platform\n\n# For local models via Ollama\ncomposer require symfony/ai-ollama-platform\n\n# For Anthropic, Gemini, Mistral, etc.\ncomposer require symfony/ai-anthropic-platform\ncomposer require symfony/ai-gemini-platform\ncomposer require symfony/ai-mistral-platform\n```\n\nAny installed `symfony/ai-*-platform` package is **auto-discovered** at container compile time. Models, capabilities, and features are read from the bridge's `ModelCatalog` automatically.\n\nAfter installation, create a provider configuration in the backend (Admin Tools \u003e AiM \u003e Providers) with your API key and preferred model.\n\n\u003e **Local providers (Ollama, LM Studio):** The *API Key* field doubles as the endpoint URL. Enter `http://localhost:11434` (Ollama) or `http://localhost:1234` (LM Studio) instead of a key. The available models are then fetched live from that endpoint.\n\n## Trying AiM from the command line\n\nOnce a provider configuration exists, you can fire requests without writing an extension first. The `aim:test` command sends a one-off request through the full pipeline and reports the response, model used, token usage, cost, timing, and whether a request-log row was written:\n\n```bash\n# Text generation (default capability)\nvendor/bin/typo3 aim:test text --prompt \"Write a haiku about TYPO3\"\n\n# Conversation, against a specific provider\nvendor/bin/typo3 aim:test conversation -p \"anthropic:*\" --prompt \"Explain dependency injection\"\n\n# Translation\nvendor/bin/typo3 aim:test translate --prompt \"Hello world\" --from English --to German\n\n# Embeddings\nvendor/bin/typo3 aim:test embed --prompt \"TYPO3 is an open-source CMS\"\n```\n\nThe capability is a positional argument (`text`, `conversation`, `translate`, or `embed`; defaults to `text`). Options:\n\n| Option | Purpose |\n|---|---|\n| `--prompt` | The prompt / text to send |\n| `--provider` / `-p` | Provider notation (`openai:gpt-4o`, `anthropic:*`); defaults to the configured default |\n| `--site` | Resolve the provider from a site's `settings.yaml` instead of the database; takes precedence over `--provider` |\n| `--system-prompt` | Optional system prompt |\n| `--max-tokens` | Token limit for the response |\n| `--from` / `--to` | Source / target language (translate only) |\n\nBecause it runs through the real pipeline, every call also lands in the request log. A quick way to see logging, cost tracking, smart routing, and grading in action before integrating the API into your own code.\n\n## Usage\n\n### Tier 1: Proxy (recommended)\n\nThe simplest way. Extensions never see providers, configurations, or API keys:\n\n```php\nuse B13\\Aim\\Ai;\n\npublic function __construct(\n    private readonly Ai $ai,\n) {}\n\n// Vision (e.g. alt text generation)\n$response = $this-\u003eai-\u003evision(\n    imageData: base64_encode($fileContent),\n    mimeType: 'image/jpeg',\n    prompt: 'Generate alt text for this image',\n    extensionKey: 'my_extension',\n);\necho $response-\u003econtent;\n\n// Text generation\n$response = $this-\u003eai-\u003etext(\n    prompt: 'Write a meta description for a bakery website.',\n    maxTokens: 160,\n    extensionKey: 'my_extension',\n);\n\n// Translation\n\n$response = $this-\u003eai-\u003etranslate(\n    text: 'Hello world',\n    sourceLanguage: 'English',\n    targetLanguage: 'German',\n    extensionKey: 'my_extension',\n);\n\n// Conversation\n$response = $this-\u003eai-\u003econversation(\n    messages: [new UserMessage('What is TYPO3?')],\n    systemPrompt: 'You are a CMS expert.',\n    extensionKey: 'my_extension',\n);\n\n// Embeddings\n$response = $this-\u003eai-\u003eembed(\n    input: 'TYPO3 is an open-source CMS',\n    dimensions: 256,\n    extensionKey: 'my_extension',\n);\n```\n\n#### Provider preference\n\nExtensions can request a specific provider without hardcoding configuration UIDs:\n\n```php\n// Use OpenAI, admin picks the model\n$response = $this-\u003eai-\u003etext(\n    prompt: 'Summarize this.',\n    provider: 'openai:*',\n    extensionKey: 'my_extension',\n);\n\n// Use a specific model\n$response = $this-\u003eai-\u003evision(\n    imageData: $data,\n    mimeType: 'image/jpeg',\n    prompt: 'Describe this image',\n    provider: 'openai:gpt-4.1',\n    extensionKey: 'my_extension',\n);\n```\n\nIf the requested provider is unavailable, AiM falls back to the default with a logged warning.\n\n### Tier 2: Fluent Builder\n\nMore control over parameters, still provider-agnostic:\n\n```php\n$response = $this-\u003eai-\u003erequest()\n    -\u003evision($imageData, 'image/jpeg')\n    -\u003eprompt('Generate alt text for this image')\n    -\u003esystemPrompt('You are an accessibility expert.')\n    -\u003emaxTokens(100)\n    -\u003etemperature(0.3)\n    -\u003eprovider('openai:*')\n    -\u003efrom('my_extension')\n    -\u003esend();\n```\n\n### Tier 3: Direct pipeline access\n\nFull control. You choose the provider, build the request, and dispatch through the pipeline:\n\n```php\nuse B13\\Aim\\Capability\\TextGenerationCapableInterface;\nuse B13\\Aim\\Middleware\\AiMiddlewarePipeline;\nuse B13\\Aim\\Provider\\ProviderResolver;\nuse B13\\Aim\\Request\\TextGenerationRequest;\n\n$resolvedProvider = $this-\u003eproviderResolver-\u003eresolveForCapability(\n    TextGenerationCapableInterface::class\n);\n\n$request = new TextGenerationRequest(\n    configuration: $resolvedProvider-\u003econfiguration,\n    prompt: 'Write a meta description for a bakery website.',\n    maxTokens: 160,\n    metadata: ['extension' =\u003e 'my_extension'],\n);\n\n$response = $this-\u003epipeline-\u003edispatch($request, $resolvedProvider);\n```\n\nAll three tiers flow through the same middleware chain: Logging, governance, cost tracking, and events always fire regardless of how the request was initiated.\n\n### Structured output (JSON Schema)\n\n```php\nuse B13\\Aim\\Request\\ResponseFormat;\n\n$response = $this-\u003eai-\u003etext(\n    prompt: 'Extract the product name and price from: \"The MacBook Pro costs $2449.99\"',\n    responseFormat: ResponseFormat::jsonSchema('product', [\n        'type' =\u003e 'object',\n        'properties' =\u003e [\n            'name' =\u003e ['type' =\u003e 'string'],\n            'price' =\u003e ['type' =\u003e 'number'],\n        ],\n        'required' =\u003e ['name', 'price'],\n        'additionalProperties' =\u003e false,\n    ]),\n    extensionKey: 'my_extension',\n);\n$data = json_decode($response-\u003econtent, true);\n```\n\n### Tool calling\n\n```php\nuse B13\\Aim\\Request\\ToolCallingRequest;\nuse B13\\Aim\\Request\\ToolDefinition;\nuse B13\\Aim\\Request\\Message\\UserMessage;\n\n$request = new ToolCallingRequest(\n    configuration: $resolvedProvider-\u003econfiguration,\n    messages: [new UserMessage('What is the weather in Berlin?')],\n    tools: [\n        new ToolDefinition(\n            name: 'get_weather',\n            description: 'Get current weather for a city',\n            parameters: [\n                'type' =\u003e 'object',\n                'properties' =\u003e [\n                    'city' =\u003e ['type' =\u003e 'string', 'description' =\u003e 'City name'],\n                ],\n                'required' =\u003e ['city'],\n                'additionalProperties' =\u003e false,\n            ],\n            strict: true,\n        ),\n    ],\n);\n\n$response = $this-\u003epipeline-\u003edispatch($request, $resolvedProvider);\nif ($response-\u003erequiresToolExecution()) {\n    foreach ($response-\u003etoolCalls as $toolCall) {\n        // $toolCall-\u003ename, $toolCall-\u003egetDecodedArguments()\n    }\n}\n```\n\n## Capabilities\n\nEach provider implements one or more capability interfaces:\n\n| Interface | Request | Response | Use Case |\n|---|---|---|---|\n| `VisionCapableInterface` | `VisionRequest` | `TextResponse` | Image analysis, alt text generation |\n| `ConversationCapableInterface` | `ConversationRequest` | `ConversationResponse` | Conversations, chatbots, multi-turn dialogs |\n| `TextGenerationCapableInterface` | `TextGenerationRequest` | `TextResponse` | Content generation, summaries |\n| `TranslationCapableInterface` | `TranslationRequest` | `TextResponse` | Text translation |\n| `ToolCallingCapableInterface` | `ToolCallingRequest` | `ToolCallingResponse` | Agentic workflows, function calling |\n| `EmbeddingCapableInterface` | `EmbeddingRequest` | `EmbeddingResponse` | Vector embeddings, semantic search, RAG |\n\n### Model-level capabilities\n\nProviders can declare per-model capabilities via `modelCapabilities`. Models listed get only the specified capabilities. Unlisted models inherit all provider capabilities except specialized ones (e.g. embedding-only models).\n\n```php\n#[AsAiProvider(\n    identifier: 'openai',\n    supportedModels: ['gpt-4o' =\u003e 'GPT-4o', 'text-embedding-3-small' =\u003e 'Embeddings'],\n    modelCapabilities: [\n        'text-embedding-3-small' =\u003e [EmbeddingCapableInterface::class],\n        // gpt-4o inherits all capabilities EXCEPT embedding\n    ],\n)]\n```\n\n### Auto model switch\n\nWhen a provider config has `gpt-4o` but an embedding request comes in, AiM automatically switches to the cheapest capable model (e.g. `text-embedding-3-small`) using the same API key. The selection is data-driven: if historical cost data exists in the request log, AiM picks the cheapest model with a good success rate. Otherwise it falls back to the most specialized model.\n\nThe switch is:\n- **Logged** with `model_requested`, `model_used`, and reroute reason\n- **Controllable** at three levels:\n\n| Level | Setting | Default |\n|---|---|---|\n| Per config | `auto_model_switch` toggle in TCA | On |\n| Per user/group | `aim.autoModelSwitch = 0` in TSconfig | On |\n| Admin | Always allowed | - |\n\n## Registering a Custom Provider\n\nAny extension can add AI providers. Create a class implementing `AiProviderInterface` plus any capability interfaces, and annotate it with `#[AsAiProvider]`:\n\n```php\nuse B13\\Aim\\Attribute\\AsAiProvider;\nuse B13\\Aim\\Capability\\TextGenerationCapableInterface;\nuse B13\\Aim\\Capability\\VisionCapableInterface;\nuse B13\\Aim\\Provider\\AiProviderInterface;\n\n#[AsAiProvider(\n    identifier: 'my-provider',\n    name: 'My AI Provider',\n    description: 'Custom provider for my use case',\n    supportedModels: [\n        'my-model-v1' =\u003e 'My Model v1',\n        'my-model-v2' =\u003e 'My Model v2',\n    ],\n    features: [\n        'supportsStructuredOutput' =\u003e true,\n        'supportsStreaming' =\u003e true,\n        'maxContextWindow' =\u003e 128000,\n    ],\n)]\nclass MyProvider implements AiProviderInterface, TextGenerationCapableInterface, VisionCapableInterface\n{\n    public function processTextGenerationRequest(TextGenerationRequest $request): TextResponse { ... }\n    public function processVisionRequest(VisionRequest $request): TextResponse { ... }\n}\n```\n\nThe provider is auto-discovered via the PHP attribute. No manual registration needed.\n\n## Symfony AI Integration\n\nAiM auto-discovers any installed Symfony AI bridge package (`symfony/ai-*-platform`). For each bridge:\n\n1. Reads the PSR-4 namespace from the package's `composer.json`\n2. Instantiates the bridge's `ModelCatalog` to read models and per-model capabilities\n3. Maps Symfony AI `Capability` enums to AiM capability interfaces\n4. Sanitizes model names for TCA compatibility (no colons)\n5. Detects the factory authentication parameter via reflection (`apiKey` vs `endpoint`)\n6. Registers a `SymfonyAiPlatformAdapter` as an AiM provider\n\nInstall a bridge, flush caches. The provider appears automatically in the backend module with all its models.\n\n## Governance \u0026 Access Control\n\nAiM provides a complete governance system for AI usage, built on native TYPO3 mechanisms.\n\n### API key encryption\n\nProvider API keys stored in `tx_aim_configuration.api_key` are encrypted using a key derived from `$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']`.\n\n| TYPO3 version | Cipher | Implementation |\n|---|---|---|\n| v14+ | XChaCha20-Poly1305 AEAD | Core `\\TYPO3\\CMS\\Core\\Crypto\\Cipher\\CipherService` |\n| v12 / v13 | XSalsa20-Poly1305 secretbox | Local libsodium implementation (CipherService not yet available) |\n\nStored values carry a version prefix (`aim:enc:v1:` for the v12/v13 path, `aim:enc:v2:` for the v14 path) so decryption auto-selects the right routine even after an upgrade. Encryption is transparent: a DataHandler hook encrypts on save, a FormDataProvider decrypts for the backend edit form, and the repository decrypts on read. Legacy plaintext rows from earlier AiM versions are migrated via the **\"[AiM] Encrypt stored provider API keys\"** upgrade wizard in the Install Tool.\n\nFor providers that put an **endpoint URL** in the `api_key` field instead of a real secret (Ollama, LM Studio, self-hosted OpenAI-compatible proxies), AiM detects the `http://` / `https://` prefix and skips encryption — the URL stays plaintext both in the column and in DB exports.\n\nIf `SYS/encryptionKey` is rotated, existing API keys can no longer be decrypted with the new key. Run the rotation command *before* the rotation takes effect, or right after with the old value still in hand:\n\n```bash\nvendor/bin/typo3 aim:rotateApiKeys --old-key='\u003cprevious SYS/encryptionKey value\u003e'\n```\n\nThe command decrypts each stored key with the supplied old value, re-encrypts with the current one, and reports the result. It is idempotent (re-running with the same old key is a no-op) and aborts without writes if any row cannot be decrypted with the supplied value. Add `--dry-run` to preview.\n\nWithout the previous key value, encrypted API keys cannot be recovered. This is by design. Save the old `SYS/encryptionKey` somewhere safe before rotating.\n\n### Provider restrictions\n\nRestrict provider configurations to specific backend user groups via the `be_groups` field on each configuration record. Only members of the listed groups (or admins) can use that configuration.\n\n### Capability permissions\n\nRegister AiM capability permissions in backend user groups (Access \u003e Custom Options):\n\n- `aim:capability_text`: Text generation\n- `aim:capability_vision`: Vision requests\n- `aim:capability_translation`: Translations\n- `aim:capability_conversation`: Conversations\n- `aim:capability_embedding`: Embeddings\n- `aim:capability_toolcalling`: Tool calling\n\n**Permissive by default**: if no AiM permissions are configured in any group, all capabilities are allowed. Once any `aim:` permission is set, only explicitly granted capabilities are allowed.\n\n### Budget limits (UserTSconfig)\n\n```typoscript\naim {\n  budget {\n    period = monthly\n    maxCost = 50.00\n    maxTokens = 500000\n    maxRequests = 1000\n  }\n  rateLimit {\n    requestsPerMinute = 10\n  }\n}\n```\n\nBudgets are tracked per user in rolling periods (daily/weekly/monthly) in `tx_aim_usage_budget`. When exceeded, requests are blocked with a clear error message.\n\n**Budgets and rate limits apply to all users, including admins.** Admins skip provider group restrictions and capability permissions, but budgets and rate limits act as a safety net against accidental cost overruns. An admin can set their own limits via UserTSconfig and will be blocked when exceeded.\n\n### Privacy levels\n\nEach provider configuration has a privacy level:\n\n| Level | Behavior |\n|---|---|\n| `standard` | Full logging: prompt, response, tokens, cost |\n| `reduced` | Metadata only: tokens, cost, model, duration. No prompt/response content |\n| `none` | No logging at all |\n\nUsers can escalate (but never downgrade) the privacy level via TSconfig:\n\n```typoscript\naim.privacyLevel = reduced\n```\n\nThe strictest level between the config and the user always wins.\n\n### Rerouting protection\n\nSet `rerouting_allowed = 0` on a provider configuration to prevent the smart router from rerouting requests away from or to that configuration. Combined with `be_groups`, this ensures confidential data (e.g. HR data on a local Ollama) stays on the designated model.\n\n## Smart Routing\n\nThe `SmartRoutingMiddleware` classifies prompt complexity using language-agnostic structural heuristics:\n\n- Character/sentence/line count\n- Question marks, enumerations, code presence\n- URLs, structural delimiters\n- Multi-language keyword signals (extensible per extension)\n\nClassification is logged per request (`complexity_score`, `complexity_label`, `complexity_reason`). When a cheaper model has proven reliable for simple prompts (based on historical request log data with minimum 10 requests and 90%+ success rate), the middleware automatically downgrades.\n\n### Quality gate\n\n\"Reliable\" on its own only means *the API call didn't error*. A cheap model can succeed every time while producing weak answers. When [LLM grading](#llm-grading) is enabled, smart routing also consults the recorded `grade_score`: a cheaper model is only chosen if its graded responses for that request type average at least **0.65** (the \"good\" boundary) across at least **10 graded requests**.\n\nThe gate is a one-way veto, not a tie-breaker. The cheapest cost-and-success-eligible model is still the one picked; a poor average grade simply removes a candidate. Crucially, **too few graded requests means \"no signal\", not \"bad\"**: a model with fewer than 10 graded samples is judged on cost and success rate exactly as before, so installs without grading enabled see no change in routing behavior.\n\nThe downgrade decision is logged with the candidate's graded quality, e.g. `... (avg grade: 0.82 over 14 graded)` or `... (ungraded)`.\n\n### Extending complexity signals\n\nShip a `Configuration/SmartRouting/ComplexitySignals.php` in any extension:\n\n```php\nreturn [\n    'ja' =\u003e [\n        'complex' =\u003e ['比較して', '設計して', '最適化して'],\n        'simple' =\u003e ['とは', 'こんにちは'],\n        'multiPart' =\u003e [' と比べて'],\n    ],\n];\n```\n\nOr add signals at runtime:\n\n```php\n$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['aim']['complexitySignals']['de']['complex'][] = 'analysiere';\n```\n\n## LLM Grading\n\nAiM can score the quality of AI responses using a second model as a judge (\"LLM-as-a-judge\"). Grading is opt-in per provider configuration and runs *after* the response has been delivered to the caller, so it adds no latency to the live request.\n\n### Enabling grading\n\nOn any provider configuration (Admin Tools \u003e AiM \u003e Providers), open the **LLM Grading** tab:\n\n| Field | Purpose |\n|---|---|\n| `grading_enabled` | Turns grading on for this configuration |\n| `judge_configuration_uid` | A *different* AiM configuration used to score responses — typically a cheaper or specialized model that supports the conversation capability |\n| `grading_rubric` | The judge's instructions: what to evaluate (factual accuracy, relevance, tone, ...). The required JSON output format is appended automatically. |\n\nGrading covers `ConversationRequest` and `TextGenerationRequest`. It only runs when the effective privacy level is `standard`, `reduced` and `none` skip it, since the judge needs the prompt and response content.\n\n### How it runs\n\n1. After a successful, gradeable response, `GraderMiddleware` marks the request log row `grade_status = pending` and registers a shutdown function.\n2. The shutdown function runs *after* the response is flushed to the caller, then calls the judge model.\n3. The judge returns a JSON `{score, label, reason}`, written back to the row (`grade_score`, `grade_label`, `grade_reason`).\n\nIf the shutdown path is missed (CLI crash, an unusual SAPI), a scheduler command picks up the stragglers:\n\n```bash\nvendor/bin/typo3 aim:grade-pending\n```\n\nRun it from the TYPO3 scheduler every few minutes. It grades rows still marked `pending` that are older than `--min-age` seconds (default 60), so it never races the live shutdown handler. The request log module shows a warning when a pending backlog builds up.\n\n### Grades\n\nThe judge assigns one of four labels. When it returns a score but no recognizable label, the label is derived from the score:\n\n| Label | Score range |\n|---|---|\n| `poor` | 0.00–0.39 |\n| `fair` | 0.40–0.64 |\n| `good` | 0.65–0.84 |\n| `excellent` | 0.85–1.00 |\n\nThe judge call deliberately bypasses the middleware pipeline (it would otherwise produce a duplicate request-log row), but its cost is still rolled into the judge configuration's `total_cost` and recorded on the graded row's `judge_cost` column.\n\n## Custom Middleware\n\nAdd middleware to intercept all AI requests:\n\n```php\nuse B13\\Aim\\Attribute\\AsAiMiddleware;\nuse B13\\Aim\\Middleware\\AiMiddlewareInterface;\n\n#[AsAiMiddleware(priority: 50)]\nclass MyMiddleware implements AiMiddlewareInterface\n{\n    public function process(\n        AiRequestInterface $request,\n        AiProviderInterface $provider,\n        ProviderConfiguration $configuration,\n        AiMiddlewareHandler $next,\n    ): TextResponse {\n        // Before: inspect or modify request\n        $response = $next-\u003ehandle($request, $provider, $configuration);\n        // After: inspect or modify response\n        return $response;\n    }\n}\n```\n\n### Enriching the request log\n\nEvery request DTO carries a `metadata` array that lands in the `metadata` JSON column of `tx_aim_request_log`. To attach extension-specific context, enrich it from your custom middleware via `$request-\u003ewithMetadata([...])` and forward the new instance. The original request stays immutable; downstream middlewares see the merged metadata:\n\n```php\n#[AsAiMiddleware(priority: 80)]\nfinal class MyExtensionContextMiddleware implements AiMiddlewareInterface\n{\n    public function process(\n        AiRequestInterface $request,\n        AiProviderInterface $provider,\n        ProviderConfiguration $configuration,\n        AiMiddlewareHandler $next,\n    ): TextResponse {\n        $request = $request-\u003ewithMetadata([\n            'my_ext.additional' =\u003e 'info',\n        ]);\n        return $next-\u003ehandle($request, $provider, $configuration);\n    }\n}\n```\n\n### Detailed / parallel logging\n\nFor richer or separate logging, register a middleware at a lower priority than `RequestLoggingMiddleware` (use a priority below `-700`). It sees the response, the resolved `$configuration`, and any metadata enriched by earlier middlewares, and is free to write wherever it likes without touching `tx_aim_request_log`:\n\n```php\n#[AsAiMiddleware(priority: -750)]\nfinal class MyExtensionDetailedLogger implements AiMiddlewareInterface\n{\n    public function __construct(private readonly MyExtensionLogRepository $repository) {}\n\n    public function process(\n        AiRequestInterface $request,\n        AiProviderInterface $provider,\n        ProviderConfiguration $configuration,\n        AiMiddlewareHandler $next,\n    ): TextResponse {\n        $response = $next-\u003ehandle($request, $provider, $configuration);\n        $this-\u003erepository-\u003erecord([\n            'provider' =\u003e $configuration-\u003eproviderIdentifier,\n            'model' =\u003e $response-\u003eusage-\u003emodelUsed,\n            'metadata' =\u003e $request-\u003emetadata,\n            'tokens' =\u003e $response-\u003eusage-\u003egetTotalTokens(),\n            'cost' =\u003e $response-\u003eusage-\u003ecost,\n            // ...any custom shape you need\n        ]);\n        return $response;\n    }\n}\n```\n\nThe middleware pipeline is intentionally the only logging extension point: it gives you the request, response, configuration, and middleware context in one place, plus full control over where the data goes.\n\n### Built-in Middleware\n\n| Middleware | Priority | Purpose |\n|---|---|---|\n| `RetryWithFallbackMiddleware` | 100 | Catches errors, retries with fallback providers |\n| `AccessControlMiddleware` | 90 | Provider access, capability permissions, budgets, rate limits |\n| `SmartRoutingMiddleware` | 75 | Complexity classification, cost-based model downgrade |\n| `CapabilityValidationMiddleware` | 50 | Validates provider capability, auto-reroutes if needed |\n| `GraderMiddleware` | -600 | Schedules LLM-as-a-judge grading after a successful response |\n| `RequestLoggingMiddleware` | -700 | Logs every request (respects privacy levels) |\n| `CostTrackingMiddleware` | -800 | Updates cumulative cost per configuration |\n| `EventDispatchMiddleware` | -900 | Fires `BeforeAiRequestEvent` / `AfterAiResponseEvent` |\n| `CoreDispatchMiddleware` | -1000 | Routes request to the correct provider capability method |\n\n## Events\n\n| Event | When | Use Case |\n|---|---|---|\n| `BeforeAiRequestEvent` | Before provider call | Modify request, add logging, enforce policies |\n| `AfterAiResponseEvent` | After provider response | Post-processing, notifications, analytics |\n| `AiRequestReroutedEvent` | When capability gate reroutes | Monitor misconfigurations, track rerouting patterns |\n\n## Backend Modules\n\nAiM adds an **AiM** module under Admin Tools with two sub-modules:\n\n### Providers\n\nManage AI provider configurations:\n- API keys, models, token costs\n- Group restrictions (`be_groups`), privacy levels, rerouting protection, auto model switch\n- **Available Providers**: modal with clickable model badges to enable/disable models\n- **Provider verification**: test connectivity with a minimal probe request, results persisted\n- **Last used**: timestamp per configuration with link to request log\n\n![Provider Management](Documentation/Images/provider-management.png)\n\n### Request Log\n\nMonitor all AI requests:\n- **Statistics dashboard**: total requests, total cost, total tokens, success rate, average duration\n- **Filtered log view**: filter by provider, extension, request type, success/failure\n- **User tracking**: shows the backend username for each request (empty for CLI/automation)\n- **Full content**: prompt, system prompt, and response content per request (respects privacy levels)\n- **Complexity classification**: score, label, and reason for each request\n- **Quality grades**: LLM-as-a-judge score, label, and reason per request when grading is enabled\n- **Token details**: prompt, completion, cached, and reasoning token breakdowns\n- **Rerouting info**: fallback and capability rerouting details\n\n## Dashboard Widgets\n\nWhen `typo3/cms-dashboard` is installed, AiM registers five widgets and a pre-configured dashboard preset (\"AiM: AI Analytics\"):\n\n| Widget | Type | Shows |\n|---|---|---|\n| Recent Requests | Table | Last 10 requests with extension, model, tokens, cost, status |\n| Provider Usage | Doughnut chart | Request distribution across providers |\n| Model Usage | Bar chart | Request count per model |\n| Success Rate | Doughnut chart | Successful vs failed requests |\n| Extension Usage | Doughnut chart | Which extensions generate the most requests |\n\nAll widgets are refreshable and grouped under \"AiM\" in the widget picker. The recent requests widget includes a button to open the full request log module.\n\n![Dashboard Widgets](Documentation/Images/dashboard-widgets.png)\n\n## Database Tables\n\n| Table | Purpose |\n|---|---|\n| `tx_aim_configuration` | Provider configurations (TCA-managed). API keys, models, cost tracking, governance settings. |\n| `tx_aim_request_log` | Per-request log (no TCA). Tokens, cost, duration, prompt/response content, complexity classification, rerouting details, LLM grading results. |\n| `tx_aim_usage_budget` | Per-user budget tracking. Rolling period counters for tokens, cost, and request count. |\n\nSee `ext_tables.sql` for the full schema.\n\n## Testing\n\n```bash\ncd typo3conf/ext/aim\n\n# Unit tests (30 tests, 54 assertions)\nBuild/Scripts/runTests.sh -s unit\n\n# Functional tests (24 tests, 57 assertions)\nBuild/Scripts/runTests.sh -s functional\n\n# With specific PHP version\nBuild/Scripts/runTests.sh -s unit -p 8.3\n\n# Specific test\nBuild/Scripts/runTests.sh -s unit -- --filter BudgetService\n```\n\n## Requirements\n\n- TYPO3 v12.4, v13.4, or v14.0+\n- PHP 8.1+\n- No AI provider dependencies (bring your own via Symfony AI bridges or native implementations)\n\n## License\n\nGPL-2.0-or-later\n\n## Credits\n\nCreated by [Oli Bartsch](https://github.com/o-ba) for [b13 GmbH, Stuttgart](https://b13.com).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fb13%2Faim","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fb13%2Faim","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fb13%2Faim/lists"}