Last active
October 9, 2025 13:14
-
-
Save arenagroove/14b5bb195d32946a3d983a9f6a15cb52 to your computer and use it in GitHub Desktop.
Minimal PHP proxy for Groq’s OpenAI-compatible API. Handles CORS for localhost and CodePen, adds optional shared secret, and trims histories for safe front-end LLM demos.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * proxy.php — Groq OpenAI-compatible relay | |
| * ----------------------------------------------------------------------------- | |
| * Description: | |
| * Secure PHP proxy for client-side LLM demos (CodePen / localhost). | |
| * Injects your Groq API key server-side, handles CORS, and optionally enforces | |
| * model allow-listing and shared secrets. | |
| * | |
| * Author: Luis Alberto Martínez Riancho (Less Rain GmbH) | |
| * Affiliation: Independent R&D — Assistant Prompt Design, Less Rain GmbH | |
| * Version: 1.0.0 | |
| * Date: 2025-10-09 | |
| * | |
| * Usage: | |
| * 1. Save as proxy.php on your local or private server. | |
| * 2. Insert your Groq API key below or load via environment variable. | |
| * 3. Point your JS fetch() to this proxy instead of api.groq.com. | |
| * | |
| * Credentials: | |
| * • API key — kept server-side (never exposed to client) | |
| * • Shared secret — optional header (X-Proxy-Key) | |
| * | |
| * Security: | |
| * • Only allows localhost and *.codepen.io / *.cdpn.io origins. | |
| * • Rejects all other requests with proper CORS handling. | |
| * • Trims chat histories to prevent large payloads. | |
| * | |
| * License: MIT (use freely with attribution) | |
| * | |
| * Docs: | |
| * • Quickstart — https://console.groq.com/docs/quickstart | |
| * • OpenAI compatibility — https://console.groq.com/docs/openai | |
| * • API reference — https://console.groq.com/docs/api-reference | |
| * | |
| * ----------------------------------------------------------------------------- | |
| * NOTE: DO NOT COMMIT REAL API KEYS TO PUBLIC REPOSITORIES OR GISTS. | |
| * | |
| * CodePen Demo — “LLM Temporal Debate: Halloween Through Time” | |
| * https://codepen.io/luis-lessrain/pen/LEGyMXV | |
| */ | |
| /* ============================================================================ | |
| * CONFIG (EDIT HERE) | |
| * ========================================================================== */ | |
| // REQUIRED: your Groq API key (keep private) | |
| $apiKey = 'YOUR_GROQ_API_KEY_HERE'; // <<< insert your private key locally | |
| // OPTIONAL: restrict models (empty array = no restriction) | |
| $allowedModels = [ | |
| 'llama-3.1-8b-instant', | |
| 'meta-llama/llama-4-scout-17b-16e-instruct', | |
| // add/remove as needed | |
| ]; | |
| // OPTIONAL: shared secret (must match JS "X-Proxy-Key", leave '' to disable) | |
| $sharedSecret = ''; // e.g. 'my_secret_123' or '' to turn off | |
| // Default model if client omits it | |
| $defaultModel = 'llama-3.1-8b-instant'; | |
| /* ============================================================================ | |
| * ORIGIN RESTRICTION (localhost + CodePen, robust) | |
| * ========================================================================== */ | |
| function str_ends_with_ci(string $haystack, string $needle): bool { | |
| $h = strtolower($haystack); | |
| $n = strtolower($needle); | |
| if ($n === '') { | |
| return true; | |
| } | |
| $len = strlen($n); | |
| return $len === 0 ? true : (substr($h, -$len) === $n); | |
| } | |
| function parse_host_from_url(?string $url): string { | |
| if (!$url) { | |
| return ''; | |
| } | |
| $p = parse_url($url); | |
| return isset($p['host']) ? strtolower($p['host']) : ''; | |
| } | |
| function origin_base_from_url(?string $url): string { | |
| if (!$url) { | |
| return ''; | |
| } | |
| $p = parse_url($url); | |
| if (!is_array($p) || empty($p['scheme']) || empty($p['host'])) { | |
| return ''; | |
| } | |
| return $p['scheme'] . '://' . $p['host'] . (isset($p['port']) ? ':' . $p['port'] : ''); | |
| } | |
| function is_local_ip(string $ip): bool { | |
| return in_array($ip, ['127.0.0.1', '::1'], true); | |
| } | |
| $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; | |
| $referer = $_SERVER['HTTP_REFERER'] ?? ''; | |
| $remoteIP = $_SERVER['REMOTE_ADDR'] ?? ''; | |
| $originHost = parse_host_from_url($origin); | |
| $refererHost = parse_host_from_url($referer); | |
| // Allow localhost and any subdomain of codepen.io / cdpn.io | |
| $hostAllowed = function (string $host): bool { | |
| if ($host === 'localhost' || $host === '127.0.0.1') { | |
| return true; | |
| } | |
| return str_ends_with_ci($host, '.codepen.io') || $host === 'codepen.io' | |
| || str_ends_with_ci($host, '.cdpn.io') || $host === 'cdpn.io'; | |
| }; | |
| $allowed = false; | |
| $acao = ''; // value echoed in Access-Control-Allow-Origin | |
| if ($originHost && $hostAllowed($originHost)) { | |
| $allowed = true; | |
| $acao = origin_base_from_url($origin); | |
| } elseif ((!$origin || strtolower($origin) === 'null') && $refererHost && $hostAllowed($refererHost)) { | |
| // Some CodePen iframes send Origin null but include Referer | |
| $acao = origin_base_from_url($referer); | |
| if ($acao) { | |
| $allowed = true; | |
| } | |
| } elseif (!$origin && is_local_ip($remoteIP)) { | |
| // Local tools (curl/postman) without Origin | |
| $allowed = true; | |
| $acao = 'http://localhost'; | |
| } | |
| // Preflight first | |
| if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { | |
| if ($allowed && $acao) { | |
| header('Access-Control-Allow-Origin: ' . $acao); | |
| header('Vary: Origin'); | |
| header('Access-Control-Allow-Methods: POST, OPTIONS'); | |
| header('Access-Control-Allow-Headers: Content-Type, X-Proxy-Key'); | |
| http_response_code(204); | |
| } else { | |
| http_response_code(403); | |
| header('Content-Type: application/json'); | |
| echo json_encode([ | |
| 'error' => 'CORS: origin not allowed', | |
| 'origin' => $origin, | |
| 'referer' => $referer, | |
| 'ip' => $remoteIP, | |
| ], JSON_UNESCAPED_SLASHES); | |
| } | |
| exit; | |
| } | |
| // Block disallowed origins | |
| if (!$allowed || !$acao) { | |
| http_response_code(403); | |
| header('Content-Type: application/json'); | |
| echo json_encode([ | |
| 'error' => 'Forbidden: origin not allowed', | |
| 'origin' => $origin, | |
| 'referer' => $referer, | |
| 'ip' => $remoteIP, | |
| ], JSON_UNESCAPED_SLASHES); | |
| exit; | |
| } | |
| // CORS for allowed callers | |
| header('Access-Control-Allow-Origin: ' . $acao); | |
| header('Vary: Origin'); | |
| header('Access-Control-Allow-Headers: Content-Type, X-Proxy-Key'); | |
| header('Access-Control-Allow-Methods: POST, OPTIONS'); | |
| /* ============================================================================ | |
| * CORE PROXY LOGIC | |
| * ========================================================================== */ | |
| function json_error($code, $msg, $extra = []) { | |
| http_response_code($code); | |
| header('Content-Type: application/json'); | |
| echo json_encode(array_merge(['error' => $msg], $extra), JSON_UNESCAPED_SLASHES); | |
| exit; | |
| } | |
| if ($_SERVER['REQUEST_METHOD'] !== 'POST') { | |
| json_error(405, 'Method not allowed'); | |
| } | |
| if ($apiKey === '') { | |
| json_error(500, 'Proxy not configured: missing API key'); | |
| } | |
| // Optional shared-secret check | |
| if ($sharedSecret !== '') { | |
| $clientKey = $_SERVER['HTTP_X_PROXY_KEY'] ?? ''; | |
| if (!hash_equals($sharedSecret, $clientKey)) { | |
| json_error(403, 'Forbidden: invalid proxy key'); | |
| } | |
| } | |
| // Parse JSON body | |
| $raw = file_get_contents('php://input'); | |
| $input = json_decode($raw, true); | |
| if (!is_array($input)) { | |
| json_error(400, 'Invalid JSON body'); | |
| } | |
| $messages = $input['messages'] ?? null; | |
| $model = $input['model'] ?? $defaultModel; | |
| $temperature = max(0.0, min(1.0, floatval($input['temperature'] ?? 0.4))); | |
| $max_tokens = max(16, min(4096, intval($input['max_tokens'] ?? 256))); | |
| if (!$messages || !is_array($messages)) { | |
| json_error(400, 'Missing or invalid "messages" array'); | |
| } | |
| if (count($messages) > 50) { | |
| $messages = array_slice($messages, -50); | |
| } | |
| // Optional server model restriction | |
| if (!empty($allowedModels) && !in_array($model, $allowedModels, true)) { | |
| json_error(400, 'Unsupported model (blocked by server)', [ | |
| 'allowed' => $allowedModels, | |
| 'received' => $model, | |
| ]); | |
| } | |
| // Build Groq request | |
| $payload = [ | |
| 'model' => $model, | |
| 'messages' => $messages, | |
| 'temperature' => $temperature, | |
| 'max_tokens' => $max_tokens, | |
| ]; | |
| $ch = curl_init('https://api.groq.com/openai/v1/chat/completions'); | |
| curl_setopt_array($ch, [ | |
| CURLOPT_RETURNTRANSFER => true, | |
| CURLOPT_POST => true, | |
| CURLOPT_HTTPHEADER => [ | |
| "Authorization: Bearer {$apiKey}", | |
| "Content-Type: application/json", | |
| "Accept: application/json", | |
| ], | |
| CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_SLASHES), | |
| CURLOPT_ENCODING => '', | |
| CURLOPT_CONNECTTIMEOUT => 10, | |
| CURLOPT_TIMEOUT => 30, | |
| ]); | |
| $response = curl_exec($ch); | |
| $curlErr = curl_error($ch); | |
| $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | |
| curl_close($ch); | |
| header('Content-Type: application/json'); | |
| if ($response === false) { | |
| json_error(502, 'Upstream request failed', ['curl_error' => $curlErr]); | |
| } | |
| $decoded = json_decode($response, true); | |
| if ($httpCode < 200 || $httpCode >= 300) { | |
| if (is_array($decoded) && isset($decoded['error'])) { | |
| json_error($httpCode, 'Groq API error', ['groq' => $decoded['error']]); | |
| } else { | |
| json_error($httpCode, 'Groq API error', ['body' => $response]); | |
| } | |
| } | |
| // Success | |
| http_response_code($httpCode); | |
| echo $response; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment