<?php
declare(strict_types=1);
session_start();
/* ---------- CORS ---------- */
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-Session-Id, X-Username');
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') { http_response_code(204); exit; }
/* ---------- Helpers ---------- */
function jerr(string $msg, array $debug = []): void {
http_response_code(200);
echo json_encode(['error' => $msg, 'debug' => $debug], JSON_UNESCAPED_SLASHES);
exit;
}
function cut(string $s, int $n = 800): string {
return function_exists('mb_substr') ? mb_substr($s, 0, $n) : substr($s, 0, $n);
}
function load_keys_from_file(?string $path): array {
if (!$path || !is_file($path)) return [];
$cfg = include $path;
return is_array($cfg) ? $cfg : [];
}
function read_json_body(): array {
$raw = file_get_contents('php://input');
if ($raw === false || $raw === '') return [];
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
jerr('Invalid JSON body: '.json_last_error_msg(), ['raw' => cut($raw, 300)]);
}
return is_array($data) ? $data : [];
}
/* ---------- Load API keys ---------- */
$cfg = [];
$try1 = realpath(__DIR__ . '/../keys/keys.php');
$try2 = realpath(__DIR__ . '/keys/keys.php');
if (!$cfg) $cfg = load_keys_from_file($try1);
if (!$cfg) $cfg = load_keys_from_file($try2);
$DEEPSEEK_API_KEY = getenv('DEEPSEEK_API_KEY') ?: ($cfg['DEEPSEEK_API_KEY'] ?? '');
$OPENAI_API_KEY = getenv('OPENAI_API_KEY') ?: ($cfg['OPENAI_API_KEY'] ?? '');
$XAI_API_KEY = getenv('XAI_API_KEY') ?: ($cfg['XAI_API_KEY'] ?? '');
$ANTHROPIC_API_KEY= getenv('ANTHROPIC_API_KEY')?: ($cfg['ANTHROPIC_API_KEY']?? '');
/* ---------- DB Connection ---------- */
$cfgPath = realpath(__DIR__ . '/root/core/db_config.php');
if (!$cfgPath || !is_file($cfgPath)) {
$alt = realpath(__DIR__ . '/../root/core/db_config.php');
if ($alt && is_file($alt)) { $cfgPath = $alt; }
}
if ($cfgPath && is_file($cfgPath)) {
require_once $cfgPath; // defines getDB()
}
$pdo = function_exists('getDB') ? getDB() : null;
/* ---------- Settings ---------- */
$ENABLE_HISTORY = true;
$MAX_HISTORY_MESSAGES = 12;
$TIMEOUT_SECONDS = 120;
/* ---------- Pricing ---------- */
const MODEL_PRICING = [
'deepseek-chat' => ['in'=>0.27, 'out'=>1.10],
'deepseek-reasoner' => ['in'=>0.55, 'out'=>2.19],
'gpt-4o' => ['in'=>2.50, 'out'=>10.00],
'gpt-4o-mini' => ['in'=>0.15, 'out'=>0.60],
'gpt-5' => ['in'=>1.25, 'out'=>10.00],
'gpt-5-mini' => ['in'=>0.25, 'out'=>2.00],
'gpt-5-nano' => ['in'=>0.05, 'out'=>0.40],
'gpt-5-pro' => ['in'=>2.50, 'out'=>15.00],
'grok-3' => ['in'=>3.00, 'out'=>15.00],
'grok-3-mini' => ['in'=>0.30, 'out'=>0.50],
'grok-code-fast-1' => ['in'=>0.20, 'out'=>1.50],
/* Claude 4.5 — Sonnet, Haiku, Opus 4.1 */
'claude-sonnet-4-5' => ['in'=>1.50, 'out'=>7.50],
'claude-sonnet-4-5-20250929' => ['in'=>1.50, 'out'=>7.50],
'claude-haiku-4-5' => ['in'=>0.50, 'out'=>2.50],
'claude-haiku-4-5-20251001' => ['in'=>0.50, 'out'=>2.50],
'claude-opus-4-1' => ['in'=>7.50, 'out'=>37.50],
'claude-opus-4-1-20250805' => ['in'=>7.50, 'out'=>37.50],
];
function estimate_cost(string $model, int $inTok, int $outTok): float {
$p = MODEL_PRICING[$model] ?? null;
if (!$p) return 0.0;
return ($inTok / 1_000_000 * $p['in']) + ($outTok / 1_000_000 * $p['out']);
}
/* ---------- Optional: usage summary ---------- */
$action = $_GET['action'] ?? '';
if ($action === 'get_usage_summary') {
if (!$pdo) jerr('DB not configured');
$username = $_SERVER['HTTP_X_USERNAME'] ?? ($_SESSION['username'] ?? null);
if (!$username) jerr('Missing username header');
try {
$sum = $pdo->prepare("
SELECT
COALESCE(SUM(total_cost_est),0) AS total_cost,
COALESCE(SUM(input_tokens),0) AS total_in,
COALESCE(SUM(output_tokens),0) AS total_out,
COUNT(*) AS calls
FROM ai_usage
WHERE username = :u
");
$sum->execute([':u' => $username]);
$summary = $sum->fetch() ?: [];
$by = $pdo->prepare("
SELECT model, COUNT(*) AS calls,
SUM(input_tokens) AS total_in,
SUM(output_tokens) AS total_out,
ROUND(SUM(total_cost_est),6) AS total_cost
FROM ai_usage
WHERE username = :u
GROUP BY model
ORDER BY total_cost DESC
");
$by->execute([':u' => $username]);
echo json_encode(['ok'=>true,'summary'=>$summary,'byModel'=>$by->fetchAll()], JSON_UNESCAPED_SLASHES);
exit;
} catch (Throwable $e) {
jerr('DB query failed', ['err'=>$e->getMessage()]);
}
}
/* ---------- Normal Chat Completion ---------- */
$req = read_json_body();
if (!$req && !empty($_POST)) $req = $_POST;
$question = trim((string)($req['question'] ?? ''));
$model = (string)($req['model'] ?? 'deepseek-chat');
$maxTokens = (int)($req['maxTokens'] ?? 800);
$temperature = (float)($req['temperature'] ?? 0.7);
$system = (string)($req['system'] ?? "You are a helpful assistant.");
if ($question === '') jerr('Please enter a question.');
/* ---------- Provider Selection ---------- */
if (preg_match('/^claude/i', $model)) {
$provider = 'anthropic';
$api_url = 'https://api.anthropic.com/v1/messages';
$api_key = $ANTHROPIC_API_KEY;
if ($api_key === '') jerr('Missing Anthropic API key.');
}
elseif (str_starts_with($model, 'deepseek')) {
$provider = 'deepseek';
$api_url = 'https://api.deepseek.com/chat/completions';
$api_key = $DEEPSEEK_API_KEY;
}
elseif (preg_match('/^grok[-_]/i', $model)) {
$provider = 'xai';
$api_url = 'https://api.x.ai/v1/chat/completions';
$api_key = $XAI_API_KEY;
}
else {
$provider = 'openai';
$api_url = 'https://api.openai.com/v1/chat/completions';
$api_key = $OPENAI_API_KEY;
}
/* ---------- History ---------- */
$sessionId = (string)($req['sessionId'] ?? ($_SERVER['HTTP_X_SESSION_ID'] ?? 'default'));
// ✅ USE CONVERSATION HISTORY FROM REQUEST (filtered by frontend)
$conversationHistory = $req['conversationHistory'] ?? [];
$messages = [['role' => 'system', 'content' => $system]];
// ✅ Add filtered conversation history from frontend
foreach ($conversationHistory as $m) {
if (isset($m['role']) && isset($m['content'])) {
$messages[] = ['role' => $m['role'], 'content' => $m['content']];
}
}
// ✅ Add current question
$messages[] = ['role'=>'user','content'=>$question];
/* ---------- Payload ---------- */
if ($provider === 'anthropic') {
$systemPrompt = '';
$filteredMessages = [];
foreach ($messages as $m) {
if ($m['role'] === 'system') {
$systemPrompt .= $m['content'] . "\n";
} else {
$filteredMessages[] = [
'role' => $m['role'],
'content' => (string)$m['content']
];
}
}
$payload = [
'model' => $model,
'max_tokens' => $maxTokens,
'messages' => $filteredMessages,
'temperature' => $temperature
];
// Only add system if it exists and is non-empty
if (trim($systemPrompt) !== '') {
$payload['system'] = trim($systemPrompt);
}
} else {
// OpenAI / Grok / DeepSeek compatible
$payload = [
'model' => $model,
'messages' => array_map(function($m) {
return [
'role' => $m['role'],
'content' => (string)$m['content']
];
}, $messages),
'max_tokens' => $maxTokens,
'temperature' => $temperature,
];
}
/* ---------- CURL Call ---------- */
if ($provider === 'anthropic') {
$headers = [
'Content-Type: application/json',
'x-api-key: ' . $api_key,
'Anthropic-Version: 2023-06-01',
];
} else {
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $api_key,
];
}
$ch = curl_init($api_url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_SLASHES),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $TIMEOUT_SECONDS,
]);
$raw = curl_exec($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$cerr = curl_error($ch);
curl_close($ch);
if ($cerr) jerr('cURL error: '.$cerr);
if ($http < 200 || $http >= 300) jerr("Upstream HTTP $http", ['resp'=>cut((string)$raw,800)]);
$json = json_decode((string)$raw, true);
if (json_last_error() !== JSON_ERROR_NONE) jerr('Invalid JSON from upstream');
if ($provider === 'anthropic') {
// Claude response
$answer = '';
if (isset($json['content'][0]['text'])) {
$answer = $json['content'][0]['text'];
} elseif (is_array($json['content'])) {
foreach ($json['content'] as $c) {
if (isset($c['text'])) {
$answer .= $c['text'];
}
}
}
} else {
// OpenAI / DeepSeek / Grok
$answer = $json['choices'][0]['message']['content'] ?? '(no content)';
}
$rawUsage = $json['usage'] ?? [];
if ($provider === 'anthropic') {
$usage = [
'prompt_tokens' => $rawUsage['input_tokens'] ?? 0,
'completion_tokens' => $rawUsage['output_tokens'] ?? 0,
];
} else {
$usage = [
'prompt_tokens' => $rawUsage['prompt_tokens'] ?? 0,
'completion_tokens' => $rawUsage['completion_tokens'] ?? 0,
];
}
/* ---------- Store History
$_SESSION['chat_log_map'][$sessionId][] = ['role'=>'user','content'=>$question];
$_SESSION['chat_log_map'][$sessionId][] = ['role'=>'assistant','content'=>$answer];
*/
/* ---------- Store Usage ---------- */
if ($pdo && $usage) {
$username = $_SERVER['HTTP_X_USERNAME'] ?? ($_SESSION['username'] ?? 'guest');
$inTok = (int)($usage['prompt_tokens'] ?? 0);
$outTok = (int)($usage['completion_tokens'] ?? 0);
$cost = estimate_cost($model, $inTok, $outTok);
try {
$stmt = $pdo->prepare("
INSERT INTO ai_usage
(username, session_id, model, provider, input_tokens, output_tokens, total_cost_est)
VALUES (:u,:sid,:m,:p,:inTok,:outTok,:cost)
");
$stmt->execute([
':u'=>$username, ':sid'=>$sessionId,
':m'=>$model, ':p'=>$provider,
':inTok'=>$inTok, ':outTok'=>$outTok,
':cost'=>$cost
]);
} catch (Throwable $e) {
error_log('Usage insert failed: '.$e->getMessage());
}
}
/* ---------- Output ---------- */
echo json_encode([
'success'=>true,
'provider'=>$provider,
'model'=>$model,
'answer'=>$answer,
'usage'=>$usage,
'sessionId'=>$sessionId
], JSON_UNESCAPED_SLASHES);
exit;