Google AI Studio Model kingfall

Inject multiple custom models with themed emojis into Google AI Studio model list. Intercepts XHR/fetch, handles array-of-arrays JSON structures.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Google AI Studio Model kingfall
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Inject multiple custom models with themed emojis into Google AI Studio model list. Intercepts XHR/fetch, handles array-of-arrays JSON structures.
// @author       Updated by everyone
// @match        https://aistudio.google.com/*
// @match        https://google.com/app/prompts/new_chat*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=aistudio.google.com
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ============== Configuration ===============
    const SCRIPT_VERSION = "v1.7";
    const DEBUG = false; // set true for verbose logging
    const LOG_PREFIX = `[Injector ${SCRIPT_VERSION}]`;

    const MODELS_TO_INJECT = [
        { name: 'models/kingfall-ab-test', displayName: `👑 Kingfall (${SCRIPT_VERSION})`, description: `Injected by script ${SCRIPT_VERSION}` },
        { name: 'models/gemini-2.5-pro-preview-03-25', displayName: `✨ Gemini 2.5 Pro 03-25 (${SCRIPT_VERSION})`, description: `Injected by script ${SCRIPT_VERSION}` },
        { name: 'models/goldmane-ab-test', displayName: `🦁 Goldmane (${SCRIPT_VERSION})`, description: `Injected by script ${SCRIPT_VERSION}` },
        { name: 'models/claybrook-ab-test', displayName: `💧 Claybrook (${SCRIPT_VERSION})`, description: `Injected by script ${SCRIPT_VERSION}` },
        { name: 'models/frostwind-ab-test', displayName: `❄️ Frostwind (${SCRIPT_VERSION})`, description: `Injected by script ${SCRIPT_VERSION}` },
        { name: 'models/calmriver-ab-test', displayName: `🌊 Calmriver (${SCRIPT_VERSION})`, description: `Injected by script ${SCRIPT_VERSION}` }
    ];

    // ============== Helpers ===============
    function log(...args) { if (DEBUG) console.log(LOG_PREFIX, ...args); }
    function isTargetURL(url) {
        return typeof url === 'string' && url.includes('ListModels');
    }

    // Recursively find the array-of-arrays model list
    function findModelListArray(obj) {
        if (Array.isArray(obj) && obj.length > 0 && obj.every(
            item => Array.isArray(item) && typeof item[0] === 'string' && item[0].startsWith('models/')
        )) {
            return obj;
        }
        if (obj && typeof obj === 'object') {
            for (const key in obj) {
                if (obj.hasOwnProperty(key)) {
                    const res = findModelListArray(obj[key]);
                    if (res) return res;
                }
            }
        }
        return null;
    }

    function processJsonData(data) {
        const NAME_IDX = 0, DISPLAY_IDX = 3, DESC_IDX = 4, METHODS_IDX = 7;
        let modified = false;

        const modelsArray = findModelListArray(data);
        if (!modelsArray) {
            log('No model list found, skipping');
            return { data, modified };
        }

        // find a template model that has methods
        const template = modelsArray.find(m => Array.isArray(m) && Array.isArray(m[METHODS_IDX])) || null;
        if (!template) {
            log('Template model not found, cannot inject new models');
        }

        MODELS_TO_INJECT.slice().reverse().forEach(model => {
            const exists = modelsArray.some(m => Array.isArray(m) && m[NAME_IDX] === model.name);
            if (!exists && template) {
                const clone = JSON.parse(JSON.stringify(template));
                clone[NAME_IDX] = model.name;
                clone[DISPLAY_IDX] = model.displayName;
                clone[DESC_IDX] = `${model.description} (based on ${template[NAME_IDX]})`;
                if (!Array.isArray(clone[METHODS_IDX])) {
                    clone[METHODS_IDX] = ['generateContent','countTokens','createCachedContent','batchGenerateContent'];
                }
                modelsArray.unshift(clone);
                modified = true;
                log('Injected', model.name);
            } else if (exists) {
                // update display name if changed
                const existing = modelsArray.find(m => m[NAME_IDX] === model.name);
                if (existing && existing[DISPLAY_IDX] !== model.displayName) {
                    existing[DISPLAY_IDX] = model.displayName;
                    modified = true;
                    log('Updated displayName for', model.name);
                }
            }
        });

        return { data, modified };
    }

    function modifyResponseBody(text, url) {
        if (typeof text !== 'string') return text;
        const ANTI_HIJACK = ")]}'\n";
        let body = text, hasPrefix = false;
        if (body.startsWith(ANTI_HIJACK)) {
            body = body.slice(ANTI_HIJACK.length);
            hasPrefix = true;
        }
        if (!body.trim()) return text;

        try {
            const json = JSON.parse(body);
            const result = processJsonData(json);
            if (result.modified) {
                let out = JSON.stringify(result.data);
                if (hasPrefix) out = ANTI_HIJACK + out;
                return out;
            }
        } catch (e) {
            console.error(LOG_PREFIX, 'JSON parse error for', url, e);
        }
        return text;
    }

    // ============== Fetch Interceptor ===============
    const origFetch = window.fetch;
    window.fetch = async function(input, init) {
        const url = input instanceof Request ? input.url : String(input);
        const response = await origFetch(input, init);
        if (response.ok && isTargetURL(url)) {
            try {
                const clone = response.clone();
                const text = await clone.text();
                const mod = modifyResponseBody(text, url);
                if (mod !== text) {
                    return new Response(mod, {
                        status: response.status,
                        statusText: response.statusText,
                        headers: response.headers
                    });
                }
            } catch (e) {
                console.error(LOG_PREFIX, 'Fetch intercept error', e);
            }
        }
        return response;
    };
    log('Fetch patch applied');

    // ============== XHR Interceptor ===============
    const xhrProto = XMLHttpRequest.prototype;
    const origOpen = xhrProto.open;
    const txtDesc = Object.getOwnPropertyDescriptor(xhrProto, 'responseText');
    const resDesc = Object.getOwnPropertyDescriptor(xhrProto, 'response');

    xhrProto.open = function(method, url) {
        this._isTarget = isTargetURL(url);
        return origOpen.apply(this, arguments);
    };

    function wrapResponse(xhr, origVal, type) {
        if (xhr._isTarget && xhr.readyState === 4 && xhr.status === 200) {
            const cacheKey = '_cached' + type;
            if (xhr[cacheKey] === undefined) {
                const txt = type === 'json' && typeof origVal === 'object'
                            ? JSON.stringify(origVal)
                            : String(origVal);
                xhr[cacheKey] = modifyResponseBody(txt, xhr._interceptorUrl);
            }
            const val = xhr[cacheKey];
            if (type === 'json' && typeof val === 'string') {
                try { return JSON.parse(val); } catch { return origVal; }
            }
            return val;
        }
        return origVal;
    }

    if (txtDesc && txtDesc.get) {
        Object.defineProperty(xhrProto, 'responseText', {
            get: function() {
                return wrapResponse(this, txtDesc.get.call(this), 'text');
            }, configurable: true
        });
    }
    if (resDesc && resDesc.get) {
        Object.defineProperty(xhrProto, 'response', {
            get: function() {
                const rt = resDesc.get.call(this);
                if (this.responseType === 'json') return wrapResponse(this, rt, 'json');
                if (!this.responseType || this.responseType === 'text') return wrapResponse(this, rt, 'text');
                return rt;
            }, configurable: true
        });
    }
    log('XHR patch applied');
})();