ChatGPT model picker + extras

A script that seemlessly adds a model picker to the ChatGPT UI

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         ChatGPT model picker + extras
// @author       Worthy
// @match        https://chatgpt.com/
// @match        https://chatgpt.com/c/*
// @grant        unsafeWindow
// @grant        GM.getValue
// @grant        GM.setValue
// @run-at       document-start
// @version      1.02
// @license      MIT
// @namespace https://greasyfork.org/users/1569445
// @description A script that seemlessly adds a model picker to the ChatGPT UI
// ==/UserScript==

(function() {
    'use strict';

    // Default state
    let selectedModel = "gpt-5-2";
    let currentButtonName = "5.2 Instant";

    // Async load saved preferences
    (async () => {
        selectedModel = await GM.getValue("model", "gpt-5-2");
        currentButtonName = await GM.getValue("modelLabel", "5.2 Instant");
    })();

    /* ==========================================================================================
       THE BRAINS (Logic from Script 2)
       ========================================================================================== */

    function patchPayload(jsonString) {
        try {
            const body = JSON.parse(jsonString);

            // 1. Force top-level model identifiers
            body.model = selectedModel;
            if (body.model_slug) body.model_slug = selectedModel;

            // 2. Force conversation_mode (The critical fix)
            if (body.conversation_mode) {
                if (typeof body.conversation_mode === 'object') {
                    body.conversation_mode.model = selectedModel;
                    if (!body.conversation_mode.kind) {
                         body.conversation_mode.kind = "primary_assistant";
                    }
                }
            } else {
                body.conversation_mode = {
                    kind: "primary_assistant",
                    model: selectedModel
                };
            }

            // 3. Nuke specific requirements that might reset the model
            if (body.requirements) {
                 delete body.requirements.lat;
                 delete body.requirements.long;
            }

            console.log(`[Model Switcher] Patched request to: ${selectedModel}`);
            return JSON.stringify(body);

        } catch (e) {
            console.error("Payload Patch Error:", e);
            return jsonString;
        }
    }

    const originalFetch = unsafeWindow.fetch;
    unsafeWindow.fetch = async function (input, init) {
        // Case 1: Standard fetch(url, options)
        if (typeof input === "string" && input.includes("/conversation") && init && init.body) {
            init.body = patchPayload(init.body);
        }
        // Case 2: fetch(Request) - frequently used by newer React builds
        else if (input instanceof Request && input.url.includes("/conversation")) {
            try {
                const clone = input.clone();
                const text = await clone.text();
                const newBody = patchPayload(text);

                input = new Request(input, {
                    body: newBody,
                    method: input.method,
                    headers: input.headers,
                    referrer: input.referrer,
                    referrerPolicy: input.referrerPolicy,
                    mode: input.mode,
                    credentials: input.credentials,
                    cache: input.cache,
                    redirect: input.redirect,
                    integrity: input.integrity,
                });
            } catch (err) {
                console.error("Failed to patch Request object:", err);
            }
        }
        return originalFetch.call(this, input, init);
    };


    /* ==========================================================================================
       THE LOOKS (UI from Script 1)
       ========================================================================================== */

    function refreshUI() {
        // Remove original header label if it exists
        const originalLabel = document.querySelector('main .sticky.top-0 h1, main .sticky.top-0 div button[role="combobox"]');
        if (originalLabel) { originalLabel.style.display = 'none'; }

        // Find the insertion point
        const mainHeader = document.querySelector('main .sticky.top-0') || document.querySelector('header.sticky');
        if (!mainHeader || document.getElementById('phantom-naming-picker')) return;

        // Create Container
        const pickerWrap = document.createElement('div');
        pickerWrap.id = 'phantom-naming-picker';
        pickerWrap.style.cssText = "position: absolute; left: 14px; top: 50%; transform: translateY(-50%); z-index: 9999; background-color: #212121; border-radius: 8px;";

        // SVG Fixed: Removed the extra quotes and brackets from your original snippet
        pickerWrap.innerHTML = `
            <div style="position: relative; font-family: Söhne Buch, ui-sans-serif, system-ui;">
                <button id="phantom-btn" style="background: #212121; border: none; color: #ffffff; font-weight: 400; font-size: 18px; cursor: pointer; display: flex; align-items: center; gap: 4px; padding: 6px 3.5px; border-radius: 8px; white-space: nowrap;">
                    ChatGPT <span id="current-model-label" style="color: #b4b4b4; margin-left: 2px;">${currentButtonName}</span>
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="opacity: 0.5; margin-left: 4px;"><path d="m6 9 6 6 6-6"/></svg>
                </button>

                <div id="phantom-menu" style="display: none; position: absolute; top: 115%; left: 0; background: #2f2f2f; border: 1px solid #424242; border-radius: 12px; padding: 4px; width: 220px; box-shadow: 0 10px 25px rgba(0,0,0,0.5);">
                    <div style="padding: 8px 12px; font-size: 11px; font-weight: 600; color: #8e8e93; text-transform: uppercase; letter-spacing: 0.5px;">Latest models</div>

                    ${createCategory("GPT-5.2", [
                        {id: 'gpt-5-2', name: 'GPT-5.2 Instant', display: '5.2 Instant', desc: 'Responds quickly for general chat'},
                        {id: 'gpt-5-2-thinking', name: 'GPT-5.2 Thinking', display: '5.2 Thinking', desc: 'Uses advanced reasoning'},
                        {id: 'gpt-5-2-pro', name: 'GPT-5.2 Pro', display: '5.2 Pro', desc: 'Best at reasoning'},
                        {id: 'auto', name: 'GPT-5.2 Auto', display: '5.2 Auto', desc: 'Automatically adjusts thinking power'}
                    ])}
                    ${createCategory("GPT-5.1", [
                        {id: 'gpt-5-1', name: 'GPT-5.1 Instant', display: '5.1 Instant', desc: 'Great for general chat'},
                        {id: 'gpt-5-1-thinking', name: 'GPT-5.1 Thinking', display: '5.1 Thinking', desc: 'Uses advanced reasoning'},
                        {id: 'gpt-5-1-pro', name: 'GPT-5.1 Pro', display: '5.1 Pro', desc: 'Previous best at reasoning'}
                    ])}
                    ${createCategory("GPT-5", [
                        {id: 'gpt-5', name: 'GPT-5 Instant', display: '5 Instant', desc: 'Previous flagship for general chat'},
                        {id: 'gpt-5-thinking', name: 'GPT-5 Thinking', display: '5 Thinking', desc: 'Uses advanced reasoning'},
                        {id: 'gpt-5-pro', name: 'GPT-5 Pro', display: '5 Pro', desc: 'Previous best at reasoning'},
                        {id: 'gpt-5-mini', name: 'GPT-5 Mini', display: '5-mini', desc: 'Faster for everyday tasks'}
                    ])}

                    <div style="height: 1px; background: #424242; margin: 4px 8px;"></div>
                    <div style="padding: 8px 12px; font-size: 11px; font-weight: 600; color: #8e8e93; text-transform: uppercase; letter-spacing: 0.5px;">Legacy models</div>

                    ${createCategory("Chat", [
                        {id: 'gpt-4o', name: 'GPT-4o', display: '4o', desc: 'Great for most tasks'},
                        {id: 'gpt-4-1', name: 'GPT-4.1', display: '4.1', desc: 'Great for quick coding'},
                        {id: 'gpt-4-5', name: 'GPT-4.5', display: '4.5', desc: 'Powerful for creative writing'},
                        {id: 'gpt-4-1-mini', name: 'GPT-4.1-mini', display: '4.1-mini', desc: 'Faster for structured tasks'},
                        {id: 'gpt-4o-mini', name: 'GPT-4o-mini', display: '4o-mini', desc: 'Faster for general tasks'}
                    ])}
                    ${createCategory("Thinking", [
                        {id: 'o3', name: 'o3', display: 'o3', desc: 'Uses advanced reasoning'},
                        {id: 'o4-mini', name: 'o4-mini', display: 'o4-mini', desc: 'Fastest at reasoning'},
                        {id: 'o3-mini', name: 'o3-mini', display: 'o3-mini', desc: 'Previous fastest at reasoning'},
                        {id: 'o1', name: 'o1', display: 'o1', desc: 'First to do reasoning'},
                        {id: 'o1-mini', name: 'o1-mini', display: 'o1-mini', desc: 'First at speed-optimised reasoning'},
                    ])}
                    ${createCategory("Ultra-legacy", [
                        {id: 'gpt-4-turbo', name: 'GPT-4-Turbo', display: '4 Turbo', desc: 'Faster for high-intelligence tasks'},
                        {id: 'gpt-4', name: 'GPT-4', display: '4', desc: 'Made for high-intelligence tasks'},
                        {id: 'gpt-3-5', name: 'GPT-3.5', display: '3.5', desc: 'First to be used in ChatGPT'},
                        {id: 'text-davinci-001', name: 'GPT-3', display: '3', desc: 'First to have human-like speech'},
                        {id: 'gpt2', name: 'GPT-2', display: '2', desc: 'First large-scale GPT'},
                        {id: 'openai-gpt', name: 'GPT-1', display: '1', desc: 'First ever GPT'}
                    ])}
                </div>
            </div>
        `;

        mainHeader.appendChild(pickerWrap);
        setupLogic();
    }

    function createCategory(title, models) {
        let subItems = models.map(m => `
            <div class="model-opt" data-id="${m.id}" data-btn-label="${m.display}" style="padding: 8px 12px; cursor: pointer; border-radius: 8px; color: #ececf1; display: flex; flex-direction: column; white-space: nowrap;">
                <span style="font-weight: 500; font-size: 13px;">${m.name}</span>
                <span style="font-size: 10px; color: #b4b4b4;">${m.desc}</span>
            </div>
        `).join('');

        return `
            <div class="cat-item" style="position: relative; padding: 10px 12px; cursor: default; border-radius: 8px; color: #ececf1; font-size: 14px; display: flex; align-items: center; justify-content: space-between;">
                ${title}
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" style="opacity: 0.5;"><path d="m9 18 6-6-6-6"/></svg>
                <div class="sub-menu" style="display: none; position: absolute; left: 98%; top: -4px; background: #2f2f2f; border: 1px solid #424242; border-radius: 12px; padding: 4px; width: 230px; box-shadow: 10px 10px 25px rgba(0,0,0,0.5);">
                    ${subItems}
                </div>
            </div>
        `;
    }

    function setupLogic() {
        const btn = document.getElementById('phantom-btn');
        const menu = document.getElementById('phantom-menu');
        const label = document.getElementById('current-model-label');

        btn.onclick = (e) => { e.stopPropagation(); menu.style.display = menu.style.display === 'none' ? 'block' : 'none'; };
        document.addEventListener('click', () => menu.style.display = 'none');

        document.querySelectorAll('.cat-item').forEach(cat => {
            const sub = cat.querySelector('.sub-menu');
            cat.onmouseenter = () => { sub.style.display = 'block'; cat.style.background = '#3e3e3e'; };
            cat.onmouseleave = () => { sub.style.display = 'none'; cat.style.background = 'none'; };
        });

        document.querySelectorAll('.model-opt').forEach(opt => {
            opt.onmouseover = (e) => { e.stopPropagation(); opt.style.background = '#4e4e4e'; };
            opt.onmouseout = () => opt.style.background = 'none';
            opt.onclick = async (e) => {
                e.stopPropagation();

                // Update State
                selectedModel = opt.getAttribute('data-id');
                currentButtonName = opt.getAttribute('data-btn-label');

                // Update UI
                label.innerText = currentButtonName;
                menu.style.display = 'none';

                // Save to Storage (So it remembers after refresh)
                await GM.setValue("model", selectedModel);
                await GM.setValue("modelLabel", currentButtonName);

                console.log(`[UI] Switched to ${selectedModel}`);
            };
        });
    }

    // Keep the UI alive
    setInterval(refreshUI, 500);
})();