├── wrangler.toml ├── .gitignore ├── README.md └── index.js /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "grok3-api-cf" 2 | main = "index.js" 3 | compatibility_date = "2025-02-26" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | node_modules/ 3 | 4 | # Environment variables 5 | .env 6 | 7 | # Production build files 8 | dist/ 9 | .wrangler/ 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | 16 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grok3 API Cloudflare Worker 2 | 3 | A Cloudflare Worker implementation for the Grok3 API. 4 | 5 | 6 | ## Deployment 7 | 8 | ```bash 9 | npx wrangler deploy 10 | npx wrangler secret put AUTH_TOKEN 11 | npx wrangler secret put GROK3_COOKIE 12 | ``` 13 | 14 | ### Explain 15 | 16 | - `AUTH_TOKEN`: The token you will be using like an OPENAI API token, see below in the curl command 17 | - `GROK3_COOKIE`: It looks like `sso=ey...; sso-rw=ey...`, you can copy it from your browser 18 | 19 | ## Usage 20 | 21 | ```bash 22 | curl -H "Authorization: Bearer AUTH_TOKEN" \ 23 | -X POST 'https://your-worker-url.workers.dev/v1/chat/completions' \ 24 | -H 'Content-Type: application/json' \ 25 | -d '{"messages": [{"role": "user", "content": "Hello"}], "model": "grok-3"}' 26 | ``` 27 | 28 | ## Special Thanks 29 | 30 | - [mem0ai/grok3-api: Unofficial Grok 3 API](https://github.com/mem0ai/grok3-api) 31 | 32 | ## License 33 | 34 | MIT -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Grok3 API Cloudflare Worker 3 | * OpenAI-compatible API for Grok3 4 | */ 5 | 6 | class GrokClient { 7 | constructor(cookies) { 8 | this.baseUrl = "https://grok.com/rest/app-chat/conversations/new"; 9 | this.cookies = cookies; 10 | this.headers = { 11 | "accept": "*/*", 12 | "accept-language": "en-GB,en;q=0.9", 13 | "content-type": "application/json", 14 | "origin": "https://grok.com", 15 | "priority": "u=1, i", 16 | "referer": "https://grok.com/", 17 | "sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126", "Brave";v="126"', 18 | "sec-ch-ua-mobile": "?0", 19 | "sec-ch-ua-platform": '"macOS"', 20 | "sec-fetch-dest": "empty", 21 | "sec-fetch-mode": "cors", 22 | "sec-fetch-site": "same-origin", 23 | "sec-gpc": "1", 24 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", 25 | "cookie": cookies 26 | }; 27 | } 28 | 29 | _preparePayload(message) { 30 | return { 31 | "temporary": false, 32 | "modelName": "grok-3", 33 | "message": message, 34 | "fileAttachments": [], 35 | "imageAttachments": [], 36 | "disableSearch": false, 37 | "enableImageGeneration": true, 38 | "returnImageBytes": false, 39 | "returnRawGrokInXaiRequest": false, 40 | "enableImageStreaming": true, 41 | "imageGenerationCount": 2, 42 | "forceConcise": false, 43 | "toolOverrides": {}, 44 | "enableSideBySide": true, 45 | "isPreset": false, 46 | "sendFinalMetadata": true, 47 | "customInstructions": "", 48 | "deepsearchPreset": "", 49 | "isReasoning": false 50 | }; 51 | } 52 | 53 | async sendMessage(message, stream = false) { 54 | const payload = this._preparePayload(message); 55 | 56 | const response = await fetch(this.baseUrl, { 57 | method: 'POST', 58 | headers: this.headers, 59 | body: JSON.stringify(payload) 60 | }); 61 | 62 | if (!response.ok) { 63 | throw new Error(`Grok API error: ${response.status} ${response.statusText}`); 64 | } 65 | 66 | if (stream) { 67 | return response.body; 68 | } else { 69 | let fullResponse = ""; 70 | const reader = response.body.getReader(); 71 | const decoder = new TextDecoder(); 72 | 73 | while (true) { 74 | const { done, value } = await reader.read(); 75 | if (done) break; 76 | 77 | const chunk = decoder.decode(value, { stream: true }); 78 | const lines = chunk.split('\n').filter(line => line.trim()); 79 | 80 | for (const line of lines) { 81 | try { 82 | const jsonData = JSON.parse(line); 83 | const result = jsonData.result || {}; 84 | const responseData = result.response || {}; 85 | 86 | if (responseData.modelResponse) { 87 | return responseData.modelResponse.message; 88 | } 89 | 90 | const token = responseData.token || ""; 91 | if (token) { 92 | fullResponse += token; 93 | } 94 | } catch (e) { 95 | // Skip JSON parse errors 96 | } 97 | } 98 | } 99 | 100 | return fullResponse.trim(); 101 | } 102 | } 103 | } 104 | 105 | // Helper to create OpenAI compatible streaming responses 106 | function createOpenAIStreamingResponse(readableStream, requestId) { 107 | const encoder = new TextEncoder(); 108 | const decoder = new TextDecoder(); 109 | 110 | return new ReadableStream({ 111 | async start(controller) { 112 | const reader = readableStream.getReader(); 113 | let fullContent = ""; 114 | let completionId = `chatcmpl-${crypto.randomUUID()}`; 115 | 116 | // Send the start of the stream 117 | const startChunk = { 118 | id: completionId, 119 | object: "chat.completion.chunk", 120 | created: Math.floor(Date.now() / 1000), 121 | model: "grok-3", 122 | choices: [{ 123 | index: 0, 124 | delta: { role: "assistant" }, 125 | finish_reason: null 126 | }] 127 | }; 128 | 129 | controller.enqueue(encoder.encode(`data: ${JSON.stringify(startChunk)}\n\n`)); 130 | 131 | // Buffer to accumulate incomplete JSON data 132 | let buffer = ''; 133 | 134 | try { 135 | while (true) { 136 | const { done, value } = await reader.read(); 137 | if (done) break; 138 | 139 | const chunk = decoder.decode(value, { stream: true }); 140 | buffer += chunk; 141 | 142 | // Process complete JSON lines from buffer 143 | let newBuffer = ''; 144 | const lines = buffer.split('\n'); 145 | 146 | for (let i = 0; i < lines.length; i++) { 147 | const line = lines[i].trim(); 148 | if (!line) continue; 149 | 150 | // If this is not the last line, it must be complete 151 | // Or if it's the last line and ends with a newline 152 | const isCompleteLine = i < lines.length - 1 || chunk.endsWith('\n'); 153 | 154 | if (isCompleteLine) { 155 | try { 156 | const jsonData = JSON.parse(line); 157 | const result = jsonData.result || {}; 158 | const responseData = result.response || {}; 159 | 160 | // Handle both token and modelResponse 161 | if (responseData.modelResponse) { 162 | // This is a final message, but we'll still process it as a token 163 | // rather than returning early, which could miss content 164 | const finalMessage = responseData.modelResponse.message || ""; 165 | 166 | // Only send if there's content 167 | if (finalMessage && finalMessage !== fullContent) { 168 | // If we had partial content already, just send the remainder 169 | const remainingContent = finalMessage.slice(fullContent.length); 170 | 171 | if (remainingContent) { 172 | fullContent = finalMessage; 173 | const chunk = { 174 | id: completionId, 175 | object: "chat.completion.chunk", 176 | created: Math.floor(Date.now() / 1000), 177 | model: "grok-3", 178 | choices: [{ 179 | index: 0, 180 | delta: { content: remainingContent }, 181 | finish_reason: null 182 | }] 183 | }; 184 | controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); 185 | } 186 | } 187 | } 188 | 189 | const token = responseData.token || ""; 190 | if (token) { 191 | fullContent += token; 192 | const chunk = { 193 | id: completionId, 194 | object: "chat.completion.chunk", 195 | created: Math.floor(Date.now() / 1000), 196 | model: "grok-3", 197 | choices: [{ 198 | index: 0, 199 | delta: { content: token }, 200 | finish_reason: null 201 | }] 202 | }; 203 | controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); 204 | } 205 | } catch (e) { 206 | // Incomplete or invalid JSON, skip this line 207 | } 208 | } else { 209 | // This is an incomplete line, add it back to the buffer 210 | newBuffer = line; 211 | } 212 | } 213 | 214 | buffer = newBuffer; // Update buffer with any incomplete data 215 | } 216 | 217 | // Send a final chunk with stop reason when we're done 218 | const finalChunk = { 219 | id: completionId, 220 | object: "chat.completion.chunk", 221 | created: Math.floor(Date.now() / 1000), 222 | model: "grok-3", 223 | choices: [{ 224 | index: 0, 225 | delta: {}, 226 | finish_reason: "stop" 227 | }] 228 | }; 229 | controller.enqueue(encoder.encode(`data: ${JSON.stringify(finalChunk)}\n\n`)); 230 | 231 | // End of stream marker 232 | controller.enqueue(encoder.encode('data: [DONE]\n\n')); 233 | } catch (e) { 234 | controller.error(e); 235 | } finally { 236 | controller.close(); 237 | } 238 | } 239 | }); 240 | } 241 | 242 | // Helper to create OpenAI compatible full responses 243 | function createOpenAIFullResponse(content) { 244 | return { 245 | id: `chatcmpl-${crypto.randomUUID()}`, 246 | object: "chat.completion", 247 | created: Math.floor(Date.now() / 1000), 248 | model: "grok-3", 249 | choices: [{ 250 | index: 0, 251 | message: { 252 | role: "assistant", 253 | content: content 254 | }, 255 | finish_reason: "stop" 256 | }], 257 | usage: { 258 | prompt_tokens: -1, 259 | completion_tokens: -1, 260 | total_tokens: -1 261 | } 262 | }; 263 | } 264 | 265 | // Main request handler 266 | async function handleRequest(request, env) { 267 | // Check if it's a POST request to /v1/chat/completions 268 | const url = new URL(request.url); 269 | if (url.pathname !== "/v1/chat/completions") { 270 | return new Response("Not Found", { status: 404 }); 271 | } 272 | 273 | if (request.method !== "POST") { 274 | return new Response("Method Not Allowed", { status: 405 }); 275 | } 276 | 277 | // Check authentication 278 | const authHeader = request.headers.get("Authorization"); 279 | if (!authHeader || !authHeader.startsWith("Bearer ")) { 280 | return new Response("Unauthorized: Bearer token required", { status: 401 }); 281 | } 282 | 283 | const token = authHeader.split(" ")[1]; 284 | if (!env.AUTH_TOKEN || token !== env.AUTH_TOKEN) { 285 | return new Response("Unauthorized: Invalid token", { status: 401 }); 286 | } 287 | 288 | // Check for required environment variables 289 | if (!env.GROK3_COOKIE) { 290 | return new Response("Server Error: GROK3_COOKIE environment variable is not set", { status: 500 }); 291 | } 292 | 293 | try { 294 | // Parse the request body 295 | const body = await request.json(); 296 | 297 | // Extract the messages 298 | const messages = body.messages || []; 299 | if (!messages.length) { 300 | return new Response("Bad Request: No messages provided", { status: 400 }); 301 | } 302 | 303 | // Get the last user message 304 | const lastUserMessage = messages.filter(m => m.role === "user").pop(); 305 | if (!lastUserMessage) { 306 | return new Response("Bad Request: No user message found", { status: 400 }); 307 | } 308 | 309 | // Initialize the Grok client 310 | const grokClient = new GrokClient(env.GROK3_COOKIE); 311 | 312 | // Determine if streaming is requested 313 | const stream = body.stream === true; 314 | 315 | if (stream) { 316 | // Handle streaming response 317 | const grokStream = await grokClient.sendMessage(lastUserMessage.content, true); 318 | const openAIStream = createOpenAIStreamingResponse(grokStream, crypto.randomUUID()); 319 | 320 | return new Response(openAIStream, { 321 | headers: { 322 | "Content-Type": "text/event-stream", 323 | "Cache-Control": "no-cache", 324 | "Connection": "keep-alive" 325 | } 326 | }); 327 | } else { 328 | // Handle normal response 329 | const response = await grokClient.sendMessage(lastUserMessage.content); 330 | const openAIResponse = createOpenAIFullResponse(response); 331 | 332 | return new Response(JSON.stringify(openAIResponse), { 333 | headers: { 334 | "Content-Type": "application/json" 335 | } 336 | }); 337 | } 338 | } catch (error) { 339 | return new Response(`Error: ${error.message}`, { status: 500 }); 340 | } 341 | } 342 | 343 | // Export the fetch handler for Cloudflare Workers 344 | export default { 345 | async fetch(request, env, ctx) { 346 | return handleRequest(request, env); 347 | } 348 | }; 349 | --------------------------------------------------------------------------------