Spaces:
Paused
Paused
File size: 4,529 Bytes
49e63bd | 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 | 'use strict';
/**
* Nscale Serverless Inference β pay-per-token, OpenAI-compatible.
* HQ London (UK); serverless inference runs on Nscale's sustainable European
* (Norway/EEA) data centres. Not EU-headquartered, so jurisdiction is UK GDPR
* + EEA hosting rather than full EU sovereignty β see the provider `region`.
*
* The catalog + pricing come from the authenticated OpenAI-compatible
* `GET /v1/models` endpoint (docs page is access-gated, so we use the API).
* Schema: { data: [{ id, owned_by, pricing: { input, output }, context_length }] }
* where pricing.input/output are USD per 1M tokens (per 1M pixels for image
* output). The endpoint carries no type/capability fields, so those are
* derived from the model id (as the Requesty/Scaleway fetchers do).
*
* Requires NSCALE_API_KEY (local ../AIToolkit/.env or a CI secret). Without it
* the fetcher skips and the provider keeps its existing data.
*/
const { loadEnv } = require('../load-env');
loadEnv();
const { getJson } = require('../fetch-utils');
const API_URL = 'https://inference.api.nscale.com/v1/models';
const loadApiKey = () => process.env.NSCALE_API_KEY || null;
const EMBED_KEYWORDS = ['embed', 'bge', 'gte', 'e5-', 'stella', 'arctic-embed', 'nomic-embed'];
const IMAGE_KEYWORDS = ['flux', 'stable-diffusion', 'sdxl', 'sd3', 'text-to-image'];
const VISION_KEYWORDS = ['-vl', 'vl-', 'vision', 'pixtral', 'llava', 'llama-4'];
const REASON_KEYWORDS = ['deepseek-r1', 'qwq', 'thinking', 'magistral', 'gpt-oss', 'reason'];
const getSizeB = (id) => {
const match = (id || '').match(/(?:\b|-)(\d+(?:\.\d+)?)[Bb](?:\b|-|:|$)/);
if (!match) return undefined;
const n = parseFloat(match[1]);
return n > 0 && n < 2000 ? n : undefined;
};
// USD per 1M β the endpoint already reports per-million, keep as-is.
const perMillion = (v) => (v == null ? 0 : Math.round(parseFloat(v) * 10000) / 10000);
function classify(id) {
const s = (id || '').toLowerCase();
const isImage = IMAGE_KEYWORDS.some((k) => s.includes(k));
if (isImage) return { type: 'image', caps: [] };
if (EMBED_KEYWORDS.some((k) => s.includes(k))) return { type: 'embedding', caps: [] };
const caps = [];
const isVision = VISION_KEYWORDS.some((k) => s.includes(k));
if (isVision) caps.push('vision');
if (REASON_KEYWORDS.some((k) => s.includes(k))) caps.push('reasoning');
return { type: isVision ? 'vision' : 'chat', caps };
}
async function fetchNscale() {
const apiKey = loadApiKey();
if (!apiKey) {
console.warn(' (no NSCALE_API_KEY found β skipping Nscale)');
return [];
}
const data = await getJson(API_URL, {
headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
});
const models = [];
let skippedImage = 0;
for (const m of data.data || []) {
const id = m.id;
if (!id) continue;
const input = perMillion(m.pricing?.input);
const output = perMillion(m.pricing?.output);
const { type, caps } = classify(id);
// Image pricing is per-megapixel (not per-image / per-token), a different
// unit than our schema expresses β skip rather than publish misleading data.
if (type === 'image') { skippedImage++; continue; }
const entry = {
name: id,
type,
input_price_per_1m: input,
output_price_per_1m: output,
currency: 'USD',
};
if (caps.length) entry.capabilities = caps;
if (m.context_length) entry.context_window = m.context_length;
// Nscale ids are Hugging Face style (owner/model) β use as hf_id for enrichment.
if (/^[^/\s]+\/[^/\s]+$/.test(id)) entry.hf_id = id;
const size_b = getSizeB(id);
if (size_b) entry.size_b = size_b;
models.push(entry);
}
if (skippedImage) console.warn(` (skipped ${skippedImage} image model(s) β per-megapixel pricing not representable)`);
models.sort((a, b) => a.input_price_per_1m - b.input_price_per_1m);
return models;
}
module.exports = { fetchNscale, providerName: 'Nscale' };
// Run standalone: node scripts/providers/nscale.js
if (require.main === module) {
fetchNscale()
.then((models) => {
console.log(`Fetched ${models.length} models from Nscale API\n`);
models.slice(0, 20).forEach((m) =>
console.log(` ${m.name.padEnd(50)} ${m.type.padEnd(10)} $${m.input_price_per_1m} / $${m.output_price_per_1m}`)
);
if (models.length > 20) console.log(` ... and ${models.length - 20} more`);
})
.catch((err) => {
console.error('Error:', err.message);
process.exit(1);
});
}
|