From a392966aec145c3ba09949f6f417f9911792828e Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Sat, 12 Sep 2026 21:50:09 -0700 Subject: [PATCH] Add OpenRouter model reasoning info --- info.os.js | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 info.os.js diff --git a/info.os.js b/info.os.js new file mode 100644 index 0000000..5cf706f --- /dev/null +++ b/info.os.js @@ -0,0 +1,227 @@ +// ==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();