// ==UserScript==
// @name ChatGPT Rate Limit - Frontend
// @namespace http://terase.cn
// @license MIT
// @version 3.1
// @description A tool to know your ChatGPT Rate Limit.
// @author Terrasse
// @match https://chatgpt.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com
// @sandbox RAW
// @grant none
// ==/UserScript==
(function() {
'use strict';
window.model_status = {
"o3": -1,
// "o4-mini-high": -1,
"o4-mini": -1,
// "GPT-4.5": -1,
"GPT-5": -1,
"GPT-5-Thinking": -1,
}
window.devarious = {
// "gpt-4-5": "GPT-4.5",
// "4.5": "GPT-4.5",
"gpt-5": "GPT-5",
"5": "GPT-5",
"Auto": "GPT-5",
"gpt-5-thinking": "GPT-5-Thinking",
"5 Thinking": "GPT-5-Thinking",
"Thinking": "GPT-5-Thinking",
}
function createTooltipHTML() {
// Management section
const managementItems = `
<dt class="text-token-text-tertiary col-span-2">Management</dt>
<dt>Remove API key</dt>
<dd class="text-token-text-secondary justify-self-end">Click Me 5x</dd>
`;
// Model switching section
const shortcuts = [];
for (const [key, model] of Object.entries(mapping)) {
let description;
if (Array.isArray(model)) {
description = `Switch in ${model.join('/')}`;
} else {
description = `Switch to ${model}`;
}
shortcuts.push({ key: `Ctrl+Alt+${key}`, description });
}
let shortcutItems = '<dt class="text-token-text-tertiary col-span-2">Model Switching</dt>';
shortcuts.forEach(shortcut => {
const keyParts = shortcut.key.split('+');
const kbdElements = keyParts.map(part =>
`<kbd><span class="min-w-[1em]">${part}</span></kbd>`
).join('');
shortcutItems += `
<dt>${shortcut.description}</dt>
<dd class="text-token-text-secondary justify-self-end">
<div class="inline-flex whitespace-pre *:inline-flex *:font-sans *:not-last:after:px-0.5 *:not-last:after:content-['+']">
${kbdElements}
</div>
</dd>
`;
});
// Combine all sections
const header = `
<div class="ms-2.5 flex h-9 items-center font-semibold">
<h2 class="text-sm">ChatGPT Rate Limit</h2>
</div>
`;
const contentList = `
<dl class="grid [grid-template-columns:minmax(0,1fr)_max-content] gap-x-6 gap-y-3 px-2.5 pb-2">
${managementItems}
${shortcutItems}
</dl>
`;
const tooltipClasses = "z-50 max-w-xs rounded-2xl popover bg-token-main-surface-primary dark:bg-[#353535] shadow-long py-1.5 px-1.5 select-none text-sm";
return `
<div id="crl_tooltip" class="${tooltipClasses}" style="position: fixed;">
${header}
${contentList}
</div>
`.replace(/\s+/g, ' ').trim();
}
function getCurrentModel() {
var bar = document.getElementById("crl_bar");
var model = bar.previousElementSibling.innerText;
if (model in window.devarious) {
model = window.devarious[model];
}
return model;
}
function updateStatusText() {
var status = window.model_status;
// var text = "";
// for (const model in status) {
// text += `${model}: ${status[model]}; `;
// }
// text = text.slice(0, -2);
var model = getCurrentModel();
var remain = "∞";
if (model in status) {
remain = `${status[model]}`;
}
var text = ` [${remain}]`;
var bar = document.getElementById("crl_bar");
if (bar) {
bar.innerText = text;
}
}
(function(fetch) {
window.fetch = function(input, init) {
var method = 'GET';
var url = '';
var payload = null;
if (typeof input === 'string') {
url = input;
} else if (input instanceof Request) {
url = input.url;
method = input.method || method;
payload = input.body || null;
} else {
console.log(`Unexpected input of type ${typeof input}: ${input}`);
}
if (init) {
method = init.method || method;
payload = init.body || payload;
}
// console.log(`Request: ${method} ${url}`);
if (method.toUpperCase() === 'POST') {
if (url.endsWith("/backend-api/f/conversation")) {
// console.log("Conversation Request");
payload = JSON.parse(payload);
var model = payload.model;
if (model in window.devarious) {
model = window.devarious[model];
}
window.postMessage({ model: model, type: "put" }, window.location.origin);
}
}
return fetch.apply(this, arguments);
};
})(window.fetch);
function receiveMessage(event) { // Accept: type="status"
if (event.origin !== window.location.origin) return;
if (event.data.type !== "status") return;
var msg = event.data;
// console.log('MAIN_WORLD 收到消息:', msg);
var status = window.model_status;
if (msg.model in status) {
status[msg.model] = msg.remain;
updateStatusText();
} else {
console.log(`Unknown model from backend: ${msg.model}, msg: ${msg}, event: ${event}`, event);
}
}
window.addEventListener('message', receiveMessage, false);
function updateAll() {
// console.log("Update All");
for (const model in window.model_status) {
window.postMessage({ model: model, type: "get" }, window.location.origin);
}
}
// Display & Refresh Button
function htmlToNode(html) {
const template = document.createElement('template');
template.innerHTML = html;
return template.content.firstChild;
}
function getModelBarFlexible() {
// there are 2 model bar (responsive), we need the visible one
const model_bars = document.querySelectorAll("button[data-testid='model-switcher-dropdown-button']");
for (const model_bar of model_bars) {
// if (window.getComputedStyle(model_bar).display !== 'none') { // not working
if (model_bar.offsetParent) { // equivalent to visible
return model_bar;
}
}
console.log("No visible model bar found", model_bars);
return null;
}
function addFrontendItems() { // return true if freshly added
var crl_bar = document.getElementById("crl_bar");
if (crl_bar) {
if (crl_bar.offsetParent === null) { // not visible
crl_bar.remove();
return false; // add back next time
}
updateStatusText();
return false;
}
// var avatar = document.querySelector('button[data-testid="profile-button"]');
// if (!avatar) return false;
// var avatarContainer = avatar.parentElement;
var model_bar = getModelBarFlexible();
if (!model_bar) return false;
model_bar = model_bar.querySelector('div');
var displayBar = htmlToNode('<span id="crl_bar" class="text-token-text-tertiary"> [...]</span>')
// var refreshButton = htmlToNode('<button onclick="updateAll();"><svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.651 7.65a7.131 7.131 0 0 0-12.68 3.15M18.001 4v4h-4m-7.652 8.35a7.13 7.13 0 0 0 12.68-3.15M6 20v-4h4"/></svg></button>')
// Add hover tooltip
addHoverTooltip(displayBar);
model_bar.append(displayBar);
return true;
}
function showTooltip(targetElement) {
// Hide existing tooltip first
hideTooltip();
const tooltipHTML = createTooltipHTML();
const tooltip = htmlToNode(tooltipHTML);
if (!tooltip) {
console.error('Failed to create tooltip');
return;
}
// Position tooltip
const rect = targetElement.getBoundingClientRect();
tooltip.style.left = rect.left + 'px';
tooltip.style.top = (rect.bottom + 5) + 'px';
document.body.appendChild(tooltip);
console.log('Tooltip shown');
}
function hideTooltip() {
const tooltip = document.getElementById('crl_tooltip');
if (tooltip) {
tooltip.remove();
console.log('Tooltip hidden');
}
}
// Global click handler for tooltip
function handleGlobalClick(event) {
const tooltip = document.getElementById('crl_tooltip');
if (tooltip && !tooltip.contains(event.target)) {
hideTooltip();
}
}
function addHoverTooltip(element) {
let hoverTimer = null;
element.addEventListener('mouseenter', function() {
hoverTimer = setTimeout(() => {
showTooltip(element);
}, 1000);
});
element.addEventListener('mouseleave', function() {
if (hoverTimer) {
clearTimeout(hoverTimer);
hoverTimer = null;
}
hideTooltip();
});
}
function tryAddFrontendItems() {
if (addFrontendItems()) {
// console.log("Frontend items added");
updateAll();
}
}
setInterval(updateAll, 60000); // Refresh every 60s
setTimeout(() => {
setInterval(tryAddFrontendItems, 200); // Make sure the bar is always there
}, 3000); // Wait for the page to load
// ====== Model Switcher ======
var mapping = {
'1': 'GPT-4.1', // 1
'3': 'o3', // 3
'4': 'o4-mini', // 4
'5': ['GPT-5', 'GPT-5-Thinking'], // 5, use name after devarious
// '5': [
// ['GPT-5', 'Auto'],
// ['GPT-5 Thinking', 'Thinking'],
// ],
'f': 'Fast',
'm': 'Thinking mini',
};
function simulateClick(element) {
const ev = new PointerEvent('pointerdown', { bubbles: true });
element.dispatchEvent(ev);
const ev2 = new PointerEvent('pointerup', { bubbles: true });
element.dispatchEvent(ev2);
}
function getModelTargets() {
// document.querySelectorAll("div[role=menuitem]")[0].querySelector("span").textContent
const menuItems = document.querySelectorAll('div[role=menuitem]');
const targets = {};
for (const item of menuItems) {
const span = item.querySelector('span');
if (span) {
let text = span.textContent;
if (text in window.devarious) {
text = window.devarious[text];
}
targets[text] = item;
}
}
return targets;
}
function switchModel(target) {
window.switch_state = 'DOING';
// expand the model switcher
const model_bar = getModelBarFlexible();
simulateClick(model_bar);
// try to switch
const do_switch = setInterval(() => {
if (window.switch_state !== 'DOING') {
clearInterval(do_switch);
return;
}
const targets = getModelTargets();
// console.log(`do_switch: ${targets}`);
if (target in targets) {
// simulateClick(targets[target]);
targets[target].click();
console.log(`Switched to ${target}`);
window.switch_state = 'DONE';
clearInterval(do_switch);
}
}, 100);
// try to expand the submenu
const do_expand = setInterval(() => {
if (window.switch_state !== 'DOING') {
clearInterval(do_expand);
return;
}
const submenu = document.querySelector('div[role=menuitem][data-has-submenu] div.grow');
// console.log(`do_expand: ${submenu.textContent}`);
if (submenu) {
// simulateClick(submenu);
submenu.click();
clearInterval(do_expand);
}
}, 100);
// after 1s, if not done, fail
setTimeout(() => {
if (window.switch_state !== 'DONE') {
console.log(`Failed to switch to ${target}`);
window.switch_state = 'DONE';
}
}, 1000);
}
function decideTarget(key) {
const model = mapping[key];
// if model is a list, toggle along with the list
if (Array.isArray(model)) {
// find the next
const current_model = getCurrentModel();
const index = model.indexOf(current_model);
return model[(index + 1) % model.length];
}
return model;
}
// monitor Ctrl+Shift+number and Ctrl+/
window.addEventListener('keydown', function(e) {
// console.log(e);
if ((e.ctrlKey || e.metaKey) && e.altKey && e.key in mapping) {
e.preventDefault();
e.stopPropagation();
const target = decideTarget(e.key);
console.log(`Switching to ${target}`);
switchModel(target);
}
// Show tooltip on Ctrl+/
if (e.ctrlKey && e.key === '/') {
e.preventDefault();
e.stopPropagation();
const crlBar = document.getElementById('crl_bar');
if (crlBar) {
const existingTooltip = document.getElementById('crl_tooltip');
if (existingTooltip) {
hideTooltip();
} else {
showTooltip(crlBar);
}
}
}
});
// Add global click listener for tooltip
document.addEventListener('click', handleGlobalClick, true);
})();