Files
tf_docs/llm_project/probe/probe_aillm.js
T

187 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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); });