كل المقالات

ابنِ أول تطبيق بالـAPI — من مفتاح فارغ إلى خدمة صالحة للإنتاج

مساعد دعم فنّي كامل في نحو مئة سطر، مع بثّ حيّ ومعالجة أخطاء وتتبّع كلفة — والخطوات التي تفصل التجربة عن الإنتاج.

نُشر في4 دقائق قراءة

سنبني في هذه المقالة خدمة صغيرة حقيقية: نقطة تستقبل سؤال عميل وتعيد ردًّا مبثوثًا، مع معالجة أخطاء لائقة وتسجيل للكلفة. الشفرة بجافاسكربت (Node) بلا أي مكتبة خارجية، والمنطق نفسه ينتقل إلى أي لغة بلا تغيير يُذكر.

1. المفتاح أولًا

أنشئ مفتاحًا من لوحة حسابك، وضعه في متغيّر بيئة. لا تضعه في الشفرة، ولا في ملفٍّ يدخل المستودع.

bash
export VORIX_API_KEY="vx_live_..."
export VORIX_BASE_URL="https://<host>/v1"

تحقّق من الإعداد بنداءٍ واحد قبل أن تكتب سطرًا:

bash
curl "$VORIX_BASE_URL/me" -H "Authorization: Bearer $VORIX_API_KEY"

إن عاد لك رصيدك وحدّك فأنت جاهز. وإن عاد 401 فالمشكلة في الترويسة أو في المفتاح لا في شفرتك — وقد وفّرت على نفسك ساعة تصحيح.

2. أصغر نداء يعمل

js
const BASE = process.env.VORIX_BASE_URL
const KEY = process.env.VORIX_API_KEY

async function ask(question) {
  const response = await fetch(`${BASE}/chat/completions`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "vorix-pro",
      messages: [
        { role: "system", content: "You are a support assistant. Answer in at most four sentences." },
        { role: "user", content: question },
      ],
      max_tokens: 400,
      temperature: 0.3,
    }),
  })

  if (!response.ok) {
    const { error } = await response.json()
    throw new Error(`${error.code}: ${error.message}`)
  }

  return response.json()
}

لاحظ ثلاثة قرارات صغيرة كبيرة الأثر: temperature منخفضة لأن ردّ الدعم يجب أن يكون ثابتًا لا مبدعًا، وmax_tokens مضبوطة على ما تعرضه واجهتك فعلًا، وتعليمات النظام تحدّد الطول صراحةً.

3. البثّ ليشعر المستخدم بالحياة

الفرق بين ردّ يصل بعد ثماني ثوانٍ صامتة وردّ يبدأ الظهور بعد نصف ثانية فرقٌ في التجربة لا في السرعة. أضف stream: true واقرأ المجرى:

js
async function askStreaming(question, onText) {
  const response = await fetch(`${BASE}/chat/completions`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "vorix-pro",
      stream: true,
      messages: [{ role: "user", content: question }],
    }),
  })

  const reader = response.body.getReader()
  const decoder = new TextDecoder()
  let buffer = ""
  let usage = null

  for (;;) {
    const { done, value } = await reader.read()
    if (done) break

    buffer += decoder.decode(value, { stream: true })

    const events = buffer.split("\n\n")
    buffer = events.pop() ?? ""

    for (const event of events) {
      const line = event.trim()
      if (!line.startsWith("data:")) continue

      const payload = line.slice(5).trim()
      if (payload === "[DONE]") return usage

      const chunk = JSON.parse(payload)
      if (chunk.error) throw new Error(chunk.error.message)
      if (chunk.usage) usage = chunk.usage

      const delta = chunk.choices?.[0]?.delta?.content
      if (delta) onText(delta)
    }
  }

  return usage
}

التفصيلة التي تُنسى عادةً: الحزمة الشبكية قد تصل مقطوعة في منتصف حدث. لذلك نحتفظ بآخر جزء في buffer بدل تفسير نصف حدث ثم الانهيار على JSON.parse.

والحدث قبل الأخير يحمل usage كاملًا؛ لذلك نحتفظ به ونعيده بعد وصول [DONE].

4. معالجة الأخطاء التي ستقع فعلًا

ثلاث حالات ستقابلها في الإنتاج، ولكلٍّ تصرّف مختلف:

  • 402 رصيد غير كافٍ. لا تُعد المحاولة أبدًا. أظهر للمستخدم رسالة واضحة وافتح صفحة الشحن. تكرار النداء لن يخلق رصيدًا.
  • 429 تجاوز الحدّ. نافذة الحدّ دقيقة واحدة. انتظر بتباعد أسّي مع عشوائية بسيطة (ثانية، ثم ثانيتان، ثم أربع). بلا العشوائية يعيد كل عملائك المحاولة في اللحظة نفسها فتصنع موجة ثانية.
  • 5xx. أعد المحاولة مرّتين أو ثلاثًا ثم استسلم برسالة مفهومة. سجّل حقل id من الرد: به نجد نداءك بالضبط إن راسلتنا.

أمّا 401 و404 و422 فلا تُعاد المحاولة معها أبدًا: مفتاح خاطئ، أو اسم نموذج غير معروف، أو حقل ناقص — كلها أخطاء برمجية تُصلَح في الشفرة لا بالتكرار.

5. سجّل الكلفة من اليوم الأول

كل رد يحمل usage. اكتب سطرًا واحدًا في سجلّك مع كل نداء:

js
console.log({
  requestId: data.id,
  model: data.served_by,
  tokens: data.usage.total_tokens,
  costMicros: data.usage.cost_micros,
  feature: "support_reply",
})

cost_micros عدد صحيح — اجمعه واخزنه بلا خوف من كسور العائمات. وحقل feature من عندك: بعد شهر سيخبرك أي ميزة في منتجك أكلت الفاتورة، وهو أهم رقم ستحتاجه.

6. قبل النشر

  • المفتاح في مدير أسرار لا في المستودع، ومفتاح مستقلّ لكل بيئة.
  • مهلة صريحة لكل نداء، وإلا علّق طلبٌ واحدٌ خيطًا إلى الأبد.
  • سقف داخلي لعدد النداءات لكل مستخدم في الدقيقة — حدّنا يحميك من الإفراط، وسقفك يحميك من مستخدم واحد يستهلك حصّة الجميع.
  • ألّا يصل المفتاح إلى المتصفّح إطلاقًا: النداء يمرّ من خادمك دائمًا.

ثم ماذا

راجع مرجع نقطة التوليد لكل حقول الطلب والرد، وصفحة الحدود لتعرف سقوفك، وحاسبة الكلفة لتقدّر فاتورتك قبل أن تطلق.

مئة سطر تفصلك عن ميزة حقيقية في منتجك. ابدأ بأصغرها، وقِس، ثم وسّع.

كل المقالات