ChatGPT model picker + extras

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

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_link:Tampermonkey}.

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 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);
})();