mirror of
https://github.com/GetOpenScript/OpenRouter.openscript.git
synced 2026-09-18 02:35:42 +00:00
Add OpenRouter model reasoning info
This commit is contained in:
227
info.os.js
Normal file
227
info.os.js
Normal 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();
|
||||
Reference in New Issue
Block a user