# 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'`
