// ==UserScript== // @name OpenRouter Model Info // @version 1.1.0 // @description Shows supported reasoning efforts and model-specific generation defaults on OpenRouter model pages and cards. // @match https://openrouter.ai/* // @run-at document_start // ==/UserScript== const STYLE_ID = 'openscript-reasoning-style'; const PANEL_ID = 'openscript-reasoning-panel'; const CHIP_CLASS = 'openscript-reasoning-chip'; const ORDER = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']; const PARAM_ORDER = ['temperature', 'top_p', 'top_k', 'min_p', 'frequency_penalty', 'presence_penalty', 'repetition_penalty']; const labels = { none: 'None', minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'XHigh', max: 'Max' }; const paramLabels = { temperature: 'Temp', top_p: 'Top P', top_k: 'Top K', min_p: 'Min P', frequency_penalty: 'Frequency', presence_penalty: 'Presence', repetition_penalty: 'Repeat', }; let route = '', liveModels = new Map(), loading, catalogLoaded = false, timer; const ensureStyles = () => { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = ` #${PANEL_ID}, .${CHIP_CLASS} { display: inline-flex; flex-wrap: wrap; align-items: center; gap: .25rem; margin-left: .4rem; vertical-align: middle; } #${PANEL_ID}::before, .${CHIP_CLASS}::before { color: var(--muted-foreground, #666); content: 'Reasoning'; font-size: .65rem; font-weight: 650; } .openscript-effort, .openscript-setting { display: inline-flex; align-items: center; gap: .22rem; min-height: 1.28rem; padding: .06rem .4rem; border: 1px solid color-mix(in srgb, var(--primary, #7c3aed) 24%, var(--border, #d8d8df)); border-radius: 999px; background: color-mix(in srgb, var(--primary, #7c3aed) 8%, var(--background, #fff)); color: var(--foreground, #27272a); font-size: .64rem; font-weight: 600; line-height: 1rem; white-space: nowrap; } .openscript-effort[data-effort="none"] { opacity: .72; } .openscript-effort[data-effort="high"], .openscript-effort[data-effort="xhigh"] { border-color: color-mix(in srgb, #f59e0b 40%, var(--border, #d8d8df)); background: color-mix(in srgb, #f59e0b 9%, var(--background, #fff)); } .openscript-effort[data-effort="max"] { border-color: color-mix(in srgb, #f43f5e 42%, var(--border, #d8d8df)); background: color-mix(in srgb, #f43f5e 9%, var(--background, #fff)); } .openscript-effort[data-default="true"]::after { color: var(--primary, #7c3aed); content: '•'; } #${PANEL_ID} .openscript-effort[data-default="true"]::after { content: '· default'; font-size: .57rem; } .openscript-defaults-label { margin-left: .18rem; padding-left: .42rem; border-left: 1px solid var(--border, #d8d8df); color: var(--muted-foreground, #666); font-size: .65rem; font-weight: 650; } .openscript-setting { border-color: color-mix(in srgb, #0ea5e9 28%, var(--border, #d8d8df)); background: color-mix(in srgb, #0ea5e9 7%, var(--background, #fff)); } `; document.head?.append(style); }; const normalizedEfforts = config => [...new Set(config?.supported_reasoning_efforts || [])] .sort((a, b) => (ORDER.indexOf(a) + 1 || 99) - (ORDER.indexOf(b) + 1 || 99)); const defaultEntries = defaults => Object.entries(defaults || {}).filter(([, value]) => value != null) .sort(([a], [b]) => (PARAM_ORDER.indexOf(a) + 1 || 99) - (PARAM_ORDER.indexOf(b) + 1 || 99)); const effortPill = (effort, config) => { const pill = document.createElement('span'); const isDefault = effort === config.default_reasoning_effort; pill.className = 'openscript-effort'; pill.dataset.effort = effort; pill.dataset.default = isDefault; pill.textContent = labels[effort] || effort.replace(/(^|[_-])\w/g, value => value.at(-1).toUpperCase()); pill.title = `${pill.textContent} reasoning effort${isDefault ? ' (default)' : ''}`; return pill; }; const settingPill = ([name, value]) => { const pill = document.createElement('span'); const label = paramLabels[name] || name.replace(/_/g, ' ').replace(/\b\w/g, value => value.toUpperCase()); pill.className = 'openscript-setting'; pill.textContent = `${label} ${typeof value === 'object' ? JSON.stringify(value) : value}`; pill.title = `Default ${name.replace(/_/g, ' ')}: ${typeof value === 'object' ? JSON.stringify(value) : value}`; return pill; }; const signature = info => JSON.stringify([ normalizedEfforts(info.reasoning), info.reasoning.default_reasoning_effort, info.reasoning.is_mandatory_reasoning, defaultEntries(info.defaults), ]); const infoPills = info => { const pills = normalizedEfforts(info.reasoning).map(effort => effortPill(effort, info.reasoning)); const defaults = defaultEntries(info.defaults); if (!defaults.length) return pills; const label = document.createElement('span'); label.className = 'openscript-defaults-label'; label.textContent = 'Defaults'; return [...pills, label, ...defaults.map(settingPill)]; }; const flightText = () => [...document.scripts].flatMap(script => { const value = script.textContent.trim(); if (!value.startsWith('self.__next_f.push(')) return []; try { const data = JSON.parse(value.slice(value.indexOf('(') + 1, value.lastIndexOf(')'))); return typeof data?.[1] === 'string' ? [data[1]] : []; } catch { return []; } }).join(''); const pageModels = () => { const models = new Map(), text = flightText(); const object = '\\{(?:[^"{}]|"(?:\\\\.|[^"\\\\])*")*\\}'; const collect = (field, part) => { const pattern = new RegExp(`"slug":"((?:\\\\.|[^"\\\\])*)"[\\s\\S]{0,20000}?"${field}":(null|${object})`, 'g'); for (const match of text.matchAll(pattern)) try { const slug = JSON.parse(`"${match[1]}"`), value = JSON.parse(match[2]); const info = models.get(slug) || { reasoning: null, defaults: {} }; info[part] = value || (part === 'defaults' ? {} : null); models.set(slug, info); } catch {} }; collect('reasoning_config', 'reasoning'); collect('default_parameters', 'defaults'); return new Map([...models].filter(([, info]) => normalizedEfforts(info.reasoning).length)); }; const addModel = (map, model) => { const reasoning = model.reasoning_config || model.features?.reasoning_config; if (!normalizedEfforts(reasoning).length) return; const endpoint = model.endpoint || {}; const info = { reasoning, defaults: model.default_parameters || endpoint.default_parameters || {} }; for (const slug of [model.slug, model.permaslug, endpoint.model_variant_slug, endpoint.model_variant_permaslug]) if (slug) map.set(slug, info); }; const modelSlug = href => { try { return decodeURIComponent(new URL(href, location.href).pathname.slice(1).replace(/\/$/, '')); } catch { return ''; } }; const infoFor = slug => liveModels.get(slug) || liveModels.get(slug.replace(/:batch$/, '')); const renderDetail = () => { const titleRow = document.getElementById('model-title-row'); if (!titleRow) return null; const heading = titleRow.querySelector('h1'), slug = modelSlug(location.href); if (!heading) return null; if (!infoFor(slug)) for (const [model, info] of pageModels()) liveModels.set(model, info); const info = infoFor(slug), current = document.getElementById(PANEL_ID); if (!info) { current?.remove(); return false; } const stamp = signature(info); if (current?.dataset.signature === stamp && current.previousElementSibling === heading) return true; current?.remove(); const panel = document.createElement('span'); panel.id = PANEL_ID; panel.dataset.signature = stamp; panel.setAttribute('aria-label', 'Supported reasoning efforts and generation defaults'); panel.title = `Supported reasoning efforts${info.reasoning.is_mandatory_reasoning ? ' (reasoning always on)' : ''}`; panel.append(...infoPills(info)); heading.after(panel); return true; }; const renderCatalog = () => { for (const root of document.querySelectorAll('[data-testid="model-list-item"], table tbody tr')) { const link = [...root.querySelectorAll('a[href]')].find(value => value.textContent.trim() && infoFor(modelSlug(value.href))); const slug = link && modelSlug(link.href), info = slug && infoFor(slug), old = root.querySelector(`.${CHIP_CLASS}`); if (!info) { old?.remove(); continue; } const stamp = `${slug}:${signature(info)}`; if (old?.dataset.signature === stamp) continue; old?.remove(); const chip = document.createElement('span'); chip.className = CHIP_CLASS; chip.dataset.signature = stamp; chip.title = `Supported reasoning efforts${info.reasoning.is_mandatory_reasoning ? ' (reasoning always on)' : ''}`; chip.append(...infoPills(info)); link.after(chip); } }; const render = () => location.pathname === '/models' ? renderCatalog() : renderDetail(); const loadCatalog = async () => { try { const response = await fetch('/api/frontend/v1/models/find?active=true&fmt=cards', { cache: 'no-store', credentials: 'same-origin', headers: { Accept: 'application/json' }, }); if (!response.ok) return; const models = new Map(), raw = await response.json(); const data = typeof raw === 'string' ? JSON.parse(raw) : raw; for (const model of data.data?.models || []) addModel(models, model); liveModels = models; catalogLoaded = true; render(); } catch (error) { console.warn('[OpenRouter Model Info] Could not load live model data.', error); } }; const run = () => { ensureStyles(); const key = `${location.pathname}${location.search}`; if (key !== route) { route = key; if (location.pathname === '/models') { liveModels = new Map(); catalogLoaded = false; } else for (const [model, info] of pageModels()) liveModels.set(model, info); document.getElementById(PANEL_ID)?.remove(); document.querySelectorAll(`.${CHIP_CLASS}`).forEach(element => element.remove()); } const rendered = render(); if ((location.pathname === '/models' && !catalogLoaded) || rendered === false) loading ||= loadCatalog().finally(() => { loading = null; }); }; const schedule = () => { clearTimeout(timer); timer = setTimeout(run, 60); }; new MutationObserver(schedule).observe(document.documentElement, { childList: true, subtree: true }); ['popstate', 'pageshow'].forEach(event => addEventListener(event, schedule)); window.navigation?.addEventListener('navigatesuccess', schedule); schedule();