File size: 5,376 Bytes
ffba252
 
 
49e63bd
ffba252
43d89fe
 
 
ffba252
43d89fe
49e63bd
43d89fe
 
 
 
 
49e63bd
43d89fe
 
 
ffba252
 
43d89fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49e63bd
43d89fe
 
 
 
 
 
ffba252
 
43d89fe
 
 
 
 
ffba252
 
43d89fe
 
 
 
ffba252
49e63bd
43d89fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ffba252
 
49e63bd
43d89fe
 
 
 
 
 
 
 
 
49e63bd
43d89fe
 
49e63bd
ffba252
43d89fe
 
 
 
 
 
 
 
49e63bd
ffba252
 
43d89fe
49e63bd
ffba252
 
 
 
 
 
 
 
43d89fe
ffba252
49e63bd
ffba252
 
43d89fe
 
 
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
'use strict';

/**
 * Nebius Token Factory (formerly Nebius AI Studio) pricing fetcher.
 *
 * The public "Model endpoints" page (tokenfactory.nebius.com/endpoints) lists
 * every shared model with pricing to anonymous users. It loads that data from
 * a CSRF-protected proxy endpoint — no API key required:
 *
 *   GET https://tokenfactory.nebius.com/proxy/inference/private/v1/models_info
 *
 * To call it we first load the app once to obtain an anonymous CSRF cookie
 * (__Host-psifi.x-csrf-token), then replay it as both the Cookie and the
 * x-csrf-token header. The response is richer than the authenticated
 * /v1/models API: each model carries huggingface_url, size_b, vendor, tags and
 * `flavors` with per-million-token prices.
 *
 *   { models: [ { type, name, huggingface_url, size_b, flavors: [
 *       { model_id, model_type, label, input_price_per_million_tokens,
 *         output_price_per_million_tokens, use_cases, tags, context_window_k } ] } ] }
 */

const { fetchRobust } = require('../fetch-utils');

const BOOTSTRAP_URL = 'https://tokenfactory.nebius.com/endpoints';
const MODELS_URL = 'https://tokenfactory.nebius.com/proxy/inference/private/v1/models_info';
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';

// Load the app once to mint an anonymous CSRF token (returned as a cookie).
async function getCsrf() {
  const res = await fetchRobust(BOOTSTRAP_URL, { headers: { 'User-Agent': UA } });
  const setCookie = res.headers.get('set-cookie') || '';
  const m = setCookie.match(/__Host-psifi\.x-csrf-token=([^;,]+)/);
  if (!m) throw new Error('could not obtain CSRF cookie from Token Factory');
  const cookieVal = m[1];
  const headerVal = decodeURIComponent(cookieVal).split('|')[0];
  return { cookieVal, headerVal };
}

const getType = (modelType) => {
  const t = (modelType || '').toLowerCase();
  if (t.includes('embedding')) return 'embedding';
  if (t === 'image2text' || t.includes('vision')) return 'vision';
  if (t === 'text2image' || t.includes('image_generation')) return 'image';
  return 'chat';
};

const hfIdFrom = (model, flavor) => {
  const url = model.huggingface_url || '';
  if (url.includes('huggingface.co/')) return url.split('huggingface.co/')[1].replace(/\/+$/, '');
  const id = flavor.model_id || '';
  return /^[^/\s]+\/[^/\s]+$/.test(id) ? id : undefined;
};

function buildEntry(model, flavor, labelSuffix) {
  const type = getType(flavor.model_type || model.type);
  const input = Number(flavor.input_price_per_million_tokens);
  const output = Number(flavor.output_price_per_million_tokens);

  const caps = [];
  const uses = (flavor.use_cases || []).map((u) => u.toLowerCase());
  const tags = (flavor.tags || model.tags || []).map((t) => t.toLowerCase());
  if (type === 'vision') caps.push('vision');
  if (uses.includes('function_calling') || tags.includes('tool use')) caps.push('tools');
  if (uses.includes('reasoning') || tags.includes('reasoning')) caps.push('reasoning');

  const entry = {
    name: model.name + (labelSuffix ? ` (${labelSuffix})` : ''),
    type,
    input_price_per_1m: isFinite(input) ? input : 0,
    output_price_per_1m: isFinite(output) ? output : 0,
    currency: 'USD',
  };
  if (caps.length) entry.capabilities = caps;

  const hf_id = hfIdFrom(model, flavor);
  if (hf_id) entry.hf_id = hf_id;

  const size_b = Number(model.size_b) || undefined;
  if (size_b) entry.size_b = size_b;

  const ctx = Number(flavor.context_window_k || model.context_window_k);
  if (ctx) entry.context_window = ctx * 1000;

  return entry;
}

async function fetchNebius() {
  const { cookieVal, headerVal } = await getCsrf();

  const res = await fetchRobust(MODELS_URL, {
    headers: {
      'User-Agent': UA,
      Accept: 'application/json',
      'x-csrf-token': headerVal,
      Cookie: `__Host-psifi.x-csrf-token=${cookieVal}`,
    },
  });
  const data = await res.json();
  const list = data.models || data.data || (Array.isArray(data) ? data : []);

  const models = [];
  for (const model of list) {
    const flavors = Array.isArray(model.flavors) && model.flavors.length ? model.flavors : [{}];
    const multi = flavors.length > 1;
    for (const flavor of flavors) {
      // Only label-suffix when a model exposes several priced variants.
      const suffix = multi ? (flavor.label || flavor.quantization) : '';
      const entry = buildEntry(model, flavor, suffix);
      if (entry.input_price_per_1m > 0 || entry.output_price_per_1m > 0) models.push(entry);
    }
  }

  models.sort((a, b) => a.input_price_per_1m - b.input_price_per_1m);
  return models;
}

module.exports = { fetchNebius, providerName: 'Nebius' };

// Run standalone: node scripts/providers/nebius.js
if (require.main === module) {
  fetchNebius()
    .then((models) => {
      console.log(`Fetched ${models.length} models from Nebius (public models_info):\n`);
      const byType = {};
      models.forEach((m) => { (byType[m.type] = byType[m.type] || []).push(m); });
      for (const [type, ms] of Object.entries(byType)) {
        console.log(`  [${type}]`);
        ms.forEach((m) =>
          console.log(`    ${m.name.padEnd(45)} $${m.input_price_per_1m} / $${m.output_price_per_1m}`)
        );
      }
    })
    .catch((err) => {
      console.error('Error:', err.message);
      process.exit(1);
    });
}