Greasy Fork is available in English.

ChatGPT Realtime Model Switcher: 4o-mini, o4-mini, o3 and more!

Allowing you to switch models during a single conversation, and highlight responses by color based on the model generating them

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.

Necesitarás 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.

Necesitará instalar una extensión como Tampermonkey para 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)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

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

// ==UserScript==
// @name              ChatGPT Realtime Model Switcher: 4o-mini, o4-mini, o3 and more!
// @name:zh-CN        ChatGPT 模型切换助手: 4o-mini、o4-mini、o3 等更多...
// @name:zh-TW        ChatGPT 模型切換助手: 4o-mini、o4-mini、o3 等更多...
// @namespace         http://tampermonkey.net/
// @version           0.54.1
// @description       Allowing you to switch models during a single conversation, and highlight responses by color based on the model generating them
// @description:zh-CN 让您在对话中随意切换语言模型,并用不同颜色标示生成回应的语言模型
// @description:zh-TW 讓您在對話中隨意切換語言模型,並用不同顏色標示生成回答的語言模型
// @match             *://chatgpt.com/*
// @author            d0gkiller87
// @license           MIT
// @grant             unsafeWindow
// @grant             GM.getValue
// @grant             GM.setValue
// @grant             GM.deleteValue
// @grant             GM_registerMenuCommand
// @grant             GM.registerMenuCommand
// @grant             GM.unregisterMenuCommand
// @run-at            document-idle
// @icon              https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com
// ==/UserScript==

(async function() {
  'use strict';

  function injectStyle( style, isDisabled = false ) {
    const styleNode = document.createElement( 'style' );
    styleNode.type = 'text/css';
    styleNode.textContent = style;
    document.head.appendChild( styleNode );
    styleNode.disabled = isDisabled;
    return styleNode;
  }

  const PlanType = Object.freeze({
    free: 0,
    plus: 1,
    pro : 2
  });

  class ModelSwitcher {
    getPlanType() {
      for ( const scriptNode of document.querySelectorAll( 'script' ) ) {
        let match;
        while ( ( match = /\\"planType\\"\s*,\s*\\"(\w+?)\\"/.exec( scriptNode.innerHTML ) ) !== null ) {
          return match[1];
        }
      }
      return 'free'
    }

    async init() {
      this.model = await GM.getValue( 'model', 'auto' );
      this.buttons = {};
      this.offsetX = 0;
      this.offsetY = 0;
      this.isDragging = false;
      this.shouldCancelClick = false;
      this.modelSelector = null;
      this.isMenuVisible = await GM.getValue( 'isMenuVisible', true );
      this.isMenuVisibleCommandId = null;
      this.modelHighlightStyleNode = null;
      this.isModelHighlightEnabled = await GM.getValue( 'isModelHighlightEnabled', true );
      this.isModelHighlightEnabledCommandId = null;
      this.isMenuVertical = await GM.getValue( 'isMenuVertical', true );
      this.isMenuVerticalCommandId = null;
      this.conversationUrlRegex = new RegExp( /https:\/\/chatgpt\.com\/backend-api\/.*conversation/ );

      const planType = PlanType[ this.getPlanType() ];

      const models = [
        // [ PlanType.pro, "o1", "o1" ], // retired
        [ PlanType.pro, "o1-pro", "o1-pro" ],
        // [ PlanType.free, "o3-mini", "o3-mini" ], // retired
        [ PlanType.plus, "o3", "o3" ],
        [ PlanType.free, "o4-mini", "o4-mini" ],
        [ PlanType.plus, "o4-mini-high", "o4-mini-high" ],
        [ PlanType.free, "gpt-3.5", "gpt-3-5" ],
        [ PlanType.free, "4o-mini", "gpt-4o-mini" ],
        [ PlanType.free, "4.1-mini", "gpt-4-1-mini" ],
        // [ PlanType.free, "gpt-4", "gpt-4" ], // same as 4o
        [ PlanType.free, "gpt-4o", "gpt-4o" ],
        [ PlanType.plus, "gpt-4.1", "gpt-4-1" ],
        // [ PlanType.plus, "4o-jawbone", "4o-jawbone" ], // retired (https://x.com/testingcatalog/status/1915483050953125965)
        [ PlanType.plus, "gpt-4.5", "gpt-4-5" ],
        [ PlanType.free, "default", "auto" ],
      ];

      this.availableModels = {};
      for ( const [ minimumPlan, modelName, modelValue ] of models ) {
        if ( planType >= minimumPlan ) {
          this.availableModels[modelName] = modelValue;
        }
      }
    }

    hookFetch() {
      const originalFetch = unsafeWindow.fetch;
      unsafeWindow.fetch = async ( resource, config = {} ) => {
        if (
          typeof resource === 'string' &&
          resource.match( this.conversationUrlRegex ) &&
          config.method === 'POST' &&
          config.headers &&
          config.headers['Content-Type'] === 'application/json' &&
          config.body &&
          this.model !== 'auto'
        ) {
          const body = JSON.parse( config.body );
          body.model = this.model;
          config.body = JSON.stringify( body );
        }
        return originalFetch( resource, config );
      };
    }

    injectToggleButtonStyle() {
      let style = `
        :root {
          color-scheme: light dark;
        }
        #model-selector {
          position: absolute;
          display: flex;
          flex-direction: column;
          gap: 6px;
          cursor: grab;
        }
        #model-selector.horizontal {
          flex-direction: row;
        }
        #model-selector.hidden {
          display: none;
        }
        #model-selector button {
          background: none;
          border: 1px solid light-dark(#151515, white);
          color: light-dark(#151515, white);
          padding: 6px;
          cursor: pointer;
          font-size: 0.9rem;
          user-select: none;
        }
        #model-selector button.selected {
          color: light-dark(white, white);
        }
        :root {
          --o1-pro-color: 139, 232, 27;
          --o3-color: 139, 232, 27;
          --gpt-3-5-color: 0, 106, 129;
          --gpt-4-1-color: 13, 121, 255;
          --gpt-4-5-color: 126, 3, 165;
          --gpt-4o-color: 18, 45, 134;
          --o4-mini-high-color: 176, 53, 0;
          --o4-mini-color: 203, 91, 0;
          --gpt-4o-jawbone-color: 201, 42, 42;
          --gpt-4o-mini-color: 67, 162, 90;
          --gpt-4-1-mini-color: 117, 166, 12;
          --auto-color: 131, 131, 139;

          --unknown-model-btn-color: 67, 162, 90;
          --unknown-model-box-shadow-color: 48, 255, 19;
        }
      `;

      for ( const model of Object.values( this.availableModels ) ) {
        style += `
          #model-selector button.btn-${ model } {
            background-color: rgb(var(--${ model }-color, var(--unknown-model-btn-color)));
          }
        `;
      }

      injectStyle( style );
    }

    refreshButtons() {
      for ( const [ model, button ] of Object.entries( this.buttons ) ) {
        const isSelected = model === `btn-${ this.model }`;
        button.classList.toggle( model, isSelected );
        button.classList.toggle( 'selected', isSelected );
      }
    }

    async reloadMenuVisibleToggle() {
      this.isMenuVisibleCommandId = await GM.registerMenuCommand(
        `${ this.isMenuVisible ? '☑︎' : '☐' } Show model selector`,
        async () => {
          this.isMenuVisible = !this.isMenuVisible;
          await GM.setValue( 'isMenuVisible', this.isMenuVisible );
          this.modelSelector.classList.toggle( 'hidden', !this.isMenuVisible );
          this.reloadMenuVisibleToggle();
        },
        this.isMenuVisibleCommandId ? { id: this.isMenuVisibleCommandId } : {}
      );
    }

    async reloadMenuVerticalToggle() {
      this.isMenuVerticalCommandId = await GM.registerMenuCommand(
        `┖ Style: ${ this.isMenuVertical ? 'vertical ↕' : 'horizontal ↔' }`,
        async () => {
          this.isMenuVertical = !this.isMenuVertical;
          await GM.setValue( 'isMenuVertical', this.isMenuVertical );

          const originalRight = parseInt( this.modelSelector.style.left ) + this.modelSelector.offsetWidth;
          const originalBottom = parseInt( this.modelSelector.style.top ) + this.modelSelector.offsetHeight;

          this.modelSelector.style.visibility = 'hidden';
          this.modelSelector.style.left = '0px';
          this.modelSelector.style.top = '0px';

          this.modelSelector.classList.toggle( 'horizontal', !this.isMenuVertical );

          this.modelSelector.style.left = `${ originalRight - this.modelSelector.offsetWidth }px`;
          this.modelSelector.style.top = `${ originalBottom - this.modelSelector.offsetHeight }px`;
          this.modelSelector.style.visibility = 'visible';

          await GM.setValue( 'relativeMenuPosition', this.getCurrentRelativeMenuPosition() );
          this.reloadMenuVerticalToggle();
        },
        this.isMenuVerticalCommandId ? { id: this.isMenuVerticalCommandId } : {}
      );
    }

    injectMessageModelHighlightStyle() {
      let style = `
        div[data-message-model-slug] {
          padding: 0px 5px;
          box-shadow: 0 0 3px 3px rgba(var(--unknown-model-box-shadow-color), 0.65);
        }
      `;
      for ( const model of Object.values( this.availableModels ) ) {
        style += `
        div[data-message-model-slug="${ model }"] {
          box-shadow: 0 0 3px 3px rgba(var(--${ model }-color, var(--unknown-model-box-shadow-color)), 0.8);
        }
        `;
      }
      this.modelHighlightStyleNode = injectStyle( style, !this.isModelHighlightEnabled );
    }

    async reloadMessageModelHighlightToggle() {
      this.isModelHighlightEnabledCommandId = await GM.registerMenuCommand(
        `${ this.isModelHighlightEnabled ? '☑︎' : '☐' } Show model identifer`,
        async () => {
          this.isModelHighlightEnabled = !this.isModelHighlightEnabled;
          await GM.setValue( 'isModelHighlightEnabled', this.isModelHighlightEnabled );
          this.modelHighlightStyleNode.disabled = !this.isModelHighlightEnabled;
          this.reloadMessageModelHighlightToggle();
        },
        this.isModelHighlightEnabledCommandId ? { id: this.isModelHighlightEnabledCommandId } : {}
      );
    }

    createModelSelectorMenu() {
      this.modelSelector = document.createElement( 'div' );
      this.modelSelector.id = 'model-selector';

      for ( const [ modelName, modelValue ] of Object.entries( this.availableModels ) ) {
        const button = document.createElement( 'button' );
        button.textContent = modelName;
        button.title = modelValue;
        button.addEventListener(
          'click',
          async event => {
            if ( this.shouldCancelClick ) {
              event.preventDefault();
              event.stopImmediatePropagation();
              return;
            }
            this.model = modelValue;
            await GM.setValue( 'model', modelValue );
            this.refreshButtons();
          }
        );
        this.modelSelector.appendChild( button );
        this.buttons[`btn-${ modelValue }`] = button;
      }
      this.modelSelector.classList.toggle( 'hidden', !this.isMenuVisible );
      this.modelSelector.classList.toggle( 'horizontal', !this.isMenuVertical );
      return this.modelSelector;
    }

    injectMenu() {
      document.body.appendChild( this.modelSelector );
    }

    monitorBodyChanges() {
      const observer = new MutationObserver( mutationsList => {
        for ( const mutation of mutationsList ) {
          if ( document.body.querySelector( '#model-selector' ) ) continue;
          this.injectMenu();
          break;
        }
      });
      observer.observe( document.body, { childList: true } );
    }

    getDefaultRelativeMenuPosition() {
      return {
        offsetRight: 33,
        offsetBottom: 36
      };
    }

    relativeToAbsolutePosition( relativeMenuPosition ) {
      return {
        left: `${ window.innerWidth - this.modelSelector.offsetWidth - relativeMenuPosition.offsetRight }px`,
        top: `${ window.innerHeight - this.modelSelector.offsetHeight - relativeMenuPosition.offsetBottom }px`
      }
    }

    getCurrentRelativeMenuPosition() {
      return {
        offsetRight: window.innerWidth - parseInt( this.modelSelector.style.left ) - this.modelSelector.offsetWidth,
        offsetBottom: window.innerHeight - parseInt( this.modelSelector.style.top ) - this.modelSelector.offsetHeight
      }
    }

    async restoreMenuPosition() {
      const menuPosition = await GM.getValue( 'menuPosition', null ); // <= v0.53.1 migration
      if ( menuPosition ) {
        this.modelSelector.style.left = menuPosition.left;
        this.modelSelector.style.top = menuPosition.top;
        await GM.setValue(
          'relativeMenuPosition', {
            offsetRight: window.innerWidth - parseInt( menuPosition.left ) - this.modelSelector.offsetWidth,
            offsetBottom: window.innerHeight - parseInt( menuPosition.top ) - this.modelSelector.offsetHeight
          }
        );
        await GM.deleteValue( 'menuPosition' );
      } else {
        const relativeMenuPosition = await GM.getValue( 'relativeMenuPosition', this.getDefaultRelativeMenuPosition() );
        const absoluteMenuPosition = this.relativeToAbsolutePosition( relativeMenuPosition );
        this.modelSelector.style.left = absoluteMenuPosition.left;
        this.modelSelector.style.top = absoluteMenuPosition.top;
      }
    }

    monitorWindowResize() {
      window.addEventListener(
        'resize', async event => {
          const relativeMenuPosition = await GM.getValue( 'relativeMenuPosition', this.getDefaultRelativeMenuPosition() );
          const absoluteMenuPosition = this.relativeToAbsolutePosition( relativeMenuPosition );
          this.modelSelector.style.left = absoluteMenuPosition.left;
          this.modelSelector.style.top = absoluteMenuPosition.top;
        }
      );
    }

    async registerResetMenuPositionCommand() {
      await GM.registerMenuCommand(
        '⟲ Reset menu position',
        async () => {
          const defaultRelativeMenuPosition = this.getDefaultRelativeMenuPosition();
          const defaultAbsoluteMenuPosition = this.relativeToAbsolutePosition( defaultRelativeMenuPosition );
          this.modelSelector.style.left = defaultAbsoluteMenuPosition.left;
          this.modelSelector.style.top = defaultAbsoluteMenuPosition.top;
          await GM.setValue( 'relativeMenuPosition', defaultRelativeMenuPosition );
        }
      );
    }

    getPoint( event ) {
      return event.touches ? event.touches[0] : event;
    }

    mouseDownHandler( event ) {
      const point = this.getPoint( event );
      this.offsetX = point.clientX - this.modelSelector.offsetLeft;
      this.offsetY = point.clientY - this.modelSelector.offsetTop;
      this.isDragging = true;
      this.shouldCancelClick = false;
      this.modelSelector.style.cursor = 'grabbing';
    }

    mouseMoveHandler( event ) {
      if ( !this.isDragging ) return;

      const point = this.getPoint( event );
      const oldLeft = this.modelSelector.style.left;
      const oldTop = this.modelSelector.style.top;
      this.modelSelector.style.left = ( point.clientX - this.offsetX ) + 'px';
      this.modelSelector.style.top = ( point.clientY - this.offsetY ) + 'px';
      if ( !this.shouldCancelClick && ( this.modelSelector.style.left != oldLeft || this.modelSelector.style.top != oldTop ) ) {
        this.shouldCancelClick = true;
      }

      // Prevent scrolling on touch
      if ( event.cancelable ) event.preventDefault();
    }

    async mouseUpHandler( event ) {
      this.isDragging = false;
      this.modelSelector.style.cursor = 'grab';
      document.body.style.userSelect = '';
      await GM.setValue( 'relativeMenuPosition', this.getCurrentRelativeMenuPosition() );
    }

    registerGrabbing() {
      // Mouse
      this.modelSelector.addEventListener( 'mousedown', this.mouseDownHandler.bind( this ) );
      document.addEventListener( 'mousemove', this.mouseMoveHandler.bind( this ) );
      document.addEventListener( 'mouseup', this.mouseUpHandler.bind( this ) );

      // Touch
      this.modelSelector.addEventListener( 'touchstart', this.mouseDownHandler.bind( this ), { passive: false } );
      document.addEventListener( 'touchmove', this.mouseMoveHandler.bind( this ), { passive: false } );
      document.addEventListener( 'touchend', this.mouseUpHandler.bind( this ) );
    }
  }

  const switcher = new ModelSwitcher();
  await switcher.init();

  switcher.hookFetch();

  switcher.injectToggleButtonStyle();
  switcher.injectMessageModelHighlightStyle();

  switcher.createModelSelectorMenu();
  await switcher.registerResetMenuPositionCommand();
  await switcher.reloadMenuVisibleToggle();
  await switcher.reloadMenuVerticalToggle();
  await switcher.reloadMessageModelHighlightToggle();

  switcher.refreshButtons();
  switcher.monitorBodyChanges();
  switcher.injectMenu();

  await switcher.restoreMenuPosition();
  switcher.monitorWindowResize();
  switcher.registerGrabbing();
})();