'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); });