Grok+

Adds back Grok 2 and shows rate limits

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