Spaces:
Paused
Paused
File size: 8,372 Bytes
ffba252 ee20373 d135f12 ee20373 ffba252 e9acff4 ffba252 ee20373 ffba252 88466b5 d756c8e ffba252 6f9105d ffba252 6f9105d ffba252 6f9105d ffba252 ee20373 e9acff4 ffba252 e9acff4 ffba252 ec9cfc9 ee20373 ffba252 ee20373 ffba252 ee20373 ffba252 ee20373 ec9cfc9 ffba252 e9acff4 ffba252 c3fc087 ec9cfc9 ee20373 ffba252 d756c8e 88466b5 d756c8e ffba252 ee20373 ffba252 ee20373 e9acff4 ee20373 e9acff4 ec9cfc9 e9acff4 ffba252 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | 'use strict';
// OpenRouter exposes a public JSON API – no scraping needed.
// Docs: https://openrouter.ai/docs/models
//
// Without an API key: ~342 models (public subset).
// With an API key: ~600+ models including image-gen (FLUX, etc.) and subscriber-only models.
// Set OPENROUTER_API_KEY in env or ../AIToolkit/.env to unlock all models.
const { loadEnv } = require('../load-env');
loadEnv();
const { getJson } = require('../fetch-utils');
const API_URL = 'https://openrouter.ai/api/v1/models';
const EU_API_URL = 'https://eu.openrouter.ai/api/v1/models';
// OpenRouter stores per-token prices; multiply by 1e6 to get per-1M price.
const toPerMillion = (val) => (val ? parseFloat(val) * 1_000_000 : 0);
function loadApiKey() {
return process.env.OPENROUTER_API_KEY || null;
}
const getSizeB = (id) => {
// Relaxed regex: match any digits followed by 'b', even if part of a word like 'e2b' or '30b-a3b'
const match = (id || '').match(/([\d.]+)[Bb](?:\b|:|$)/);
if (!match) return undefined;
const num = parseFloat(match[1]);
return (num > 0 && num < 2000) ? num : undefined;
};
// Derive model type from architecture modalities.
function getModelType(architecture) {
if (!architecture) return 'chat';
const inMods = architecture.input_modalities || [];
const outMods = architecture.output_modalities || [];
if (outMods.includes('audio')) return 'audio';
if (outMods.includes('image')) return 'image';
if (inMods.includes('image') || inMods.includes('video')) return 'vision';
if (inMods.includes('audio')) return 'audio';
return 'chat';
}
// Derive capabilities array from modalities + supported parameters.
function getCapabilities(architecture, supportedParams) {
const caps = [];
const inMods = (architecture?.input_modalities || []);
const outMods = (architecture?.output_modalities || []);
const params = supportedParams || [];
// Inputs
if (inMods.includes('image')) caps.push('vision');
if (inMods.includes('video')) caps.push('video');
if (inMods.includes('audio')) caps.push('audio');
if (inMods.includes('file')) caps.push('files');
// Outputs
if (outMods.includes('image')) caps.push('image-out');
if (outMods.includes('video')) caps.push('video-out');
if (outMods.includes('audio')) caps.push('audio-out');
if (params.includes('tools')) caps.push('tools');
if (params.includes('reasoning')) caps.push('reasoning');
return caps;
}
async function fetchOpenRouter() {
const apiKey = loadApiKey();
const headers = {
Accept: 'application/json',
'HTTP-Referer': 'https://github.com/providers-comparison',
};
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
// If we have an API key, use the /user endpoint to get EU-filtered models correctly.
// Standard /models endpoint doesn't filter by subdomain.
const globalUrl = apiKey ? `${API_URL}/user` : API_URL;
const euUrl = apiKey ? `${EU_API_URL}/user` : EU_API_URL;
process.stdout.write('OpenRouter: fetching Global... ');
const globalData = await getJson(globalUrl, { headers });
let euModelIds = new Set();
if (apiKey) {
process.stdout.write('EU... ');
try {
const euData = await getJson(euUrl, { headers });
if (euData?.data) {
euModelIds = new Set(euData.data.map(m => m.id));
}
} catch (e) {
console.warn(`\n ⚠ Failed to fetch EU models: ${e.message}`);
}
}
const models = [];
for (const model of globalData.data || []) {
const pricing = model.pricing || {};
const inputPrice = toPerMillion(pricing.prompt);
const outputPrice = toPerMillion(pricing.completion);
const audioPrice = toPerMillion(pricing.audio);
// pricing.image: per-image cost for image-gen models (e.g. FLUX) — in USD per image
// (NOT the same as per-pixel input cost on vision models like Gemini, which also have prompt price set)
const imagePrice = parseFloat(pricing.image || '0');
// Skip meta-routes with sentinel negative prices (e.g. openrouter/auto)
if (inputPrice < 0 || outputPrice < 0) continue;
// Skip the free-router meta-model
if (model.id === 'openrouter/free') continue;
// Skip models with genuinely zero pricing across all fields (unpriced/placeholder entries).
// Exception: models with a :free suffix are real free models and should be kept.
if (inputPrice === 0 && outputPrice === 0 && imagePrice === 0 && audioPrice === 0 && !model.id.endsWith(':free')) continue;
const type = getModelType(model.architecture);
const capabilities = getCapabilities(model.architecture, model.supported_parameters);
// Tag with eu-endpoint if model is available via EU subdomain
if (euModelIds.has(model.id)) {
capabilities.push('eu-endpoint');
}
const modelEntry = {
name: model.id,
type,
input_price_per_1m: Math.round(inputPrice * 10000) / 10000,
output_price_per_1m: Math.round(outputPrice * 10000) / 10000,
currency: 'USD',
};
if (model.hugging_face_id) modelEntry.hf_id = model.hugging_face_id;
if (audioPrice > 0) {
modelEntry.audio_price_per_1m = Math.round(audioPrice * 10000) / 10000;
}
// For pure image-gen models (no per-token pricing), store the per-image price
if (imagePrice > 0 && inputPrice === 0 && outputPrice === 0) {
modelEntry.price_per_image = Math.round(imagePrice * 100000) / 100000;
}
if (capabilities.length) modelEntry.capabilities = capabilities;
const apiParams = model.architecture?.parameters;
const apiSize = (apiParams && apiParams > 0) ? Math.round(apiParams / 1_000_000_000 * 10) / 10 : null;
// Attempt detection in priority order:
// 1. Explicit architecture parameters from API
// 2. Regex on canonical HF ID if provided by OpenRouter
// 3. Regex on the model description (common for new models missing architecture metadata)
// 4. Regex on the OpenRouter ID itself
let sizeB = apiSize;
if (!sizeB && model.hugging_face_id) sizeB = getSizeB(model.hugging_face_id);
if (!sizeB && model.description) {
// Improved description regex: catch "size of 2B", "effective 2B", "196B parameters", etc.
const descMatch = model.description.match(/([\d.]+)[Bb](?:[ -]parameter| size| effective)/i) ||
model.description.match(/effective parameter size of ([\d.]+)[Bb]/i);
if (descMatch) sizeB = parseFloat(descMatch[1]);
}
if (!sizeB) sizeB = getSizeB(model.id);
if (sizeB) modelEntry.size_b = sizeB;
models.push(modelEntry);
}
// Sort: free first (price=0), then by input price
models.sort((a, b) => {
const aFree = a.input_price_per_1m === 0 ? 1 : 0;
const bFree = b.input_price_per_1m === 0 ? 1 : 0;
if (aFree !== bFree) return aFree - bFree; // paid first, free last
return a.input_price_per_1m - b.input_price_per_1m;
});
return models;
}
module.exports = { fetchOpenRouter, providerName: 'OpenRouter' };
// Run standalone: node scripts/providers/openrouter.js
if (require.main === module) {
fetchOpenRouter()
.then((models) => {
const apiKey = loadApiKey();
const free = models.filter(m => m.input_price_per_1m === 0 && !m.price_per_image);
const vision = models.filter(m => m.type === 'vision');
const imageGen = models.filter(m => m.type === 'image');
const eu = models.filter(m => m.capabilities?.includes('eu-endpoint'));
console.log(`Fetched ${models.length} models from OpenRouter API ${apiKey ? '(authenticated)' : '(public – set OPENROUTER_API_KEY for more models)'}`);
console.log(` Free: ${free.length}, Vision: ${vision.length}, Image-gen: ${imageGen.length}, EU-Endpoint: ${eu.length}`);
const audioModels = models.filter(m => m.audio_price_per_1m > 0);
console.log(` Audio-priced models: ${audioModels.length}`);
console.log('\nSample Audio-priced models:');
audioModels.slice(0, 5).forEach((m) =>
console.log(` ${m.name.padEnd(55)} Audio: $${m.audio_price_per_1m}/M [${m.type}]`)
);
console.log('\nFirst 5 EU-available:');
eu.slice(0, 5).forEach((m) =>
console.log(` ${m.name.padEnd(55)} $${m.input_price_per_1m} / $${m.output_price_per_1m} [${m.type}]`)
);
})
.catch((err) => {
console.error('Error:', err.message);
process.exit(1);
});
}
|