ChatGPT Rate Limit - Frontend

A tool to know your ChatGPT Rate Limit.

Per 08-08-2025. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         ChatGPT Rate Limit - Frontend
// @namespace    http://terase.cn
// @license      MIT
// @version      2.5
// @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",
    "gpt-5-thinking": "GPT-5-Thinking",
    "5 Thinking": "GPT-5-Thinking",
}

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>')
    
    model_bar.append(displayBar);
    return true;
}
function tryAddFrontendItems() {
    if (addFrontendItems()) {
        // console.log("Frontend items added");
        updateAll();
    }
}

setInterval(updateAll, 60000); // Refresh every 60s
setInterval(tryAddFrontendItems, 200); // Make sure the bar is always there


// ====== Model Switcher ======

var mapping = {
    // '1': 'GPT-4.1', // 1
    // '3': 'o3', // 3
    // '4': 'o4-mini-high', // 4
    '5': ['GPT-5', 'GPT-5 Thinking'],
};

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) {
            targets[span.textContent] = 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] div.grow');
        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
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);
    }
});

})();