Skip to content

Instantly share code, notes, and snippets.

@arenagroove
Last active October 9, 2025 13:14
Show Gist options
  • Select an option

  • Save arenagroove/14b5bb195d32946a3d983a9f6a15cb52 to your computer and use it in GitHub Desktop.

Select an option

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.
<?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