Greasy Fork is available in English.

Grok+

Adds back Grok 2 and shows rate limits

  1. // ==UserScript==
  2. // @name Grok+
  3. // @namespace https://6942020.xyz/
  4. // @version 1.5
  5. // @description Adds back Grok 2 and shows rate limits
  6. // @author WadeGrimridge
  7. // @match https://grok.com/*
  8. // @license MIT
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. "use strict";
  14.  
  15. const CONFIG = {
  16. MAX_RETRIES: 10,
  17. RETRY_DELAY: 1000,
  18. RATE_LIMIT_ENDPOINT: "/rest/rate-limits",
  19. REQUEST_KINDS: ["DEFAULT", "REASONING", "DEEPSEARCH", "DEEPERSEARCH"],
  20. MODELS: {
  21. "grok-2": { displayName: "Grok 2" },
  22. "grok-3": {
  23. DEFAULT: "Grok 3",
  24. REASONING: "Think",
  25. DEEPSEARCH: "DeepSearch",
  26. DEEPERSEARCH: "DeeperSearch",
  27. },
  28. },
  29. };
  30.  
  31. const state = {
  32. rateInfoElement: null,
  33. selectedModel: "grok-3",
  34. modelRateLimits: {
  35. "grok-2": null,
  36. "grok-3": {
  37. DEFAULT: null,
  38. REASONING: null,
  39. DEEPSEARCH: null,
  40. DEEPERSEARCH: null,
  41. },
  42. },
  43. };
  44.  
  45. const formatTime = (seconds) => {
  46. const hours = Math.floor(seconds / 3600);
  47. const minutes = Math.floor((seconds % 3600) / 60);
  48. const remainingSeconds = seconds % 60;
  49.  
  50. if (hours > 0) {
  51. return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
  52. }
  53. return remainingSeconds > 0
  54. ? `${minutes}m ${remainingSeconds}s`
  55. : `${minutes}m`;
  56. };
  57.  
  58. const isValidRateData = (data) =>
  59. data &&
  60. typeof data.remainingQueries === "number" &&
  61. typeof data.totalQueries === "number" &&
  62. (typeof data.windowSizeSeconds === "number" ||
  63. typeof data.waitTimeSeconds === "number");
  64.  
  65. const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  66.  
  67. const fetchRateLimit = async (
  68. modelName,
  69. requestKind = "DEFAULT",
  70. attempt = 1
  71. ) => {
  72. try {
  73. const response = await fetch(CONFIG.RATE_LIMIT_ENDPOINT, {
  74. method: "POST",
  75. headers: { "Content-Type": "application/json" },
  76. body: JSON.stringify({ requestKind, modelName }),
  77. });
  78.  
  79. if (response.status !== 200 && attempt <= CONFIG.MAX_RETRIES) {
  80. await sleep(CONFIG.RETRY_DELAY);
  81. return fetchRateLimit(modelName, requestKind, attempt + 1);
  82. }
  83.  
  84. const data = await response.json();
  85. if (!isValidRateData(data)) return;
  86.  
  87. updateRateInfo(data, modelName, requestKind);
  88. } catch (error) {
  89. console.error("[grok-ratelimit] Rate limit fetch failed:", error);
  90. if (attempt > CONFIG.MAX_RETRIES && state.rateInfoElement) {
  91. state.rateInfoElement.textContent = "Couldn't fetch ratelimit info";
  92. }
  93. }
  94. };
  95.  
  96. const formatRateLimitLine = (data, displayName) => {
  97. const timeStr = data.waitTimeSeconds
  98. ? formatTime(data.waitTimeSeconds)
  99. : formatTime(data.windowSizeSeconds);
  100. return `${displayName}: ${data.remainingQueries}/${data.totalQueries} (${timeStr})`;
  101. };
  102.  
  103. const updateRateInfo = (data, modelName, requestKind = "DEFAULT") => {
  104. if (!state.rateInfoElement) return;
  105.  
  106. if (modelName === "grok-3") {
  107. state.modelRateLimits[modelName][requestKind] = data;
  108. } else {
  109. state.modelRateLimits[modelName] = data;
  110. }
  111.  
  112. const lines = [];
  113.  
  114. CONFIG.REQUEST_KINDS.forEach((kind) => {
  115. const modelData = state.modelRateLimits["grok-3"][kind];
  116. if (modelData) {
  117. lines.push(
  118. formatRateLimitLine(modelData, CONFIG.MODELS["grok-3"][kind])
  119. );
  120. }
  121. });
  122.  
  123. const grok2Data = state.modelRateLimits["grok-2"];
  124. if (grok2Data) {
  125. lines.push(
  126. formatRateLimitLine(grok2Data, CONFIG.MODELS["grok-2"].displayName)
  127. );
  128. }
  129.  
  130. state.rateInfoElement.textContent = lines.join(" | ");
  131. };
  132.  
  133. const interceptFetch = () => {
  134. const originalFetch = window.fetch;
  135. window.fetch = async function (...args) {
  136. const [resource, options] = args;
  137. const url =
  138. resource instanceof Request ? resource.url : resource.toString();
  139.  
  140. const isChatUrl = (url) => {
  141. return (
  142. (url.includes("/rest/app-chat/conversations/") &&
  143. url.endsWith("/responses")) ||
  144. url === "https://grok.com/rest/app-chat/conversations/new"
  145. );
  146. };
  147.  
  148. if (options?.method === "POST" && isChatUrl(url)) {
  149. try {
  150. const body = JSON.parse(options.body);
  151. if (body.modelName && state.selectedModel === "grok-2") {
  152. const newOptions = { ...options };
  153. body.modelName = "grok-2";
  154. newOptions.body = JSON.stringify(body);
  155. args[1] = newOptions;
  156. }
  157. } catch {}
  158. }
  159.  
  160. if (!url.includes(CONFIG.RATE_LIMIT_ENDPOINT)) {
  161. return originalFetch.apply(this, args);
  162. }
  163.  
  164. const response = await originalFetch.apply(this, args);
  165. const { modelName, requestKind } = JSON.parse(options.body);
  166. const clone = response.clone();
  167. clone.json().then((data) => {
  168. if (isValidRateData(data)) {
  169. updateRateInfo(data, modelName, requestKind);
  170. }
  171. });
  172.  
  173. return response;
  174. };
  175. };
  176.  
  177. const createRateInfoElement = () => {
  178. const targetDiv = document.querySelector(
  179. 'main div:has(> a[aria-label="Home page"])'
  180. );
  181. if (!targetDiv || state.rateInfoElement) return;
  182.  
  183. const headerDiv = targetDiv.parentElement;
  184. headerDiv.classList.remove(
  185. "@[80rem]/nav:h-0",
  186. "@[80rem]/nav:top-8",
  187. "@[80rem]/nav:from-transparent",
  188. "@[80rem]/nav:via-transparent"
  189. );
  190.  
  191. state.rateInfoElement = document.createElement("div");
  192. state.rateInfoElement.className = "ml-2 text-sm break-words";
  193. state.rateInfoElement.style.maxWidth = "calc(100vw - 240px)";
  194. state.rateInfoElement.textContent = "Fetching ratelimit info...";
  195. targetDiv.appendChild(state.rateInfoElement);
  196.  
  197. initializeRateLimits();
  198. };
  199.  
  200. const initializeRateLimits = async () => {
  201. await fetchRateLimit("grok-3", "DEFAULT");
  202. for (const kind of CONFIG.REQUEST_KINDS.slice(1)) {
  203. await sleep(100);
  204. await fetchRateLimit("grok-3", kind);
  205. }
  206. await sleep(100);
  207. await fetchRateLimit("grok-2");
  208. };
  209.  
  210. const waitForElement = () => {
  211. const targetDiv = document.querySelector(
  212. 'main div:has(> a[aria-label="Home page"])'
  213. );
  214. if (targetDiv) {
  215. createRateInfoElement();
  216. } else {
  217. requestAnimationFrame(waitForElement);
  218. }
  219. };
  220.  
  221. const createModelPickerOverlay = () => {
  222. if (document.getElementById("model-picker-overlay")) return;
  223. const overlay = document.createElement("div");
  224. overlay.id = "model-picker-overlay";
  225. Object.assign(overlay.style, {
  226. position: "fixed",
  227. bottom: "16px",
  228. right: "16px",
  229. backgroundColor: "white",
  230. border: "1px solid #ccc",
  231. borderRadius: "8px",
  232. padding: "8px",
  233. display: "flex",
  234. gap: "8px",
  235. zIndex: "10000",
  236. fontSize: "14px",
  237. });
  238. const makeButton = (model, label) => {
  239. const btn = document.createElement("button");
  240. btn.textContent = label;
  241. btn.dataset.model = model;
  242. btn.style.padding = "4px 8px";
  243. btn.style.cursor = "pointer";
  244. btn.style.border = "1px solid #888";
  245. btn.style.borderRadius = "4px";
  246. btn.style.backgroundColor =
  247. state.selectedModel === model ? "#ddd" : "white";
  248. btn.addEventListener("click", () => {
  249. state.selectedModel = model;
  250. overlay.querySelectorAll("button").forEach((b) => {
  251. b.style.backgroundColor =
  252. b.dataset.model === model ? "#ddd" : "white";
  253. });
  254. });
  255. return btn;
  256. };
  257. overlay.appendChild(makeButton("grok-3", "Grok 3"));
  258. overlay.appendChild(makeButton("grok-2", "Grok 2"));
  259. document.body.appendChild(overlay);
  260. };
  261.  
  262. interceptFetch();
  263. waitForElement();
  264. createModelPickerOverlay();
  265. })();