187 lines
7.0 KiB
JavaScript
187 lines
7.0 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* probe_aillm.js — live-пробник api.aillm.ru (шаг 1 брифа).
|
||
*
|
||
* Проверяет:
|
||
* 1. response_format {type:"json_object"} — поддерживается?
|
||
* 2. stream:true — формат чанков
|
||
* 3. reasoning_effort:"low" — принимается? есть ли reasoning_content?
|
||
* 4. ответ на 429 (имитируем/смотрим заголовки при ошибках)
|
||
* 5. размер контекста (увеличиваем промпт до ошибки/предела)
|
||
* 6. пустой content / обрыв
|
||
* 7. задержка этапа-1-подобного запроса (3 замера)
|
||
*
|
||
* Ключ читается из файла (первый ключ sk-* после метки с "https"/"aillm").
|
||
* Аргументы командной строки с ключом НЕ принимаются.
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
|
||
const KEY_FILE = process.env.LLM_KEY_FILE || '/home/naeel/nubes/llm.key';
|
||
const BASE_URL = process.env.LLM_BASE_URL || 'https://api.aillm.ru';
|
||
const MODEL = process.env.LLM_MODEL || 'gpt-oss-120b';
|
||
|
||
// ── поиск ключа: первая строка, начинающаяся с sk-, после строки, содержащей 'https' ──
|
||
function findApiKey() {
|
||
const lines = fs.readFileSync(KEY_FILE, 'utf8').split(/\r?\n/);
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const t = lines[i].trim();
|
||
if (t.toLowerCase().includes('https') || t.toLowerCase().includes('aillm')) {
|
||
// дальше ищем ближайший sk-... в следующих 5 строках
|
||
for (let j = i + 1; j < Math.min(lines.length, i + 6); j++) {
|
||
const k = lines[j].trim();
|
||
if (k.startsWith('sk-')) return k;
|
||
}
|
||
}
|
||
}
|
||
// fallback: первый sk-* в файле
|
||
for (const l of lines) {
|
||
const t = l.trim();
|
||
if (t.startsWith('sk-')) return t;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
const API_KEY = findApiKey();
|
||
|
||
function log(label, obj) {
|
||
console.log(`\n=== ${label} ===`);
|
||
console.log(typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2));
|
||
}
|
||
|
||
async function chat(body, { timeoutMs = 60000 } = {}) {
|
||
const ctrl = new AbortController();
|
||
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
||
try {
|
||
const resp = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
||
method: 'POST',
|
||
headers: {
|
||
Authorization: `Bearer ${API_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(body),
|
||
signal: ctrl.signal,
|
||
});
|
||
const text = await resp.text();
|
||
let json = null;
|
||
try { json = JSON.parse(text); } catch { json = { _raw: text.slice(0, 500) }; }
|
||
return { status: resp.status, headers: Object.fromEntries(resp.headers), json };
|
||
} catch (e) {
|
||
return { status: 'ERR', error: e.name + ': ' + e.message };
|
||
} finally {
|
||
clearTimeout(timer);
|
||
}
|
||
}
|
||
|
||
function t() {
|
||
const s = Date.now();
|
||
return () => Date.now() - s;
|
||
}
|
||
|
||
async function main() {
|
||
if (!API_KEY) {
|
||
console.error('Ключ не найден в ' + KEY_FILE);
|
||
process.exit(1);
|
||
}
|
||
console.log('API_KEY: ' + API_KEY.slice(0, 6) + '*** (маска)');
|
||
console.log('BASE_URL: ' + BASE_URL);
|
||
console.log('MODEL: ' + MODEL);
|
||
|
||
// 1. response_format json_object
|
||
log('1. response_format json_object', await chat({
|
||
model: MODEL,
|
||
messages: [{ role: 'user', content: 'Верни JSON {"resource_type":"nubes_vc_vdc"} и ничего больше.' }],
|
||
response_format: { type: 'json_object' },
|
||
temperature: 0,
|
||
}));
|
||
|
||
// 3. reasoning_effort low
|
||
log('3. reasoning_effort low', await chat({
|
||
model: MODEL,
|
||
messages: [{ role: 'user', content: 'Одним словом: что такое vDC?' }],
|
||
reasoning_effort: 'low',
|
||
temperature: 0,
|
||
}));
|
||
|
||
// 7. задержка этапа 1 (3 замера)
|
||
log('7. задержка (3 замера этап-1-подобного)', await (async () => {
|
||
const results = [];
|
||
for (let i = 0; i < 3; i++) {
|
||
const d = t();
|
||
await chat({
|
||
model: MODEL,
|
||
messages: [
|
||
{ role: 'system', content: 'Ты ассистент документации. Ответь строгим JSON: {"resource_type":"..."}.' },
|
||
{ role: 'user', content: 'как создать виртуальный датацентр' },
|
||
],
|
||
temperature: 0,
|
||
});
|
||
results.push(d());
|
||
}
|
||
return results;
|
||
})());
|
||
|
||
// 5. размер контекста — увеличиваем промпт
|
||
log('5. контекст (пробуем ~8k и ~16k токенов)', await (async () => {
|
||
const mk = (words) => Array(words).fill('слово').join(' ');
|
||
const r = {};
|
||
for (const words of [2000, 8000, 16000]) {
|
||
const d = t();
|
||
const res = await chat({
|
||
model: MODEL,
|
||
messages: [{ role: 'user', content: `Повтори одно слово "ок". ${mk(words)}` }],
|
||
max_tokens: 10,
|
||
});
|
||
r[words] = { ms: d(), status: res.status, err: res.error || (res.json && res.json.error ? res.json.error.message : null) };
|
||
}
|
||
return r;
|
||
})());
|
||
|
||
// 6. пустой content (маленький max_tokens + reasoning)
|
||
log('6. маленький max_tokens (пустой content?)', await chat({
|
||
model: MODEL,
|
||
messages: [{ role: 'user', content: 'Распиши подробно архитектуру Terraform провайдера Nubes.' }],
|
||
max_tokens: 1,
|
||
temperature: 0,
|
||
}));
|
||
|
||
// 2. stream
|
||
log('2. stream:true (первые байты)', await (async () => {
|
||
try {
|
||
const resp = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
||
method: 'POST',
|
||
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ model: MODEL, messages: [{ role: 'user', content: 'Скажи "привет"' }], stream: true }),
|
||
});
|
||
const reader = resp.body.getReader();
|
||
const dec = new TextDecoder();
|
||
let buf = '';
|
||
for (let i = 0; i < 3; i++) {
|
||
const { value, done } = await reader.read();
|
||
if (done) break;
|
||
buf += dec.decode(value, { stream: true });
|
||
}
|
||
return { status: resp.status, firstChunks: buf.slice(0, 800) };
|
||
} catch (e) {
|
||
return { error: e.name + ': ' + e.message };
|
||
}
|
||
})());
|
||
|
||
// 4. 429 — пытаемся получить, посылая запрос с заведомо неверным (но тем же) ключом не будем; отдельно проверим /v1/models
|
||
log('4. /v1/models (проверка доступности)', await (async () => {
|
||
try {
|
||
const resp = await fetch(`${BASE_URL}/v1/models`, {
|
||
headers: { Authorization: `Bearer ${API_KEY}` },
|
||
});
|
||
const text = await resp.text().catch(() => '');
|
||
let m = null; try { m = JSON.parse(text).data.map(x => x.id); } catch { m = text.slice(0, 500); }
|
||
return { status: resp.status, models: m };
|
||
} catch (e) {
|
||
return { error: e.name + ': ' + e.message };
|
||
}
|
||
})());
|
||
}
|
||
|
||
main().catch((e) => { console.error('FATAL', e); process.exit(1); });
|