# LLM Integration Documentation ## Architecture ``` app/LLM/ ├── LLMProvider.php — Abstract base class (contract for all providers) ├── LLMManager.php — Factory: loads org config from DB, returns provider instance ├── svcOpenAI.php — OpenAI (fully implemented) ├── svcAnthropic.php — Anthropic/Claude (fully implemented) ├── svcGemini.php — Google Gemini (fully implemented) ├── svcMistral.php — Mistral AI (fully implemented) └── svcDeepSeek.php — DeepSeek (fully implemented) ``` ## Database Storage — sp_orgs_meta LLM configs are stored per-org in `sp_orgs_meta`: | column | description | |------------|------------------------------------| | orgid | Foreign key to sp_orgs | | keyval | Provider key (e.g. `llmOpenAI`) | | metval | JSON config blob | | active | 1 = enabled, 0 = disabled | ### keyval names | Provider | keyval | |------------|----------------| | OpenAI | `llmOpenAI` | | Anthropic | `llmAnthropic` | | Gemini | `llmGemini` | | Mistral | `llmMistral` | | DeepSeek | `llmDeepSeek` | ### metval JSON structure (all providers) ```json { "secretKey": "sk-...", "model": "gpt-4o", "max_tokens": 4096, "temperature": 0.7 } ``` ## LLMManager — Factory Class ```php use App\LLM\LLMManager; // Get a configured, ready-to-use provider instance $llm = LLMManager::forOrg($orgId, 'openai'); // List all configured providers for an org $providers = LLMManager::getAvailableProviders($orgId); // returns: ['openai', 'gemini'] // Save or update a provider config LLMManager::saveOrgConfig($orgId, 'openai', [ 'secretKey' => 'sk-...', 'model' => 'gpt-4o', 'max_tokens' => 4096, 'temperature' => 0.7, ]); ``` ## LLMProvider — Base Class Interface All providers expose the same methods: ```php // Send a prompt, get full response $response = $llm->sendPrompt(array $messages, array $options = []); // Returns: ['success' => bool, 'content' => string, 'usage' => array, 'error' => string] // Stream a prompt, callback per chunk $result = $llm->streamPrompt(array $messages, array $options, callable $callback); // Returns: ['success' => bool, 'error' => string] // Get available models $models = $llm->getModels(); // Returns: [['id' => 'gpt-4o', 'label' => 'GPT-4o'], ...] // Test API key + connectivity $status = $llm->testConnection(); // Returns: ['success' => bool, 'latency_ms' => int, 'error' => string] ``` ### Fluent setters (chainable) ```php $llm->setModel('gpt-4o') ->setMaxTokens(2048) ->setTemperature(0.5) ->setSystemPrompt('You are a helpful assistant.') ->setTimeout(60); ``` ## Usage Examples (PHP) ### Basic chat ```php $llm = LLMManager::forOrg($orgId, 'openai'); $response = $llm->sendPrompt([ ['role' => 'user', 'content' => 'Summarize this contract: ...'] ]); if ($response['success']) { echo $response['content']; // $response['usage'] = ['prompt_tokens' => 120, 'completion_tokens' => 80, 'total_tokens' => 200] } ``` ### With system prompt and custom options ```php $llm = LLMManager::forOrg($orgId, 'anthropic'); $llm->setSystemPrompt('You are a legal document assistant.'); $response = $llm->sendPrompt( [['role' => 'user', 'content' => 'Draft an NDA for two parties.']], ['model' => 'claude-sonnet-4-6', 'max_tokens' => 2048, 'temperature' => 0.3] ); ``` ### Streaming (PHP — CLI or long-running process) ```php $llm = LLMManager::forOrg($orgId, 'gemini'); $llm->streamPrompt( [['role' => 'user', 'content' => 'Write a report on...']], [], function(string $chunk) { echo $chunk; flush(); } ); ``` ### Multi-turn conversation ```php $messages = [ ['role' => 'user', 'content' => 'My name is Carlos.'], ['role' => 'assistant', 'content' => 'Nice to meet you, Carlos!'], ['role' => 'user', 'content' => 'What is my name?'], ]; $response = $llm->sendPrompt($messages); ``` ## API Endpoints — appi/controllers/llm.php All endpoints are authenticated via `Auth::API()`. Internal AJAX requests (X-Requested-With: XMLHttpRequest) pass through automatically. External requests require `?apikey=` param. ### POST /appi/llm/chat Full response. ```json // Request { "provider": "openai", "org_id": 100, "messages": [{"role": "user", "content": "Hello"}], "options": {"model": "gpt-4o", "max_tokens": 1024, "system_prompt": "..."} } // Response { "success": true, "provider": "openai", "content": "Hello! How can I help you?", "usage": {"prompt_tokens": 10, "completion_tokens": 8, "total_tokens": 18} } ``` ### POST /appi/llm/stream SSE streaming. Each chunk sent as: ``` event: chunk data: {"chunk":"partial text here"} event: done data: "[DONE]" event: error data: {"error":"something went wrong"} ``` ### GET /appi/llm/models?provider=openai&org_id=100 ```json { "success": true, "provider": "openai", "models": [{"id": "gpt-4o", "label": "GPT-4o"}, ...] } ``` ### GET /appi/llm/providers?org_id=100 ```json { "success": true, "org_id": 100, "providers": ["openai", "gemini"] } ``` ### POST /appi/llm/test ```json // Request {"provider": "openai", "org_id": 100} // Response {"success": true, "provider": "openai", "latency_ms": 342, "error": ""} ``` ## Frontend JS Client — public/assets/js/llm-client.js Load in any page that needs LLM functionality: ```php $this->JavaScript[] = '/public/assets/js/llm-client.js'; $this->view->JavaScript = $this->JavaScript; ``` ### LLMClient.chat() ```js const res = await LLMClient.chat({ provider: 'openai', orgId: 100, messages: [{ role: 'user', content: 'Hello' }], options: { model: 'gpt-4o', system_prompt: 'You are helpful.' } }); console.log(res.content); ``` ### LLMClient.stream() ```js const output = document.getElementById('output'); await LLMClient.stream({ provider: 'anthropic', orgId: 100, messages: [{ role: 'user', content: 'Write a report on...' }], options: { model: 'claude-sonnet-4-6' }, onChunk: (chunk) => { output.innerHTML += chunk; }, onDone: () => { console.log('Stream complete'); }, onError: (err) => { console.error('Error:', err); } }); ``` ### LLMClient.getModels() ```js const { models } = await LLMClient.getModels('openai', 100); // models = [{ id: 'gpt-4o', label: 'GPT-4o' }, ...] ``` ### LLMClient.getProviders() ```js const { providers } = await LLMClient.getProviders(100); // providers = ['openai', 'gemini'] ``` ### LLMClient.test() ```js const { success, latency_ms, error } = await LLMClient.test('openai', 100); ``` ## Provider API Differences (internals) | Provider | Auth Header | System Prompt | Role names | Quirks | |------------|-------------------------|-----------------------|-------------------------|---------------------------------| | OpenAI | `Authorization: Bearer` | `role: system` msg | user / assistant | No temperature on o1/o3 models | | Anthropic | `x-api-key` | Top-level `system` key| user / assistant | Requires `anthropic-version` header | | Gemini | `?key=` query param | `systemInstruction` | user / model | Messages use `parts: [{text}]` | | Mistral | `Authorization: Bearer` | `role: system` msg | user / assistant | None | | DeepSeek | `Authorization: Bearer` | `role: system` msg | user / assistant | No temperature on R1 (reasoner) | ## Available Models ### OpenAI - `gpt-4o` — GPT-4o (default) - `gpt-4o-mini` — GPT-4o Mini - `gpt-4-turbo` — GPT-4 Turbo - `o1` — o1 - `o1-mini` — o1 Mini - `o3-mini` — o3 Mini ### Anthropic - `claude-sonnet-4-6` — Claude Sonnet 4.6 (default) - `claude-opus-4-6` — Claude Opus 4.6 - `claude-haiku-4-5-20251001` — Claude Haiku 4.5 ### Gemini - `gemini-2.5-flash` — Gemini 2.5 Flash (default) - `gemini-2.5-flash-lite` — Gemini 2.5 Flash Lite - `gemini-2.0-flash` — Gemini 2.0 Flash - `gemini-1.5-pro` — Gemini 1.5 Pro ### Mistral - `mistral-large-latest` — Mistral Large (default) - `mistral-small-latest` — Mistral Small - `codestral-latest` — Codestral - `open-mistral-nemo` — Mistral Nemo ### DeepSeek - `deepseek-chat` — DeepSeek Chat V3 (default) - `deepseek-reasoner` — DeepSeek Reasoner R1 ## Adding a New Provider 1. Create `app/LLM/svcNewProvider.php` extending `LLMProvider` 2. Implement: `__construct(array $config)`, `sendPrompt()`, `streamPrompt()`, `getModels()`, `testConnection()` 3. Register in `LLMManager.php`: - Add to `$providers` array: `'newprovider' => svcNewProvider::class` - Add to `$metaKeys` array: `'newprovider' => 'llmNewProvider'` 4. Add JSON config row to `sp_orgs_meta` with `keyval = 'llmNewProvider'`