<?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, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-Session-Id');
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 : [];
}
/* ---------- CONFIG ---------- */
$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'] ?? '');
$ENABLE_HISTORY = true;
$MAX_HISTORY_MESSAGES = 12;
$TIMEOUT_SECONDS = 120;
/* ---------- Input ---------- */
$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, accurate assistant. Be concise and clear. Use markdown when it helps readability.");
$includeArtifacts = (bool)($req['includeArtifacts'] ?? false);
/* Per-request history control (default TRUE) */
$includeHistoryReq = $req['includeHistory'] ?? null;
$USE_HISTORY = $ENABLE_HISTORY && ($includeHistoryReq === null ? true : (bool)$includeHistoryReq);
/* Client session id */
$sessionId = (string)($req['sessionId'] ?? ($_SERVER['HTTP_X_SESSION_ID'] ?? 'default'));
if ($sessionId === '') $sessionId = 'default';
/* Optional reset and history override */
$resetSession = (bool)($req['reset'] ?? false);
/* NEW: client-provided history override — array of {role:'user'|'assistant', content:string} */
$historyOverride = $req['history'] ?? null;
if ($question === '') jerr('Please enter a question.');
/* ---------- Provider selection ---------- */
$provider = 'openai';
$api_url = 'https://api.openai.com/v1/chat/completions';
$api_key = $OPENAI_API_KEY;
if (str_starts_with($model, 'deepseek')) {
$provider = 'deepseek'; $api_url = 'https://api.deepseek.com/chat/completions'; $api_key = $DEEPSEEK_API_KEY;
if ($api_key === '') jerr('Missing DeepSeek API key. Set env or put it in keys.php.');
} elseif (preg_match('/^grok[-_]/i', $model)) {
$provider = 'xai'; $api_url = 'https://api.x.ai/v1/chat/completions'; $api_key = $XAI_API_KEY;
if ($api_key === '') jerr('Missing xAI (Grok) API key. Set env or put it in keys.php.');
} else {
$provider = 'openai'; $api_url = 'https://api.openai.com/v1/chat/completions'; $api_key = $OPENAI_API_KEY;
if ($api_key === '') jerr('Missing OpenAI API key. Set env or put it in keys.php.');
}
/* ---------- Model allowlists (warn only) ---------- */
$valid_openai = ['gpt-5','gpt-5-mini','gpt-5-nano','gpt-5-thinking','gpt-5-pro','gpt-4o','gpt-4o-mini'];
$valid_deepseek= ['deepseek-chat','deepseek-reasoner'];
$valid_xai = ['grok-3','grok-3-mini','grok-code-fast-1','grok-4-0709'];
$warn_invalid = null;
if ($provider === 'openai' && !in_array($model, $valid_openai, true)) $warn_invalid = "Unrecognized OpenAI model '$model' (will still attempt).";
if ($provider === 'deepseek' && !in_array($model, $valid_deepseek, true)) $warn_invalid = "Unrecognized DeepSeek model '$model' (will still attempt).";
if ($provider === 'xai' && !in_array($model, $valid_xai, true)) $warn_invalid = "Unrecognized xAI model '$model' (will still attempt).";
/* ---------- Session-state maps ---------- */
$_SESSION['chat_log_map'] = $_SESSION['chat_log_map'] ?? [];
$_SESSION['usage_totals_map'] = $_SESSION['usage_totals_map'] ?? [];
if ($resetSession) {
unset($_SESSION['chat_log_map'][$sessionId], $_SESSION['usage_totals_map'][$sessionId]);
}
/* ---------- Compute history to use ---------- */
$messages = [['role' => 'system', 'content' => $system]];
$history = $_SESSION['chat_log_map'][$sessionId] ?? [];
/* If a client-provided history is given, prefer it and replace stored */
if (is_array($historyOverride)) {
$sanitized = [];
foreach ($historyOverride as $m) {
$role = $m['role'] ?? '';
$content = (string)($m['content'] ?? '');
if (($role === 'user' || $role === 'assistant') && $content !== '') {
$sanitized[] = ['role' => $role, 'content' => $content, 'ts' => time()];
}
}
$_SESSION['chat_log_map'][$sessionId] = $sanitized;
$history = $sanitized;
}
if ($USE_HISTORY && !empty($history)) {
$slice = array_slice($history, -$MAX_HISTORY_MESSAGES);
foreach ($slice as $m) {
$role = $m['role'] ?? '';
if ($role === 'user' || $role === 'assistant') {
$messages[] = ['role' => $role, 'content' => (string)$m['content']];
} elseif ($role === 'artifact' && $includeArtifacts) {
$artifactContent = "I have an artifact titled \"{$m['title']}\". Here is the content:\n\n```\n{$m['content']}\n```";
$messages[] = ['role' => 'user', 'content' => $artifactContent];
}
}
}
$messages[] = ['role' => 'user', 'content' => $question];
/* ---------- Payload ---------- */
$payload = ['model' => $model, 'messages' => $messages];
if ($provider === 'openai' && str_starts_with($model, 'gpt-5')) {
if (!empty($req['forceTemperature'])) $payload['temperature'] = $temperature;
$payload['max_completion_tokens'] = $maxTokens;
} else {
$payload['max_tokens'] = $maxTokens;
$payload['temperature'] = $temperature;
}
if (!empty($req['response_format'])) $payload['response_format'] = $req['response_format'];
/* ---------- cURL ---------- */
$ch = curl_init($api_url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $api_key,
'User-Agent: blocksnips-unified/1.2',
],
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, ['provider'=>$provider,'model'=>$model,'url'=>$api_url]);
if ($http < 200 || $http >= 300) {
$maybe = json_decode((string)$raw, true);
$msg = is_array($maybe) ? ($maybe['error']['message'] ?? $maybe['message'] ?? ("HTTP $http")) : ("HTTP $http");
error_log("API Error: HTTP $http | provider=$provider model=$model | resp=" . cut((string)$raw, 800));
jerr($msg, ['http_code'=>$http,'provider'=>$provider,'model'=>$model,'url'=>$api_url,'upstream'=>cut((string)$raw, 800)]);
}
$json = json_decode((string)$raw, true);
if (json_last_error() !== JSON_ERROR_NONE) jerr('Upstream returned non-JSON', ['http_code'=>$http,'snippet'=>cut((string)$raw, 200)]);
$answer = $json['choices'][0]['message']['content'] ?? '(no content)';
$usage = $json['usage'] ?? null;
/* ---------- Save back ---------- */
$log = $_SESSION['chat_log_map'][$sessionId] ?? [];
$log[] = ['role'=>'user', 'content'=>$question, 'ts'=>time()];
$log[] = ['role'=>'assistant', 'content'=>$answer, 'ts'=>time()];
$_SESSION['chat_log_map'][$sessionId] = $log;
/* Usage per session */
$usage_payload = null;
$ut = $_SESSION['usage_totals_map'][$sessionId] ?? ['prompt'=>0,'completion'=>0,'total'=>0];
if (is_array($usage)) {
$pt = (int)($usage['prompt_tokens'] ?? 0);
$ct = (int)($usage['completion_tokens'] ?? 0);
$tt = (int)($usage['total_tokens'] ?? 0);
$_SESSION['last_usage'] = ['prompt'=>$pt,'completion'=>$ct,'total'=>$tt];
$ut['prompt'] += $pt; $ut['completion'] += $ct; $ut['total'] += $tt;
$usage_payload = [
'prompt_tokens' => $pt, 'completion_tokens' => $ct, 'total_tokens' => $tt,
'total_prompt' => $ut['prompt'], 'total_completion' => $ut['completion'], 'total_tokens_cumulative' => $ut['total'],
];
}
$_SESSION['usage_totals_map'][$sessionId] = $ut;
/* ---------- Out ---------- */
$out = [
'success' => true,
'provider' => $provider,
'model' => $model,
'answer' => $answer,
'usage' => $usage_payload,
'sessionId' => $sessionId,
'usedHistory' => $USE_HISTORY,
];
if ($warn_invalid) $out['warning'] = $warn_invalid;
echo json_encode($out, JSON_UNESCAPED_SLASHES);