ChatGPT Model Switcher: Switch models

Injects a dropdown allowing you to switch LLMs during the chat

// ==UserScript==
// @name         ChatGPT Model Switcher: Switch models
// @description  Injects a dropdown allowing you to switch LLMs during the chat
// @namespace    http://github.com/c0des1ayr
// @author       c0des1ayr
// @license      MIT
// @version      1.0.1
// @match        *://chatgpt.com/*
// @icon         https://chatgpt.com/favicon.ico
// @grant        unsafeWindow
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.xmlHttpRequest
// @connect      api.github.com
// @run-at       document-idle
// ==/UserScript==

(async function() {
    'use strict';
/**
 * Fetches a JSON file from a GitHub pre-release.
 * @returns {Promise<Object>} - A promise that resolves to the JSON list of models.
 */
function getModels() {
    return new Promise((resolve, reject) => {
        const releasesUrl = `https://api.github.com/repos/c0des1ayr/openai-models-list/releases`;
        
        GM.xmlHttpRequest({
            method: 'GET',
            url: releasesUrl,
            onload: function(response) {
                if (response.status === 200) {
                    const releases = JSON.parse(response.responseText);
                    const release = releases.find(r => r.tag_name === "continuous" && r.prerelease);
                    if (release) {
                        const asset = release.assets.find(a => a.name === "models.json");
                        if (asset) {
                            GM.xmlHttpRequest({
                                method: 'GET',
                                url: asset.browser_download_url,
                                onload: function(assetResponse) {
                                    if (assetResponse.status === 200) {
                                        try {
                                            const data = JSON.parse(assetResponse.responseText);
                                            resolve(data);
                                        } catch (e) {
                                            reject('Error parsing JSON data: ' + e);
                                        }
                                    } else {
                                        reject('Failed to download asset: ' + assetResponse.status);
                                    }
                                },
                                onerror: function() {
                                    reject('Error downloading asset');
                                }
                            });
                        } else {
                            reject('Asset not found in the release');
                        }
                    } else {
                        reject('Release not found');
                    }
                } else {
                    reject('Failed to fetch releases: ' + response.status);
                }
            },
            onerror: function() {
                reject('Error fetching releases');
            }
        });
    });
}

class ModelSwitcher {
    constructor( useOther = "Original", models ) {
        this.useOther = useOther;
        this.models = models;
        this.containerSelector = '#composer-background div:nth-of-type(2) div:first-child';
    }
    
    hookFetch() {
        const originalFetch = unsafeWindow.fetch;
        unsafeWindow.fetch = async ( resource, config = {} ) => {
            if (
                resource === 'https://chatgpt.com/backend-api/conversation' &&
                config.method === 'POST' &&
                config.headers &&
                config.headers['Content-Type'] === 'application/json' &&
                config.body
            ) {
                if ( this.useOther != "Original" ) {
                    const body = JSON.parse( config.body );
                    console.log('hehe, using', this.useOther)
                    body.model = this.useOther;
                    config.body = JSON.stringify( body );
                }
            }
            return originalFetch( resource, config );
        };
    }
    
    injectDropdownStyle() {
        if ( !document.getElementById( 'dropdownCss' ) ) {
            const styleNode = document.createElement( 'style' );
            styleNode.id = 'dropdownCss';
            styleNode.type = 'text/css';
            styleNode.textContent = `.dropdown {
                position: relative;
                display: inline-block;
                width: 2.5rem;
                height: 1.5rem;
                background-color: hsl(0deg 0% 40%);
                border-radius: 25px;
                background-color: var(--dropdown-bg-color, #ccc);
                color: var(--dropdown-text-color, #000);
                border: 1px solid #666;
                padding: 0.2rem;
            }
            @media (prefers-color-scheme: dark) {
                .dropdown {
                    --dropdown-bg-color: #333;
                    --dropdown-text-color: #fff;
                }
            }
            @media (prefers-color-scheme: light) {
                .dropdown {
                    --dropdown-bg-color: #fff;
                    --dropdown-text-color: #000;
                }
            }`;
            document.head.appendChild( styleNode );
        }
    }
    
    getContainer() {
        return document.querySelector( this.containerSelector );
    }
    
    injectDropdown( container = null ) {
        console.log( 'inject' );
        if ( !container ) container = this.getContainer();
        if ( !container ) {
            console.error( 'container not found!' );
            return;
        }
        if ( container.querySelector( '#cb-dropdown' ) ) {
            console.log( '#cb-dropdown already exists' );
            return;
        }
        container.classList.add( 'items-center' );
        
        const dropdown = document.createElement( 'select' );
        dropdown.id = 'cb-dropdown';
        dropdown.className = 'dropdown'
        
        const original = document.createElement( 'option' );
        original.value = 0;
        original.text = "Original"
        
        container.appendChild( dropdown );
        dropdown.options.add( original );
        for (let i = 0; i < this.models.length; i++) {
            let option = document.createElement( 'option' );
            option.value = i + 1;
            option.text = models[i];
            dropdown.options.add( option );
        }
        
        for (let i = 0; i < dropdown.options.length; i++){
            if (dropdown.options[i].value == this.useOther){
                dropdown.options[i].selected = true;
                break;
            }
        }
        
        const cb = document.querySelector( '#cb-dropdown' );
        cb.addEventListener(
            'change', async () => {
                this.useOther = cb.options[cb.selectedIndex].text;
                console.log('Using', this.useOther)
                await GM.setValue( 'useOther', this.useOther );
            },
            false
        );
    }
    
    monitorChild( nodeSelector, callback ) {
        const node = document.querySelector( nodeSelector );
        if ( !node ) {
            console.log( `${ nodeSelector } not found!` )
            return;
        }
        const observer = new MutationObserver( mutationsList => {
            for ( const mutation of mutationsList ) {
                console.log( nodeSelector );
                callback( observer, mutation );
                break;
            }
        });
        observer.observe( node, { childList: true } );
    }
    
    __tagAttributeRecursively(selector) {
        // Select the node using the provided selector
        const rootNode = document.querySelector(selector);
        if (!rootNode) {
            console.warn(`No element found for selector: ${selector}`);
            return;
        }
        
        // Recursive function to add the "xx" attribute to the node and its children
        function addAttribute(node) {
            node.setAttribute("xxx", ""); // Add the attribute to the current node
            Array.from(node.children).forEach(addAttribute); // Recurse for all child nodes
        }
        
        addAttribute(rootNode);
    }
    
    
    monitorNodesAndInject() {
        this.monitorChild( 'body main', () => {
            this.injectDropdown();
            
            this.monitorChild( 'main div:first-child div:first-child', ( observer, mutation ) => {
                observer.disconnect();
                this.injectDropdown();
            });
        });
        
        this.monitorChild( this.containerSelector, ( observer, mutation ) => {
            observer.disconnect();
            setTimeout( () => this.injectDropdown(), 500 );
        });
        this.monitorChild( 'main div:first-child div:first-child', ( observer, mutation ) => {
            observer.disconnect();
            this.injectDropdown();
        });
    }
}

const useOther = await GM.getValue( 'useOther', "Original" );
const models = await (async () => {
    do {
        try {
            const oai = await getModels();
            return oai.models;
        } catch (error) {
            console.error('Error fetching data:', error);
        }
    } while (true);
})();
if (models === false) {return}
const switcher = new ModelSwitcher( useOther, models );
switcher.hookFetch();
switcher.injectDropdownStyle();
switcher.monitorNodesAndInject();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址