Add OpenRouter model reasoning info

This commit is contained in:
2026-09-12 21:50:09 -07:00
commit a392966aec

227
info.os.js Normal file
View File

@@ -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();