Grok Model Switcher

Allows switching between Grok-3 and Grok-2 models on grok.com by patching POST requests, with a header UI for model selection and rate limit display.

  1. // ==UserScript==
  2. // @name Grok Model Switcher
  3. // @description Allows switching between Grok-3 and Grok-2 models on grok.com by patching POST requests, with a header UI for model selection and rate limit display.
  4. // @author James007
  5. // @namespace https://greasyfork.org/users/1463345-james007
  6. // @version 1.6
  7. // @match https://grok.com/*
  8. // @icon https://grok.com/images/favicon-light.png
  9. // @license MIT
  10. // @grant none
  11. // @run-at document-end
  12. // @homepageURL https://greasyfork.org/scripts/your-script-id
  13. // @supportURL https://greasyfork.org/scripts/your-script-id/feedback
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. const generateId = () => Math.random().toString(16).slice(2);
  20.  
  21. const createMenu = () => {
  22. const menu = document.createElement('div');
  23. menu.id = 'grok-switcher-menu';
  24. menu.className = 'flex flex-row items-center gap-2';
  25. menu.innerHTML = `
  26. <div class="flex flex-col">
  27. <div id="rate_limit_grok3" class="text-gray-400 text-xs">grok-3: N/A</div>
  28. <div id="rate_limit_grok2" class="text-gray-400 text-xs">grok-2: N/A</div>
  29. </div>
  30. <button id="toggle_model" class="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring transition-colors duration-100 bg-blue-600 hover:bg-blue-700 text-white h-8 px-3 rounded-full">Using grok-3</button>
  31. `;
  32. return menu;
  33. };
  34.  
  35. const insertMenu = () => {
  36. const headerContainer = document.querySelector('.absolute.flex.flex-row.items-center.gap-0\\.5.ml-auto.end-3');
  37. if (headerContainer) {
  38. const menu = createMenu();
  39. headerContainer.insertBefore(menu, headerContainer.firstChild);
  40. return menu;
  41. } else {
  42. console.error('Header container not found, retrying...');
  43. return null;
  44. }
  45. };
  46.  
  47. const waitForHeader = (callback) => {
  48. const maxAttempts = 20;
  49. let attempts = 0;
  50.  
  51. const tryInsert = () => {
  52. const menu = insertMenu();
  53. if (menu) {
  54. callback(menu);
  55. } else if (attempts < maxAttempts) {
  56. attempts++;
  57. setTimeout(tryInsert, 500); // Retry every 500ms
  58. } else {
  59. console.error('Failed to find header container after max attempts, falling back to body');
  60. const menu = createMenu();
  61. document.body.appendChild(menu);
  62. callback(menu);
  63. }
  64. };
  65.  
  66. tryInsert();
  67. };
  68.  
  69. const updateRateLimits = (limits) => {
  70. const grok3Elem = document.getElementById('rate_limit_grok3');
  71. const grok2Elem = document.getElementById('rate_limit_grok2');
  72. grok3Elem.textContent = limits?.['grok-3']
  73. ? `grok-3: ${limits['grok-3'].remainingQueries}/${limits['grok-3'].totalQueries}`
  74. : 'grok-3: N/A';
  75. grok2Elem.textContent = limits?.['grok-2']
  76. ? `grok-2: ${limits['grok-2'].remainingQueries}/${limits['grok-2'].totalQueries}`
  77. : 'grok-2: N/A';
  78. };
  79.  
  80. const fetchRateLimits = async () => {
  81. try {
  82. const models = ['grok-3', 'grok-2'];
  83. const limits = {};
  84. for (const model of models) {
  85. const response = await fetch('https://grok.com/rest/rate-limits', {
  86. method: 'POST',
  87. headers: {
  88. 'Content-Type': 'application/json',
  89. 'X-Xai-Request-Id': generateId(),
  90. 'Accept-Language': 'en-US,en;q=0.9',
  91. 'User-Agent': navigator.userAgent,
  92. 'Accept': '*/*',
  93. 'Origin': 'https://grok.com',
  94. 'Sec-Fetch-Site': 'same-origin',
  95. 'Sec-Fetch-Mode': 'cors',
  96. 'Sec-Fetch-Dest': 'empty',
  97. 'Referer': 'https://grok.com/',
  98. 'Accept-Encoding': 'gzip, deflate, br',
  99. 'Priority': 'u=1, i'
  100. },
  101. body: JSON.stringify({ requestKind: 'DEFAULT', modelName: model })
  102. });
  103. if (!response.ok) throw new Error(`Failed to fetch ${model} rate limits`);
  104. limits[model] = await response.json();
  105. }
  106. updateRateLimits(limits);
  107. return limits;
  108. } catch (error) {
  109. updateRateLimits(null);
  110. alert('Failed to fetch rate limits. Please try again later.');
  111. }
  112. };
  113.  
  114. const startRateLimitRefresh = () => {
  115. fetchRateLimits();
  116. setInterval(fetchRateLimits, 30000);
  117. };
  118.  
  119. const createPatcher = () => {
  120. const originalFetch = window.fetch;
  121. const originalXhrOpen = XMLHttpRequest.prototype.open;
  122. const originalXhrSend = XMLHttpRequest.prototype.send;
  123. let grok2Active = false;
  124.  
  125. const isTargetUrl = (url) => {
  126. return (url.includes('/rest/app-chat/conversations/') && url.endsWith('/responses')) ||
  127. url === 'https://grok.com/rest/app-chat/conversations/new';
  128. };
  129.  
  130. const patchFetch = async (input, init) => {
  131. if (grok2Active && init?.method === 'POST' && typeof input === 'string' && isTargetUrl(input)) {
  132. try {
  133. const payload = JSON.parse(init.body);
  134. payload.modelName = 'grok-2';
  135. init.body = JSON.stringify(payload);
  136. } catch (error) {
  137. alert('Failed to patch fetch request.');
  138. }
  139. }
  140. return originalFetch(input, init);
  141. };
  142.  
  143. const patchXhrOpen = function(method, url) {
  144. this._url = url;
  145. this._method = method;
  146. return originalXhrOpen.apply(this, arguments);
  147. };
  148.  
  149. const patchXhrSend = function(body) {
  150. if (grok2Active && this._method === 'POST' && isTargetUrl(this._url)) {
  151. try {
  152. const payload = JSON.parse(body);
  153. payload.modelName = 'grok-2';
  154. body = JSON.stringify(payload);
  155. } catch (error) {
  156. alert('Failed to patch XHR request.');
  157. }
  158. }
  159. return originalXhrSend.call(this, body);
  160. };
  161.  
  162. return {
  163. enable: async () => {
  164. grok2Active = true;
  165. window.fetch = patchFetch;
  166. XMLHttpRequest.prototype.open = patchXhrOpen;
  167. XMLHttpRequest.prototype.send = patchXhrSend;
  168. await fetchRateLimits();
  169. },
  170. disable: async () => {
  171. grok2Active = false;
  172. window.fetch = originalFetch;
  173. XMLHttpRequest.prototype.open = originalXhrOpen;
  174. XMLHttpRequest.prototype.send = originalXhrSend;
  175. await fetchRateLimits();
  176. },
  177. isActive: () => grok2Active
  178. };
  179. };
  180.  
  181. const init = () => {
  182. const tailwind = document.createElement('script');
  183. tailwind.src = 'https://cdn.tailwindcss.com';
  184. tailwind.onerror = () => alert('Failed to load TailwindCSS. Some styles may not work.');
  185. document.head.appendChild(tailwind);
  186.  
  187. const patcher = createPatcher();
  188.  
  189. const setupMenu = (menu) => {
  190. const toggleButton = document.getElementById('toggle_model');
  191. toggleButton.addEventListener('click', async () => {
  192. try {
  193. if (patcher.isActive()) {
  194. await patcher.disable();
  195. toggleButton.textContent = 'Using grok-3';
  196. toggleButton.classList.replace('bg-red-600', 'bg-blue-600');
  197. toggleButton.classList.replace('hover:bg-red-700', 'hover:bg-blue-700');
  198. } else {
  199. await patcher.enable();
  200. toggleButton.textContent = 'Using grok-2';
  201. toggleButton.classList.replace('bg-blue-600', 'bg-red-600');
  202. toggleButton.classList.replace('hover:bg-blue-700', 'hover:bg-red-700');
  203. }
  204. } catch (error) {
  205. alert('Failed to toggle model. Please try again.');
  206. }
  207. });
  208. startRateLimitRefresh();
  209. };
  210.  
  211. tailwind.onload = () => {
  212. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  213. waitForHeader(setupMenu);
  214. } else {
  215. document.addEventListener('DOMContentLoaded', () => waitForHeader(setupMenu));
  216. }
  217. };
  218. };
  219.  
  220. init();
  221. })();