IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya13/34-dars25 daqiqa
Mundarija (34)

LLM API asoslari: backend'dan modelga so'rov, xabarlar, tokenlar va narx

Qisqacha: LLM API — oddiy HTTP so'rov: siz model nomi, tizim ko'rsatmasi, xabarlar ro'yxati va sozlamalarni yuborasiz, javobda matn va token hisobini olasiz. Narx tokenlarda hisoblanadi, va bir xil ma'noli o'zbekcha gap inglizchadan taxminan 1.7 baravar ko'p token oladi. API kalit faqat serverda, faqat sarlavhada yuradi — brauzerga hech qachon tushmaydi. Kanon tajribasida temperature: 0 ham bir xil javob kafolatlamadi, "fikrlovchi" model esa 11 baravar sekin ishladi va javobni yarmida kesdi.

Bu darsda

  • LLM API so'rovining tuzilishini — tizim ko'rsatmasi, rollar, sozlamalar — tushuntira olasiz.
  • Javobdagi finishReason va token hisobini o'qiysiz.
  • Token, kontekst oynasi va narxni hisoblaysiz; o'zbekcha matn nega qimmatroq ekanini bilasiz.
  • temperature, maxOutputTokens va fikrlash rejimining haqiqiy ta'sirini kanon o'lchovida ko'rasiz.
  • API kalitni xavfsiz saqlaysiz va xato kodlarini to'g'ri turlaysiz.
  • vazifalar-api ga provayder shartnomasi va Gemini provayderini qo'shasiz.

Oldin bilishingiz kerak: Built-in fetch va undici, Sirlarni boshqarish, AI bilan samarali ishlash: prompting va kontekst, Bekor qilish: AbortController.

Oldingi ikki darsda ma'lumotni himoyalash va o'chirishni qurdik. Endi 31-qismning ikkinchi katta mavzusi boshlanadi: vazifalar-api ga AI xususiyati. Keyingi uchta darsda bitta yo'l quriladi — POST /v1/vazifalar/:id/qadamlar: katta vazifani bajariladigan kichik qadamlarga bo'lish. Bu dars — poydevor: modelga so'rov qanday yuboriladi va javob qanday o'qiladi.

1. Nega bu kerak?

Aziz ro'yxatiga yozdi: "Bitiruv malakaviy ishining kirish qismini yozish". Uch hafta o'tdi, vazifa joyida. Muammo — vazifa juda katta: qayerdan boshlashni bilmaysiz. Agar ilova uni "manbalarni to'plash — 2 soat", "dolzarblikni yozish — 1 soat" kabi qadamlarga bo'lib bersa, boshlash osonlashadi.

Bu ishni oddiy kod qila olmaydi: har vazifa boshqacha. Til modeli (LLM — large language model) esa matnni tushunib, matn bilan javob beradi. Bizga uni chaqirish kerak.

Birinchi xayol — frontend'dan to'g'ridan chaqirish. Lekin unda API kalit brauzerga tushadi. Kalit esa — sizning hisobingiz va pulingiz: kimdir DevTools'ni ochib uni ko'chiradi va o'z ishlariga ishlatadi. Shuning uchun model faqat backend orqali chaqiriladi:

sequenceDiagram
  participant B as Brauzer
  participant A as vazifalar-api
  participant G as Gemini API
  B->>A: POST /v1/vazifalar/7/qadamlar (cookie)
  A->>A: sessiya, egalik, kvota
  A->>G: generateContent (x-goog-api-key)
  G-->>A: matn + tokenlar
  A-->>B: qadamlar (kalitsiz)

Backend "o'rtadagi qorovul" vazifasini bajaradi: kim so'rayotganini tekshiradi (28-qism), vazifa shu odamniki ekanini ko'radi (RLS), keyin modelga boradi. Kalit backend'dan tashqariga chiqmaydi.

2. LLM API qanday ishlaydi

2.1 Bitta so'rov — bitta javob

LLM — keyingi so'z bo'lagini bashorat qiluvchi model. API orqali u oddiy HTTP xizmatga o'xshaydi: POST so'rov, JSON tana, JSON javob. Ikki muhim xususiyati bor.

Birinchisi — holatsiz (stateless). Model oldingi so'rovingizni eslab qolmaydi. Suhbat davom etishi uchun har so'rovda butun tarixni qayta yuborasiz. Bu tokenlar va narxga bevosita ta'sir qiladi: o'ninchi xabarda oldingi to'qqiztasi ham hisoblanadi.

Ikkinchisi — deterministik emas. Bir xil so'rov ikki marta turli javob berishi mumkin. Buni o'lchovda ko'ramiz.

2.2 So'rov tuzilishi

Kanon tajribasida Gemini'ga SDK'siz, oddiy fetch bilan so'rov yuborildi. Tajriba skriptidan soddalashtirilgan parcha (vaqt o'lchash va xato ushlash olib tashlandi):

js
const API = "https://generativelanguage.googleapis.com/v1beta";
const KALIT = process.env.GEMINI_API_KEY ?? "";

async function yubor(model, tana) {
  const r = await fetch(`${API}/models/${model}:generateContent`, {
    method: "POST",
    signal: AbortSignal.timeout(90_000),
    headers: {
      "content-type": "application/json",
      "x-goog-api-key": KALIT,
    },
    body: JSON.stringify(tana),
  });
  return { status: r.status, j: await r.json() };
}

const VAZIFA = "Bitiruv malakaviy ishining kirish qismini yozish";
const tana = {
  systemInstruction: { parts: [{
    text: "Sen vazifalar ilovasidagi yordamchisan. "
      + "Javobni o'zbek tilida (lotin), qisqa ber.",
  }] },
  contents: [{ role: "user", parts: [{
    text: "Shu vazifani 4 ta aniq qadamga bo'l, "
      + `har qadam bitta qatorda: "${VAZIFA}"`,
  }] }],
  generationConfig: { temperature: 0.2, maxOutputTokens: 512 },
};
const { j } = await yubor("gemini-3.5-flash-lite", tana);

Tananing uchta qismi bor:

Qism Nima O'xshatish
systemInstruction tizim ko'rsatmasi (system prompt) xodimga ish boshida berilgan yo'riqnoma
contents suhbat: user va model xabarlari mijoz bilan yozishma
generationConfig sozlamalar: harorat, maksimal token "qisqa yoz", "ijodkor bo'l"

Tizim ko'rsatmasi foydalanuvchi xabaridan alohida maydonda keladi. Bu muhim: model tizim ko'rsatmasiga foydalanuvchi matnidan ko'proq ishonadi. Bu ajratish prompt injection'ga qarshi birinchi himoya — uni Strukturali javob va tool calling darsida chuqur ko'ramiz.

AbortSignal.timeout(90_000) ga e'tibor bering. U kanonda keyin qo'shildi — sababini «Fikrlash rejimi» bo'limida ko'rasiz.

Maslahat: Gemini hujjatini ochsangiz, adashmang. 2026-11 holatiga qo'llanmalar endi asosan yangi Interactions API ni (/v1beta/interactions) ko'rsatadi. Unda maydonlar boshqacha nomlangan: system_instruction, input, generation_config. Kanon ishlatgan generateContent esa API ma'lumotnomasida turibdi va 2026-11-02 da ishladi. G'oya ikkalasida bir xil — tizim ko'rsatmasi, xabarlar, sozlamalar. Shuning uchun ilova kodi provayder shakliga emas, o'z shartnomasiga tayanishi kerak — loyiha qadamida aynan shuni quramiz.

2.3 Provayderlar: bir g'oya, uch shakl

Gemini, OpenAI va Anthropic API'lari bir xil g'oyada ishlaydi, lekin maydon nomlari farq qiladi. Kanon uchala provayder uchun kod yozdi; haqiqiy kalit bilan faqat Gemini sinaldi (OpenAI va Anthropic — sinalmagan, tip tekshiruvidan o'tadi):

Gemini OpenAI (va mos API'lar) Anthropic
Tizim ko'rsatmasi systemInstruction messages[0], role: "system" system (alohida)
Model roli model assistant assistant
Maks. token maxOutputTokens max_completion_tokens max_tokens (majburiy)
Kalit sarlavhasi x-goog-api-key Authorization: Bearer x-api-key

Bitta neytral so'rovdan ikki xil tana yasashni brauzerda sinab ko'ring:

js
function geminiTanasi(sorov) {
  return {
    systemInstruction: { parts: [{ text: sorov.tizim }] },
    contents: sorov.xabarlar.map((x) => ({
      role: x.rol,
      parts: [{ text: x.matn }],
    })),
    generationConfig: {
      temperature: sorov.harorat ?? 0.2,
      maxOutputTokens: sorov.maksToken ?? 1024,
    },
  };
}

function openaiTanasi(sorov, model) {
  return {
    model,
    messages: [
      { role: "system", content: sorov.tizim },
      ...sorov.xabarlar.map((x) => ({
        role: x.rol === "model" ? "assistant" : "user",
        content: x.matn,
      })),
    ],
    temperature: sorov.harorat ?? 0.2,
    max_completion_tokens: sorov.maksToken ?? 1024,
  };
}

const sorov = {
  tizim: "Sen vazifalar ilovasidagi yordamchisan.",
  xabarlar: [
    { rol: "user", matn: "Non olishni qadamlarga bo'l" },
    { rol: "model", matn: "1. Do'konga borish" },
    { rol: "user", matn: "Yana qisqaroq" },
  ],
  maksToken: 200,
};
const g = geminiTanasi(sorov);
console.log(g.contents.map((c) => c.role).join(" → "));
console.log(g.generationConfig);
for (const m of openaiTanasi(sorov, "model-nomi").messages) {
  console.log(`${m.role}: ${m.content}`);
}

Konsolda:

text
user → model → user
{ temperature: 0.2, maxOutputTokens: 200 }
system: Sen vazifalar ilovasidagi yordamchisan.
user: Non olishni qadamlarga bo'l
assistant: 1. Do'konga borish
user: Yana qisqaroq

Uchta xabar — holatsizlikning amaldagi ko'rinishi: ikkinchi user xabari ("Yana qisqaroq") oldingi ikkitasisiz ma'nosiz. Suhbat tarixini model emas, biz saqlaymiz va har safar qayta yuboramiz. Ikkinchi funksiyada tizim ko'rsatmasi ro'yxatning boshiga tushdi, model esa assistant ga aylandi. Ilova kodi bitta neytral shaklni biladi, provayder fayli uni o'z formatiga o'giradi — loyiha qadamida aynan shunday qatlam quramiz.

3. Javobni o'qish

3.1 Kanon o'lchovi: ikki model

Kanon bir xil so'rovni (yuqoridagi tana) ikki modelga yubordi. Modellar ro'yxati shu kuni API'dan olindi — kanon faqat haqiqatda mavjud nomlarni ishlatdi. Har --- qatorida uchta narsaga qarang: vaqt (ms), finish va tokenlar. Haqiqiy chiqish (2026-11-02, Toshkentdan):

text
=== 1) bitta so'rov, ikki model (system instruction + user xabari)
--- gemini-3.5-flash-lite: HTTP 200, 1415 ms, finish=STOP tokenlar: kirish=77 chiqish=93 fikrlash=0 jami=170, modelVersion=gemini-3.5-flash-lite
1. Mavzuning dolzarbligi, maqsadi va vazifalarini aniqlash.
2. Obyekt, predmet va ilmiy farazni shakllantirish.
3. Qo'llanilgan tadqiqot usullari va ishning ilmiy yangiligini yozish.
4. Kirish qismini umumiy talablar asosida tahrirlash va yakunlash.
--- gemini-3.8-flash: HTTP 200, 16317 ms, finish=MAX_TOKENS tokenlar: kirish=77 chiqish=85 fikrlash=423 jami=585, modelVersion=gemini-3.8-flash
1. Mavzuning dolzarbligi va o'rganilganlik darajasini yoritish.
2. Tadqiqotning maqsadi va vazifalarini aniq belgilash.
3. Ishning obyekti, predmeti va tadqiqot metodlarini yozish.
4. Ilmiy yangiligi, amaliy ahamiyati va ish tuzilishini

Nima bo'ldi? Ikkala model ham HTTP 200 qaytardi. Lekin birinchisi 1.4 soniyada to'liq javob berdi. Ikkinchisi 16 soniya ishladi, 423 token "o'yladi" va javobni yarmida kesdi (MAX_TOKENS). Bu uch farqni keyingi bo'limlarda birma-bir ochamiz.

AI javoblari deterministik emas: siz xuddi shu so'rovni yuborsangiz, boshqa matn olasiz. Shuning uchun bu — "bir yugurish" natijasi.

3.2 finishReason — javob nega tugadi

Javobdagi eng muhim maydon — finishReason:

Qiymat Ma'nosi Kod nima qilsin
STOP model o'zi tugatdi normal
MAX_TOKENS token chegarasiga yetdi — javob kesilgan qayta urinish yoki chegara oshirish
SAFETY va boshqalar xavfsizlik filtri to'xtatdi foydalanuvchiga tushunarli xato

Ikkinchi modelning javobiga qarang: to'rtinchi qadam "ish tuzilishini" so'zida uzilgan. HTTP 200 — muvaffaqiyat, lekin javob yaroqsiz. finishReason ni tekshirmaydigan kod buni sezmaydi va foydalanuvchiga yarim gapni ko'rsatadi.

3.3 Token hisobi

usageMetadata da uchta son keladi: kirish (promptTokenCount), chiqish (candidatesTokenCount) va fikrlash (thoughtsTokenCount). Pul shu sonlar bo'yicha olinadi. Ikkinchi model 85 token javob yozdi, lekin undan oldin 423 token "o'yladi". O'ylash ham chiqish narxida hisoblanadi.

4. Tokenlar, kontekst oynasi va narx

4.1 Token nima

Token — model ko'radigan matn bo'lagi. U so'z ham, so'zning qismi ham, tinish belgisi ham bo'lishi mumkin. Model matnni harf yoki so'z bilan emas, token bilan o'qiydi va yozadi. Narx ham, chegaralar ham tokenda.

Kanon countTokens usuli bilan bir xil ma'noli ikki gapni o'lchadi:

text
=== 5) countTokens: bir xil ma'noli gap — o'zbekcha va inglizcha (token = narx)
uz: 79 belgi, 9 so'z → 29 token
en: 78 belgi, 15 so'z → 17 token

Gaplar: "Ertaga soat to'qqizda bo'lim boshlig'iga haftalik hisobotni topshirishim kerak." va uning inglizcha tarjimasi.

Bir xil ma'noli gap: token soni
  • o'zbekcha (79 belgi, 9 so'z)29 token
  • inglizcha (78 belgi, 15 so'z)17 token

Manba: IlmHamroh kanon o'lchovi, 2026-11-02: Gemini countTokens, gemini-3.5-flash-lite; bitta gap juftligi

Belgilar soni deyarli teng, so'zlar o'zbekchada kamroq, tokenlar esa 1.7 baravar ko'p. Sabab — tokenizator (matnni bo'laklarga bo'luvchi) asosan ingliz matnida o'rgatilgan: "the", "report" — bittadan token. O'zbekcha "topshirishim" va "boshlig'iga" esa bir necha bo'lakka bo'linadi. Apostrof ham alohida bo'lak bo'lishi mumkin. Amaliy xulosa: o'zbekcha mahsulotning AI xarajatini inglizcha maqolalardagi raqamlar bilan rejalashtirmang — o'z matningizda o'lchang. Bu bitta gap juftligi o'lchovi: boshqa matnda nisbat boshqacha chiqishi mumkin.

4.2 Kontekst oynasi

Kontekst oynasi (context window) — bitta so'rovda model ko'ra oladigan maksimal token: tizim ko'rsatmasi, butun suhbat va javob birga. Kanon olgan modellar ro'yxatidan:

text
models/gemini-3.5-flash-lite                     in= 1048576 out= 65536  generateContent,countTokens,createCachedContent,batchGenerateContent

Kirish — 1 048 576 token, chiqish — 65 536. Bir million token — bir necha qalin kitob. Lekin katta oyna "hammasini yuboraveramiz" degani emas: har token pul turadi va har so'rovda qayta yuboriladi. vazifalar-api bitta vazifa matnini yuboradi — ~200 token.

4.3 Narxni hisoblash

Provayderlar narxni odatda 1 million token uchun beradi, kirish va chiqish alohida. Chiqish doim qimmatroq. Quyidagi narxlar shartli — hisoblash usulini ko'rsatish uchun. Haqiqiy narxni provayderning narxlar sahifasidan oling, ular tez-tez o'zgaradi:

js
// Shartli narx — 1 mln token uchun AQSh dollarida (haqiqiy emas!)
const NARX = { kirish: 0.10, chiqish: 0.40 };

function narx(tokenlar) {
  const d = (tokenlar.kirish * NARX.kirish
    + tokenlar.chiqish * NARX.chiqish) / 1e6;
  return Number(d.toFixed(6));
}

const lite = { kirish: 77, chiqish: 93 };
const fikrlovchi = { kirish: 77, chiqish: 85 + 423 };
console.log("lite:", narx(lite), "$");
console.log("fikrlash bilan:", narx(fikrlovchi), "$");
console.log("nisbat:", (narx(fikrlovchi) / narx(lite)).toFixed(1));

const oylik = 2000 * 30 * 3; // foydalanuvchi × kun × so'rov
console.log("oyiga:", (narx(lite) * oylik).toFixed(2), "$");

Konsolda:

text
lite: 0.000045 $
fikrlash bilan: 0.000211 $
nisbat: 4.7
oyiga: 8.10 $

Bitta so'rov arzimas ko'rinadi. Lekin 2 000 foydalanuvchi kuniga uch martadan bossa — oyiga 180 000 so'rov. Fikrlovchi model bilan xuddi shu hisob 4.7 baravar qimmat. Shartli narxda ham manzara aniq: modelni tanlash — arxitektura qarori, u har oy hisob-fakturada ko'rinadi. Kvota va xarajat nazoratini AI xususiyatlarini prodakshnda ishlatish darsida quramiz.

5. Sozlamalar: harorat, chegara va fikrlash

5.1 temperature — tasodiflik darajasi

Harorat (temperature) — model keyingi tokenni qanchalik "erkin" tanlashi. Past qiymat — eng ehtimolli variant, yuqori — xilma-xil. Ko'p qo'llanmalarda "temperature: 0 — har safar bir xil javob" deyiladi. Kanon buni tekshirdi: bitta so'rov, ikki marta:

text
=== 3) temperature: 0 va 1.5 — bir xil so'rov ikki marta (lite)
temperature=0: ["Kirish qismini yozish tartibi","Bitiruv ishining kirish qismi"] — farqli
temperature=1.5: ["Kirish qismini yozish tartibi","BMI kirish qismini yozish tartibi"] — farqli

temperature: 0 da ham ikki xil sarlavha. Provayder tomonidagi hisoblash (parallel apparat, yuklama) to'liq takrorlanmaydi. Saboq ikki tomonlama:

  • Testlarda haqiqiy modelga tayanmang. Kanon testlari soxta provayder bilan yozilgan — tarmoqqa chiqmaydi va har safar bir xil javob beradi.
  • Prodakshnda javobni har safar tekshiring: shakli, uzunligi, mazmuni. "Kecha ishlagan edi" — LLM uchun dalil emas.

Kanon 0.2 ni tanladi: qadamlar aniq bo'lsin, lekin so'zlar "qotib" qolmasin.

5.2 maxOutputTokens — javob chegarasi

Chegara — xarajat va kechikishni nazorat qilish vositasi. Lekin juda kichik chegara javobni buzadi:

text
=== 4) maxOutputTokens=12 — javob kesiladi
finish=MAX_TOKENS tokenlar: kirish=46 chiqish=8 fikrlash=0 jami=54 → "1. Mavzuning dolzarbl"

12 token chegara bilan model "dolzarbl" da to'xtadi. Hisobda chiqish 8 token ko'rsatilgan, chegara esa 12 edi — provayderlar chegarani har doim aynan bir xil sanamaydi. Kanon 1 024 ni tanladi: 8 ta qadam uchun yetarli, cheksiz javobga yo'l qo'ymaydi.

5.3 Fikrlash rejimi va osilib qolgan so'rov

Yangi modellarning ko'pi javobdan oldin "o'ylaydi" — ichki fikrlash tokenlarini yozadi. Murakkab masalada bu sifatni oshiradi. Bizning vazifamiz oddiy: to'rt qadam yozish. Kanon raqamlarini solishtiring:

gemini-3.5-flash-lite gemini-3.8-flash
Vaqt 1 415 ms 16 317 ms (11.5 ×)
Chiqish + fikrlash tokeni 93 85 + 423 = 508 (5.5 ×)
Natija STOP, to'liq MAX_TOKENS, kesilgan

Fikrlash 512 token chegarasining katta qismini yeb qo'ydi va javobga joy qolmadi. Kanon fikrlashni kamaytirishga urindi:

text
=== 2) thinking: gemini-3.8-flash, thinkingLevel=minimal (fikrlash tokenlari pul turadi)
HTTP 400, 256 ms, finish=undefined tokenlar: kirish=undefined chiqish=undefined fikrlash=0 jami=undefined xato: Thinking level MINIMAL is not supported for this model. Please retry with other thinking level.

"Bu model uchun MINIMAL fikrlash darajasi qo'llab-quvvatlanmaydi" — sozlamalar modelga bog'liq, hujjatdagi umumiy ro'yxat har modelda ishlamaydi. Kanon xulosasi: vazifaga mos eng oddiy model — gemini-3.5-flash-lite.

Yana bir hodisa — o'lchov jurnalida. Birinchi yugurishda lite model 32 290 ms javob berdi (tarmoq yoki "sovuq" ishga tushish). 3.8-flash'ga so'rov esa umuman javob qaytarmadi: ulanish ochiq turaverdi, va jarayon 200 soniyadan keyin qo'lda to'xtatildi. O'shanda skriptda timeout yo'q edi. Shundan keyin har so'rovga AbortSignal.timeout(90_000) qo'shildi. Loyihada esa chegara qattiqroq — AI_TIMEOUT_MS, sukut bo'yicha 20 soniya. Saboq: tashqi API'ga timeout'siz so'rov — foydalanuvchini abadiy kuttirish va server ulanishlarini band qilish.

Tekshirib ko'ring: Javob HTTP 200, finishReason: "MAX_TOKENS". Kod foydalanuvchiga nima qaytarishi kerak?

Javob

Yarim javobni "tayyor" deb ko'rsatmaslik kerak. Vazifamizda javob — qadamlar ro'yxati; uzilgan oxirgi qadam yaroqsiz. Variantlar: chegarani oshirib qayta so'rash, to'liq kelgan qadamlarni ajratib olish yoki xato qaytarish. Kanon Strukturali javob va tool calling darsida JSON sxema bilan ishlaydi: kesilgan JSON tekshiruvdan o'tmaydi va bitta tuzatish so'rovi yuboriladi. HTTP status — yetarli signal emas.

6. Xatolar va API kalit

6.1 Noto'g'ri kalit — 401 emasmi?

Kanon ataylab noto'g'ri kalit yubordi:

text
=== 6) noto'g'ri kalit — xato shakli (haqiqiy kalit emas)
HTTP 400: INVALID_ARGUMENT — API key not valid. Please pass a valid API key.

"API kaliti yaroqsiz" — lekin status 400, 401 emas. Problem Details darsida status kodlarini o'rgangan o'quvchi 401 kutadi. Provayderlar standartdan chetga chiqadi. Shuning uchun xatoni faqat status bo'yicha emas, provayderning o'z kodi (INVALID_ARGUMENT) va matni bo'yicha ham turlash kerak.

Muhimroq xatolar — vaqtinchalik: 429 (limit — juda ko'p so'rov) va 503 (provayder band). Ular uchun qayta urinish o'rinli. 400 uchun esa ma'nosiz: kalit qayta urinishda to'g'rilanib qolmaydi. Qayta urinish siyosatini Resilience naqshlari darsida quramiz; hozir xatolarni turlab qo'yamiz.

6.2 Kalitni xavfsiz saqlash

Sirlarni boshqarish darsidagi qoidalar AI kalitiga to'liq tegishli. Bunga yana uchtasi qo'shiladi:

  1. Faqat sarlavhada. Gemini kalitni URL parametri (?key=…) sifatida ham qabul qiladi. Lekin URL loglarga, proxy jurnallariga va xato hisobotlariga tushadi. Kanonda kalit faqat x-goog-api-key sarlavhasida. Kanonning so'rov hisoblagichi URL'da key= ko'rsa, so'rovni umuman yubormaydi.
  2. Faqat muhitdan. Kod process.env.GEMINI_API_KEY ni o'qiydi; .env.example da qiymat bo'sh. Kanon tajribalari tugagach, butun kanon papkasida (3 143 fayl) kalit qiymati qidirildi — 0 faylda topildi.
  3. Logga — hech qachon. Xato xabariga ham. Kanonning Gemini provayderi xato matnini 200 belgigacha kesib loglaydi, so'rov sarlavhalarini esa umuman yozmaydi.

Diqqat: Kalit bir marta git'ga yoki ochiq chatga tushsa — uni darhol bekor qiling va yangisini yarating. "O'chirib qo'ydim" yetmaydi: git tarixida va boshqalarning nusxalarida qoladi.

6.3 SDK'lar haqida

Har provayderning rasmiy SDK'si bor: OpenAI SDK (openai), Anthropic SDK (@anthropic-ai/sdk), Google Gen AI SDK (@google/genai). Ular tana yasash, xatolar, qayta urinish va oqimni o'zi bajaradi. Kanon SDK'siz yo'lni tanladi — 0 qo'shimcha paket, xatolar aniq bizning turlarimizga o'giriladi, kalit va oqim to'liq nazoratda. Bu tanlovning narxi va SDK'lar bilan o'lchovli solishtirish — Vercel AI SDK va provayderlarni almashtirish darsida. Bu darsda fetch ning afzalligi boshqa: so'rovning har maydonini ko'rasiz, hech narsa "sehrli" emas.

7. Ko'p uchraydigan xatolar

Noto'g'ri tasavvur Haqiqat
"Frontend'dan chaqirsak tezroq" Kalit brauzerga tushadi; hamma chaqiruv backend orqali
"temperature: 0 — har safar bir xil javob" Kanon o'lchovida ikki xil javob; testda soxta provayder
"HTTP 200 — javob tayyor" finishReason: MAX_TOKENS — javob kesilgan
"Kuchliroq model — har doim yaxshiroq" 11.5 × sekin, 5.5 × ko'p chiqish tokeni va kesilgan javob
"Inglizcha narx hisobi bizga ham to'g'ri" O'zbekcha bir gap ~1.7 × ko'p token oldi
"Model suhbatni eslab qoladi" Holatsiz: tarixni har so'rovda o'zingiz yuborasiz
"Kalitni URL'ga qo'ysa ham bo'ladi" URL loglarga tushadi; faqat sarlavhada
"Timeout kerak emas — provayder javob beradi" Kanonda so'rov 200 s osilib qoldi

8. Mashqlar

Mashqlar kurs/mashqlar/31/13-llm-api/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).

1-mashq (oson): bo'shliqlarni to'ldiring

  • Javob token chegarasida uzilganini bildiruvchi finishReason —
  • Gemini'da tizim ko'rsatmasi maydoni — system
  • Kanon kalitni qaysi sarlavhada yuboradi — x-goog-
  • Kanon o'lchovida o'zbekcha gap necha token oldi —
  • Model oldingi so'rovni eslamaydi: API
Yechim
  • MAX_TOKENS — HTTP 200 bo'lsa ham javob kesilgan.
  • Instruction — systemInstruction; OpenAI'da role: "system" xabari.
  • api-key — URL'dagi ?key= emas.
  • 29 — inglizchasi 17 token.
  • holatsiz (stateless) — suhbat tarixini har safar yuboramiz.

2-mashq (o'rta): javobni tekshirish

Provayderdan kelgan Gemini javobini (j) o'qiydigan javobniOqi(j) funksiyasini yozing. U { matn, tugash, tokenlar: { kirish, chiqish } } qaytarsin; chiqishga fikrlash tokenlari ham qo'shilsin; thought: true belgili qismlar matnga kirmasin. Javob MAX_TOKENS bilan tugasa — Error("javob kesildi") tashlasin. Ishora: ?. va ?? (Ixtiyoriy zanjir), filter, map, join.

Yechim
js
function javobniOqi(j) {
  const n = j.candidates?.[0];
  const matn = (n?.content?.parts ?? [])
    .filter((p) => p.thought !== true)
    .map((p) => p.text ?? "")
    .join("");
  const tugash = n?.finishReason ?? "NOMALUM";
  if (tugash === "MAX_TOKENS") throw new Error("javob kesildi");
  const u = j.usageMetadata ?? {};
  return {
    matn,
    tugash,
    tokenlar: {
      kirish: u.promptTokenCount ?? 0,
      chiqish: (u.candidatesTokenCount ?? 0)
        + (u.thoughtsTokenCount ?? 0),
    },
  };
}

const yaxshi = {
  candidates: [{
    finishReason: "STOP",
    content: { parts: [
      { text: "o'ylayapman…", thought: true },
      { text: "1. Manbalarni to'plash" },
    ] },
  }],
  usageMetadata: {
    promptTokenCount: 77, candidatesTokenCount: 9,
    thoughtsTokenCount: 40,
  },
};
const kesilgan = {
  candidates: [{
    finishReason: "MAX_TOKENS", content: { parts: [] },
  }],
};
console.log(javobniOqi(yaxshi));
try {
  javobniOqi(kesilgan);
} catch (e) {
  console.log("xato:", e.message);
}

Konsolda:

text
{
  matn: "1. Manbalarni to'plash",
  tugash: 'STOP',
  tokenlar: { kirish: 77, chiqish: 49 }
}
xato: javob kesildi

Fikrlash qismi ("o'ylayapman…") matnga kirmadi, lekin uning 40 tokeni chiqishga qo'shildi — pul aynan shunday olinadi. Kanonning Gemini provayderi ham xuddi shunday qiladi. matn qo'shtirnoqda chiqdi — ichida apostrof bor.

3-mashq (qiyin): token byudjeti

Suhbat holatsiz: har yangi xabarda butun tarix qayta yuboriladi. suhbatXarajati(xabarlar, tizim) funksiyasini yozing. xabarlar — har biri { rol, token }, tizim — tizim ko'rsatmasi tokenlari. Har model xabari bitta API chaqiruvi natijasi: o'sha chaqiruvning kirishi — tizim + undan oldingi hamma xabarlar, chiqishi — shu xabar. Funksiya chaqiruvlar soni, jami kirish va jami chiqish tokenini qaytarsin. Ishora: for...of va yig'uvchi o'zgaruvchilar.

Yechim
js
function suhbatXarajati(xabarlar, tizim) {
  let oldingi = 0;
  let kirish = 0;
  let chiqish = 0;
  let chaqiruv = 0;
  for (const x of xabarlar) {
    if (x.rol === "model") {
      chaqiruv++;
      kirish += tizim + oldingi;
      chiqish += x.token;
    }
    oldingi += x.token;
  }
  return { chaqiruv, kirish, chiqish };
}

const suhbat = [
  { rol: "user", token: 30 }, { rol: "model", token: 100 },
  { rol: "user", token: 10 }, { rol: "model", token: 100 },
  { rol: "user", token: 10 }, { rol: "model", token: 100 },
];
console.log(suhbatXarajati(suhbat, 200));
console.log(suhbatXarajati(suhbat.slice(0, 2), 200));

Konsolda:

text
{ chaqiruv: 3, kirish: 1020, chiqish: 300 }
{ chaqiruv: 1, kirish: 230, chiqish: 100 }

Birinchi chaqiruv 230 token kirish oldi, ikkinchisi — 340, uchinchisi — 450 (200 + 30 + 100 + 10 + 100 + 10). Uch chaqiruvda kirish 1 020 token — chiqishdan 3.4 baravar ko'p. Suhbat uzaygani sari har yangi javob qimmatlashadi, chunki tarix tobora katta. Shuning uchun chat ilovalari eski xabarlarni qisqartiradi yoki xulosaga aylantiradi. vazifalar-api esa bitta vazifa uchun bitta chaqiruv qiladi — tarix yo'q, xarajat bashorat qilinadi.

9. Loyiha qadami: provayder shartnomasi va Gemini

Bu darsdan vazifalar-api da yangi papka paydo bo'ladi: src/ai/. Uch dars davomida unda vazifani qadamlarga bo'lish yo'li quriladi. Bu darsda — ikki fayl: provayder shartnomasi va Gemini provayderi.

9.1 src/ai/llm.ts — shartnoma

Ilova kodi Gemini'ni bilmaydi. U faqat shartnomani — interfeysni biladi. Provayder almashsa yoki testda soxtasi kerak bo'lsa, ilova kodi o'zgarmaydi. Bu 25-qismdagi ombor (repository) interfeysi bilan bir xil g'oya. Kanon fayli (qisqartirilgan: oqim va embedding turlari keyingi darslarda):

ts
export type Rol = "user" | "model";
export type LlmXabar = { rol: Rol; matn: string };

export type LlmSorov = {
  /** tizim ko'rsatmasi — foydalanuvchi matnidan ALOHIDA maydon */
  tizim: string;
  xabarlar: LlmXabar[];
  jsonSxema?: Record<string, unknown> | undefined;
  harorat?: number | undefined;
  maksToken?: number | undefined;
  signal?: AbortSignal | undefined;
  model?: string | undefined;
};

export type Tokenlar = { kirish: number; chiqish: number };
export type LlmJavob = {
  matn: string; model: string; tokenlar: Tokenlar;
  tugash: string; ms: number;
};

export interface LlmProvayder {
  readonly nom: string;
  readonly model: string;
  yarat(s: LlmSorov): Promise<LlmJavob>;
  oqim(s: LlmSorov): AsyncIterable<LlmBolak>;
  embedding(matnlar: string[], tur: EmbeddingTuri,
    signal?: AbortSignal): Promise<number[][]>;
}

rol — Gemini nomlari (user, model), lekin ular provayderdan mustaqil shartnoma qismi: OpenAI fayli model ni assistant ga o'giradi. jsonSxema — keyingi darslardan biri uchun. signal — foydalanuvchi uzilsa so'rovni bekor qilish uchun: undan oqim darsida foydalanamiz.

Xatolar — turlangan klass. Har xato turi keyin aniq HTTP javobiga aylanadi:

ts
export type LlmXatoTuri =
  | "limit" | "band" | "timeout" | "rad"
  | "notogri" | "provayder" | "ochiq";
export class LlmXatosi extends Error {
  constructor(
    readonly tur: LlmXatoTuri,
    xabar: string,
    readonly qaytaUrinishMs?: number,
    readonly status?: number,
  ) {
    super(xabar);
    this.name = "LlmXatosi";
  }
  /** qayta urinsa bo'ladimi: faqat vaqtinchalik xatolar */
  get vaqtinchalik(): boolean {
    return this.tur === "limit" || this.tur === "band"
      || this.tur === "timeout"
      || (this.tur === "provayder" && (this.status ?? 0) >= 500);
  }
}

export function httpXatosi(
  status: number, xabar: string, retryAfter: string | null,
): LlmXatosi {
  const kut = retryAfter === null ? undefined
    : /^\d+$/.test(retryAfter) ? Number(retryAfter) * 1000
    : Math.max(0, Date.parse(retryAfter) - Date.now()) || undefined;
  if (status === 429) {
    return new LlmXatosi("limit", xabar, kut, status);
  }
  if (status === 503 || status === 529) {
    return new LlmXatosi("band", xabar, kut, status);
  }
  return new LlmXatosi("provayder", xabar, undefined, status);
}

httpXatosi provayderning Retry-After sarlavhasini ikki shaklda o'qiydi: soniya (7) yoki HTTP sana. 529 — Anthropic'ning "haddan tashqari yuklangan" kodi. Noto'g'ri kalitning 400 i provayder turiga tushadi — vaqtinchalik emas, qayta urinilmaydi.

Timeout uchun ikki yordamchi:

ts
export function chegaraliSignal(
  ms: number, tashqi?: AbortSignal,
): AbortSignal {
  const t = AbortSignal.timeout(ms);
  return tashqi === undefined ? t : AbortSignal.any([t, tashqi]);
}

export function xatogaAylantir(
  x: unknown, tashqi?: AbortSignal,
): unknown {
  if (x instanceof LlmXatosi) return x;
  const vaqt = x instanceof Error && (x.name === "TimeoutError"
    || (x.name === "AbortError" && tashqi?.aborted !== true));
  if (vaqt) {
    return new LlmXatosi("timeout", "LLM javobi vaqtida kelmadi");
  }
  return x;
}

AbortSignal.any([t, tashqi]) — ikki signaldan biri ishlasa bekor bo'ladigan yangi signal: o'z timeout'imiz yoki tashqi sabab (AbortController darsini eslang). xatogaAylantir ularni ajratadi. Timeout — bizning xato turimiz. Foydalanuvchi o'zi uzgan bo'lsa (tashqi.aborted) — xato asl holida qoladi: u xato emas, bekor qilish.

9.2 src/ai/gemini.ts — provayder

Gemini provayderi shartnomani REST orqali bajaradi. yarat() va uning yordamchilari (telefon uchun bo'lindi, ma'nosi kanon bilan aynan):

ts
#tana(s: LlmSorov): Record<string, unknown> {
  const json = s.jsonSxema === undefined ? {} : {
    responseMimeType: "application/json",
    responseJsonSchema: s.jsonSxema,
  };
  return {
    systemInstruction: { parts: [{ text: s.tizim }] },
    contents: s.xabarlar.map((x) => ({
      role: x.rol, parts: [{ text: x.matn }],
    })),
    generationConfig: {
      temperature: s.harorat ?? 0.2,
      maxOutputTokens: s.maksToken ?? 1024,
      ...json,
    },
  };
}

async #post(
  yol: string, tana: unknown, signal: AbortSignal,
): Promise<Response> {
  const r = await fetch(`${this.#s.api}/${yol}`, {
    method: "POST", signal,
    headers: {
      "content-type": "application/json",
      "x-goog-api-key": this.#s.kalit,
    },
    body: JSON.stringify(tana),
  });
  if (!r.ok) {
    const j = (await r.json().catch(() => ({}))) as GeminiJavob;
    const xabar = `${j.error?.status ?? ""} `
      + (j.error?.message ?? "").slice(0, 200);
    throw httpXatosi(r.status,
      `Gemini ${String(r.status)}: ${xabar}`,
      r.headers.get("retry-after"));
  }
  return r;
}

#javob(j: GeminiJavob, model: string, ms: number): LlmJavob {
  const blok = j.promptFeedback?.blockReason;
  if (blok !== undefined) {
    throw new LlmXatosi("rad", `so'rov rad etildi: ${blok}`);
  }
  const n = j.candidates?.[0];
  const matn = (n?.content?.parts ?? [])
    .filter((p) => p.thought !== true)
    .map((p) => p.text ?? "").join("");
  const tugash = n?.finishReason ?? "NOMALUM";
  if (matn === "" && tugash !== "STOP") {
    throw new LlmXatosi("rad", `bo'sh javob (${tugash})`);
  }
  const u = j.usageMetadata ?? {};
  // fikrlash (thinking) tokenlari ham chiqish narxida hisoblanadi
  const chiqish = (u.candidatesTokenCount ?? 0)
    + (u.thoughtsTokenCount ?? 0);
  return {
    matn, model: j.modelVersion ?? model,
    tokenlar: { kirish: u.promptTokenCount ?? 0, chiqish },
    tugash, ms,
  };
}

async yarat(s: LlmSorov): Promise<LlmJavob> {
  const model = s.model ?? this.model;
  const signal = chegaraliSignal(this.#s.timeoutMs, s.signal);
  const t0 = performance.now();
  try {
    const r = await this.#post(`models/${model}:generateContent`,
      this.#tana(s), signal);
    const j = (await r.json()) as GeminiJavob;
    return this.#javob(j, model, Math.round(performance.now() - t0));
  } catch (x) {
    throw xatogaAylantir(x, s.signal);
  }
}

Bu kodda darsning hamma saboqlari bor. Kalit — sarlavhada, timeout — har so'rovda, xato matni 200 belgigacha kesiladi. Xavfsizlik filtri (blockReason) va bo'sh javob — rad turi. Fikrlash tokenlari chiqishga qo'shiladi. # bilan boshlangan metodlar — klassning yopiq a'zolari (# maydonlar): ularni tashqaridan chaqirib bo'lmaydi.

Bitta joyga e'tibor bering: matn === "" && tugash !== "STOP" — faqat bo'sh javob xato. Kesilgan, lekin bo'sh bo'lmagan javob (MAX_TOKENS) bu yerda o'tib ketadi. Kanon uni keyingi qatlamda — JSON sxema tekshiruvida ushlaydi. Faylda oqim() va embedding() metodlari ham bor — ular keyingi darslarda.

9.3 Sozlamalar

src/config.ts ga AI bo'limi qo'shiladi (Zod bilan, 25-qism naqshi). .env.example dagi namunasi:

ini
# AI_PROVAYDER: yoq (sukut — AI yo'llari 404)
#   | gemini | openai | anthropic
# AI_PROVAYDER=gemini
# GEMINI_API_KEY=
# AI_MODEL=gemini-3.5-flash-lite
# AI_TIMEOUT_MS=20000

AI_PROVAYDER sukut bo'yicha yoq — kalitsiz o'rnatilgan loyiha AI'siz, xatosiz ishlaydi. gemini tanlanib kalit berilmasa — ilova ishga tushmaydi: config'dagi refine "AI_PROVAYDER=gemini uchun GEMINI_API_KEY kerak" deydi. Xato ishga tushishda chiqishi — birinchi foydalanuvchi so'rovida chiqishidan yaxshi. OpenAI va Anthropic uchun AI_MODEL majburiy: kanon ular uchun taxminiy model nomi yozmadi, chunki ularni sinamagan.

Kalitni o'zingiz olsangiz (Google AI Studio'da) — u faqat .env da turadi. Kalitsiz ham dars to'liq o'tiladi: keyingi darslardagi testlar soxta provayder bilan yuradi va tarmoqqa chiqmaydi.

Hali commit qilmang — provayder qatlami Strukturali javob va tool calling darsida tayyor bo'ladigan yo'l bilan bitta commit'da ketadi.

10. Real ishda

  • Qayerda uchraydi: matnni qisqartirish, tasniflash, qadamlarga bo'lish, qidiruv, chat yordamchilar. Ko'p mahsulotda AI — bitta tugma, orqasida esa shu darsdagi so'rov.
  • Jamoada: model tanlash — o'lchov bilan: sifat, kechikish, narx, til. O'zbekcha mahsulotda o'z matnlaringiz bilan token va sifatni o'lchang.
  • Intervyu savollari: "System prompt va user prompt farqi?", "Temperature nima qiladi, 0 deterministikmi?", "Token nima va narx qanday hisoblanadi?", "API kalitni qanday himoyalaysiz?", "LLM so'roviga nega timeout kerak?"
  • Vositalar: rasmiy SDK'lar, provayderlarni birlashtiruvchi qatlamlar (Vercel AI SDK, OpenRouter), lokal modellar (Ollama). Kanon faqat Gemini'ni sinadi, qolganlari — hujjat bo'yicha.

Xulosa

  • LLM API — oddiy POST: tizim ko'rsatmasi, xabarlar, sozlamalar; javobda matn, finishReason va tokenlar.
  • API holatsiz: suhbat tarixini har so'rovda o'zingiz yuborasiz va har safar to'laysiz.
  • finishReason ni tekshiring: HTTP 200 + MAX_TOKENS — kesilgan javob.
  • Narx tokenda; o'zbekcha gap kanon o'lchovida ~1.7 × ko'p token oldi; fikrlash tokenlari ham pul.
  • temperature: 0 deterministik emas — testda soxta provayder, prodakshnda javobni tekshirish.
  • Kalit — faqat serverda, faqat sarlavhada, faqat muhitdan; har so'rovda timeout.

Keyingi dars: LLM javoblarini streaming qilish — javobni to'liq kutmasdan, qadamma-qadam foydalanuvchiga uzatamiz va foydalanuvchi sahifani yopsa, modelga so'rovni ham to'xtatamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
LLM API asoslari: backend'dan modelga so'rov, xabarlar, tokenlar va narx — IlmHamroh