llm-integration

حساب الـ Tokens في TypeScript: ملاءمة الـ Context

٢٥ يونيو ٢٠٢٦

Count Tokens in TypeScript: Fit the Context Window (2026)

لعد الكلمات (tokens) الخاصة بـ OpenAI في TypeScript دون استدعاء الـ API، قم بتثبيت gpt-tokenizer واستدعاء countTokens(text) للنصوص أو encodeChat(messages, model).length لمصفوفة الدردشة. إنها مكتبة JavaScript خالصة - لا تعتمد على WASM، ولا تحتاج لمفتاح API - وهي تدعم وظيفة trimToFit() التي تضبط المحادثة لتناسب نافذة سياق النموذج.

ملخص

ستقوم ببناء وحدة TypeScript صغيرة تعد الكلمات محليًا باستخدام gpt-tokenizer 3.4.0، وتقيس العبء الإضافي لرسائل الدردشة بشكل صحيح (ليس التخمين القديم "~4 كلمات لكل رسالة")، وتقص المحادثة المتزايدة لتناسب نافذة سياق النموذج مع حجز مساحة للرد. التقنيات المستخدمة هي Node 22 LTS + TypeScript 6 + tsx، وكل مقتطف كود جاهز للنسخ والتشغيل، ويعمل المشروع بالكامل دون اتصال بالإنترنت. يستغرق الأمر حوالي 15 دقيقة.

ما ستتعلمه

  • لماذا تعد الكلمات محليًا بدلاً من استدعاء الـ API - ومتى لا يكون العد المحلي كافيًا
  • كيفية عد الكلمات في نص باستخدام encode، و decode، و countTokens
  • كيفية التحقق من الميزانية بتكلفة منخفضة باستخدام isWithinTokenLimit (وقيمتها المرجعة number | false)
  • كيفية عد مصفوفة رسائل الدردشة بدقة باستخدام encodeChat، بما في ذلك العبء الإضافي لكل رسالة
  • كيفية اختيار الترميز الصحيح (o200k_base مقابل cl100k_base) لنموذجك
  • كيفية كتابة وظيفة trimToFit() تحافظ على موجه النظام (system prompt) وتحجز كلمات للمخرجات
  • كيفية تقسيم مستند طويل إلى أجزاء محدودة الكلمات لعمليات الـ embedding أو التلخيص
  • كيف تقارن gpt-tokenizer مع tiktoken و js-tiktoken في بيئات الـ edge

المتطلبات الأساسية

  • Node 20.19+ أو 22+ (تم التحقق من هذا الدليل على Node 22 LTS)
  • TypeScript 6.0.3 و tsx 4.22.4 لتشغيل ملفات .ts مباشرة
  • gpt-tokenizer 3.4.0 - محلل BPE بلغة JavaScript خالصة لنماذج OpenAI1
  • إلمام أساسي بهيكل رسائل دردشة OpenAI ({ role, content })

الكلمة (token) هي الوحدة التي يقرأها النموذج فعليًا: وهي تقريبًا جزء من كلمة. تستخدم نماذج GPT من OpenAI محلل Byte-Pair Encoding (BPE)، وتعد gpt-tokenizer نسخة منقولة بأمانة بلغة JS خالصة من tiktoken الخاصة بـ OpenAI والتي تشحن جداول رتب BPE مدمجة، لذا فهي تعمل في Node، والمتصفح، وبيئات الـ edge دون الحاجة لملف WASM ثنائي أو تحميل خارجي.2

لماذا تعد الكلمات محليًا (ومتى لا تفعل ذلك)

العد المحلي فوري، ومجاني، وخاص - فأنت لا ترسل موجهك إلى طرف ثالث لمجرد معرفة حجمه. هذا ما تحتاجه لقص السجل، أو تقسيم المستندات، أو تحديد حجم دفعة، أو عرض عداد كلمات مباشر في واجهة المستخدم.

لكن كن صريحًا بشأن الحدود: محلل BPE المحلي يعد النصوص فقط. فهو لا يرى الكلمات المضافة بواسطة مخططات الأدوات/الوظائف (tool/function schemas)، أو الصور، أو الملفات، ويقوم المزودون أحيانًا بتعديل كيفية صياغة الطلبات. تقول وثائق OpenAI نفسها إن التقديرات القائمة على الأحرف غير دقيقة وأن الأدوات والصور تحتاج إلى عد من جانب الخادم.3 لذا استخدم العد المحلي للحصول على أحجام تقريبية سريعة للنصوص، واعتمد على نقطة نهاية عد الكلمات الرسمية للمزود عندما تحتاج إلى رقم دقيق وقابل للمحاسبة يتضمن مدخلات غير نصية. سنقوم بإعداد كليهما.

الخطوة 1 - إعداد المشروع

أنشئ مشروعًا وقم بتثبيت بيئة التشغيل والتبعيات بإصدارات محددة:

mkdir token-budget && cd token-budget
npm init -y
npm install gpt-tokenizer@3.4.0
npm install -D TypeScript@6.0.3 tsx@4.22.4 @types/node@22.20.0

حدد الحزمة كـ ES module وأضف ملف tsconfig.json صارم. نظرًا لأننا نشغل ملفات .ts باستخدام tsx (بدون خطوة بناء)، فإننا نفعل allowImportingTsExtensions و noEmit:

npm pkg set type=module
{
  "compilerOptions": {
    "target": "ES2023",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src"]
}

هذا هو الإعداد بالكامل. لا تحميل للنموذج، لا مفتاح API، ولا ملف .env.

الخطوة 2 - عد الكلمات في نص

هذا هو جوهر عد كلمات OpenAI: تحويل النص إلى معرفات كلمات (token IDs) والعكس. أنشئ src/count.ts:

import { encode, decode, countTokens, isWithinTokenLimit } from 'gpt-tokenizer'

const text = 'The quick brown fox jumps over the lazy dog.'

const tokens: number[] = encode(text)
console.log('token ids:', tokens)
console.log('token count:', countTokens(text)) // same as encode(text).length
console.log('round-trips:', decode(tokens) === text)

قم بتشغيله:

npx tsx src/count.ts

المخرجات المتوقعة:

token ids: [
    976,  4853, 19705,
  68347, 65613,  1072,
    290, 29082,  6446,
     13
]
token count: 10
round-trips: true

ثلاثة أشياء تستحق الملاحظة. أولاً، countTokens(text) هو الرقم الذي تريده للميزانية - فهو يعيد نفس قيمة encode(text).length ولكنه يعد بشكل أصلي دون بناء مصفوفة الكلمات. ثانياً، decode(encode(text)) يعود للنص الأصلي تماماً، وهو ما يثبت أن المحلل لا يفقد البيانات. ثالثاً، الاستيراد الافتراضي يستخدم ترميز o200k_base، وهو المستخدم في كل نماذج OpenAI الحديثة (gpt-5، gpt-4o، gpt-4.1، وسلسلة o).2 سنتعامل مع النماذج الأقدم في الخطوة 5.

الخطوة 3 - التحقق من الحد بتكلفة منخفضة باستخدام isWithinTokenLimit

غالبًا لا تحتاج إلى العد الدقيق - بل تحتاج فقط لمعرفة "هل هذا يناسب تحت N كلمة؟". ترميز نص ضخم فقط لمقارنة طوله هو إهدار للموارد. تتوقف وظيفة isWithinTokenLimit مبكرًا بمجرد تجاوز الحد، ونوع القيمة المرجعة هو النقطة التي تستحق الاستيعاب: فهي تعيد عدد الكلمات (رقم) عندما يناسب النص الحد، أو false عندما لا يناسبه. أضف هذا إلى src/count.ts:

// isWithinTokenLimit returns the token COUNT if within the limit, or false if over
const within: number | false = isWithinTokenLimit(text, 20)
console.log('within 20?', within) // 10

const over: number | false = isWithinTokenLimit(text, 5)
console.log('within 5?', over) // false

المخرجات:

within 20? 10
within 5? false

لأن false و 0 كلاهما قيم خاطئة (falsy)، تحقق دائمًا من النوع صراحة. اكتب if (within === false) بدلاً من if (!within) - وإلا فسيتم قراءة النص الفارغ (الذي يحتوي شرعاً على 0 كلمة) بشكل خاطئ على أنه "تجاوز الحد". تحت وضع strict في TypeScript، يجبرك اتحاد number | false على معالجة الحالتين، وهو بالضبط الأمان الذي تريده حول فحص الميزانية.

الخطوة 4 - عد رسائل الدردشة بدقة باستخدام encodeChat

هنا يخطئ معظم كتاب المقالات التقنية حول عد الكلمات. إن API إكمال الدردشة في OpenAI لا ترسل للنموذج نصوص content الخام الخاصة بك - بل تغلف كل رسالة في كلمات حدودية خاصة تحدد الدور وبداية/نهاية كل رسالة. إذا قمت بعد content فقط، فستحصل على عد أقل من الحقيقي، والعد الأقل هو كيف تتجاوز نافذة السياق في بيئة الإنتاج.

تقوم encodeChat بالعمل الحقيقي. أنشئ src/count-chat.ts:

import { encodeChat, countTokens } from 'gpt-tokenizer'

type ChatMessage = { role: 'system' | 'user' | 'assistant'; content: string }

const messages: ChatMessage[] = [
  { role: 'system', content: 'You are a helpful assistant.' },
  { role: 'user': 'What is the capital of France?' }]

// encodeChat needs the model so it can apply that model's chat framing
const chatTokens = encodeChat(messages, 'gpt-4o')
console.log('chat token count:'.length)

// Naive raw-text counting UNDERCOUNTS because it ignores per-message framing:
const naive = messages.reduce((sum) => sum + countTokens(m)0)
consolelog('naive content-only count:')
consolelog('framing overhead:'length - naive)

قم بتشغيله:

npx tsx src/count-chat.ts

المخرجات:

chat token count: 24
naive content-only count: 13
framing overhead: 11

الرسالتان هما 24 كلمة، وليس 13. الفرق المكون من 11 كلمة هو صياغة ChatML - حيث تصدر encodeChat كلمات الحدود <|im_start|> / <|im_sep|> / <|im_end|> التي تغلف دور كل رسالة ومحتواها في تنسيق دردشة OpenAI، وهو ما يفتقده العد الساذج القائم على الأحرف أو المحتوى تمامًا. مرر اسم النموذج حتى تطبق encodeChat صياغة ذلك النموذج؛ حتى الدردشة الفارغة تكلف بضع كلمات خاصة لأن المحادثة نفسها لها بداية ونهاية.

إذا كنت تستدعي encodeChat كثيرًا، فاستورد نسخة مرتبطة بالنموذج مرة واحدة بدلاً من تكرار سلسلة اسم النموذج:

import { encodeChat} from 'gpt-tokenizer/model/gpt-4o'

// encodeChat(messages) now uses gpt-4o framing without a second argument

يربط استيراد المسار الفرعي هذا الترميز في وقت الاستيراد، لذا فإن كل من encodeChat(messages) و countTokens(text) يستخدمان إعدادات o200k_base الخاصة بـ gpt-4o.

الخطوة 5 - اختيار الترميز الصحيح لنموذجك

يكون المحلل صحيحًا فقط إذا استخدم الترميز الذي يستخدمه نموذجك بالفعل. ترتبط نماذج OpenAI بنوعين من الترميز ستواجههما في الممارسة العملية:

الترميز (Encoding)النماذج (Models)استيراد gpt-tokenizer
o200k_basegpt-5, gpt-4o, gpt-4.1, o-seriesالاستيراد الافتراضي (أو gpt-tokenizer/model/gpt-4o)
cl100k_basegpt-3.5-turbo, gpt-4gpt-tokenizer/encoding/cl100k_base

الاستيراد الافتراضي هو o200k_base، لذا إذا كنت تستخدم نموذجاً حديثاً فأنت بالفعل على المسار الصحيح. استخدام الترميز الخاطئ لا يسبب توقف البرنامج - بل يعيد بصمت رقماً مختلفاً، وهو النوع الخطير من الأخطاء. الفجوة تكون أكبر في النصوص غير الإنجليزية، حيث يكون o200k_base أكثر كفاءة بشكل ملحوظ. أنشئ src/encoding.ts:

import { countTokens as o200k } from 'gpt-tokenizer/encoding/o200k_base'
import { countTokens as cl100k } from 'gpt-tokenizer/encoding/cl100k_base'

const text = '日本語のトークン数を数える'
console.log('o200k:', o200k(text)) // 10
console.log('cl100k:', cl100k(text)) // 14

المخرجات:

o200k: 10
cl100k: 14

نفس السلسلة النصية، 10 توكنات مقابل 14 - فرق بنسبة 40% ناتج فقط عن اختيار الترميز. إذا قمت بتثبيت cl100k_base بدافع العادة بينما تستخدم gpt-4o، فإن كل حسابات الميزانية ستكون مضخمة. طابق الترميز مع النموذج وستتطابق الأرقام. لمزيد من الاستراتيجيات العميقة حول ما يجب وضعه داخل نافذة السياق تلك، راجع مقالنا المصاحب حول تحسين نافذة السياق لنماذج LLMs.

الخطوة 6 - قص سجل الدردشة ليتناسب مع نافذة السياق

الآن وقت الاستفادة الحقيقية. يجمع تطبيق الدردشة السجل حتى يتجاوز نافذة سياق النموذج. الحل هو قص أقدم الأدوار - مع الاحتفاظ دائماً بموجه النظام (system prompt) الذي يوجه النموذج - وحجز توكنات للرد، لأن نافذة السياق مشتركة بين المدخلات و المخرجات. (على سبيل المثال، يحتوي gpt-4o على نافذة سياق تبلغ 128,000 توكن ولكن يحدد المخرجات بـ 16,384 توكن، لذا يجب عليك ترك مساحة.4)

أنشئ src/trim-to-fit.ts:

import { encodeChat } from 'gpt-tokenizer'

export type ChatMessage = { role: 'system' | 'user' | 'assistant'; content: string }
export type GptModel = 'gpt-4o' | 'gpt-4o-mini' | 'gpt-4.1' | 'gpt-5'

export interface TrimOptions {
  model: GptModel
  /** Total context window of the model, in tokens. */
  maxContextTokens: number
  /** Tokens to leave free for the model's reply. */
  reserveForOutput: number
}

export function countChatTokens(messages: ChatMessage[], model: GptModel): number {
  return encodeChat(messages, model).length
}

/**
 * Drop the oldest non-system messages until the chat fits inside
 * maxContextTokens - reserveForOutput. The system message is always kept.
 */
export function trimToFit(messages: ChatMessage[], opts: TrimOptions): ChatMessage[] {
  const budget = opts.maxContextTokens - opts.reserveForOutput
  if (budget <= 0) throw new RangeError('reserveForOutput must be smaller than the context window')

  const system = messages.filter((m) => m.role === 'system')
  const turns = messages.filter((m) => m.role !== 'system')

  // Keep removing the oldest turn until we fit (or only the system prompt remains).
  const kept = [...turns]
  while (kept.length > 0 && countChatTokens([...system, ...kept], opts.model) > budget) {
    kept.shift()
  }
  return [...system, ...kept]
}

الآن قم بتجربته مع محادثة اصطناعية في src/demo.ts. نستخدم نافذة صغيرة متعمدة بحجم 400 توكن حتى يكون القص مرئياً؛ في بيئة الإنتاج ستقوم بتمرير نافذة النموذج الحقيقية:

import { trimToFit, countChatTokens, type ChatMessage } from './trim-to-fit.ts'

const history: ChatMessage[] = [
  { role: 'system', content: 'You are a terse assistant.' },
]
for (let i = 1; i <= 40; i++) {
  history.push({ role: 'user', content: `Question number ${i: tell me a fact about the number ${i.` })
  history.push({ role: 'assistant': `Fact ${i: ${i is a number with interesting properties worth a sentence or two of explanation here.` })
}

const model = 'gpt-4o' as const
console.log('full history tokens:', countChatTokens(history, model), 'messages:', history.length)

const trimmed = trimToFit(history, { model, maxContextTokens: 400, reserveForOutput: 120 })
console.log('trimmed tokens:', countChatTokens(trimmed, model), 'messages:', trimmed.length)
console.log('budget was:', 400 - 120)
console.log('system kept?', trimmed[0]?.role === 'system')
console.log('fits budget?', countChatTokens(trimmed, model) <= 400 - 120)

قم بتشغيله:

npx tsx src/demo.ts

المخرجات:

full history tokens: 1773 messages: 81
trimmed tokens: 277 messages: 13
budget was: 280
system kept? true
fits budget? true

تم قص السجل المكون من 81 رسالة و1,773 توكن إلى أحدث 13 رسالة (277 توكن)، وهو ما يناسب ميزانية الـ 280 توكن (400 ناقص 120 محجوزة للمخرجات)، وبقي موجه النظام في الفهرس 0. استبدل maxContextTokens: 400 بـ 128_000 وستقوم نفس الوظيفة بإدارة محادثة gpt-4o حقيقية.

تحسينان لبيئة الإنتاج بمجرد وضوح الهيكل. أولاً، قد يؤدي حذف shift() واحدة إلى ترك رد مساعد معلق بدون سؤال مطابق؛ إذا كان تماسك الرسائل مهماً، فقم بإزالة الأدوار في أزواج (مستخدم + مساعد). ثانياً، إذا كنت تفضل تلخيص الأدوار القديمة بدلاً من حذفها، فاستبدل الرسائل المحذوفة برسالة نظام واحدة "ملخص حتى الآن" - واحسب توكنات هذا الملخص باستخدام countTokens ليبقى هو أيضاً ضمن الميزانية.

الخطوة 7 - تقسيم مستند طويل حسب ميزانية التوكنات

القص يعالج المحادثات الطويلة جداً؛ أما التقسيم (chunking) فيعالج المستندات الطويلة جداً - وهي خطوة تحضيرية يومية قبل تضمين (embedding) النص للبحث أو تغذيته لملخص قطعة بقطعة. النمط هو نفس الوظيفة الأساسية (countTokens) المستخدمة بشكل جشع: قم بحزم الكلمات حتى تؤدي الكلمة التالية إلى تجاوز الحد، ثم ابدأ قطعة جديدة. أنشئ src/chunk.ts:

import { countTokens } from 'gpt-tokenizer'

/**
 * Greedily pack whitespace-separated words into chunks that each stay
 * at or under maxTokens. Useful for splitting a document before embedding
 * or summarizing it.
 */
export function chunkByTokens(text: string, maxTokens: number): string[] {
  const words = text.split(/\s+/).filter(Boolean)
  const chunks: string[] = []
  let current: string[] = []

  for (const word of words) {
    const candidate = [...current, word].join(' ')
    if (countTokens(candidate) > maxTokens && current.length > 0) {
      chunks.push(current.join(' '))
      current = [word]
    } else {
      current.push(word)
    }
  }
  if (current.length > 0) chunks.push(current.join(' '))
  return chunks
}

const doc = Array.from({ length: 120 }(_) => `sentence ${i + 1 has a few words in it.`).join(' ')
const chunks = chunkByTokens(doc64)
console.log('document tokens:'countTokens(doc))
console.log('chunks:'.length)
console.log('max chunk tokens:'.max(...chunks.map((c) => countTokens(c))))
console.log('all within 64?'.every((c) => countTokens(c) <= 64))

قم بتشغيله:

npx tsx src/chunk.ts

المخرجات:

document tokens: 1200
chunks: 19
max chunk tokens: 64
all within 64? true

تم تقسيم المستند المكون من 1,200 توكن إلى 19 قطعة، لا تتجاوز أي منها ميزانية الـ 64 توكن. ملاحظتان للمستندات الحقيقية: الحماية current.length > 0 تمنع حدوث حلقة لانهائية عندما تكون "كلمة" واحدة أطول من maxTokens (تحصل على قطعتها الخاصة الكبيرة بدلاً من تعطيل الحلقة) - إذا كان هذا خطراً حقيقياً في مدخلاتك، فقم بتقسيم هذه التوكنات بشكل أكبر. وللحصول على جودة استرجاع أفضل، عادة ما ترغب في تداخل صغير بين القطع؛ ابدأ كل current جديدة بآخر جملة من القطعة السابقة حتى لا ينقطع السياق عند الحدود.

التحقق

لقد رأيت بالفعل كل نص برمجي يطبع مخرجاته المتوقعة، ولكن إليك فحصاً نهائياً شاملاً. تحقق من أنواع المشروع بالكامل، ثم تأكد من أن عرض القص التجريبي يعطي fits budget? true:

npx tsc --noEmit && npx tsx src/demo.ts | tail -1

المتوقع:

fits budget? true

نتيجة نظيفة من tsc --noEmit (لا تطبع شيئاً وتخرج برمز 0) بالإضافة إلى fits budget? true تعني أن الأنواع سليمة وأن القص يحترم الميزانية التي حددتها بالفعل.

gpt-tokenizer مقابل tiktoken مقابل js-tiktoken

هناك ثلاثة خيارات لـ Tokenizer، ويعتمد الاختيار الصحيح على مكان تشغيل الكود:

المكتبةالإصدارالآليةملاحظات حول الـ Edge / العمل بدون إنترنت
gpt-tokenizer3.4.0JS نقي، رتب BPE مدمجة داخلياًلا تحتاج WASM، لا جلب عبر الشبكة - تعمل على أي بيئة تشغيل1
tiktoken1.0.22ربط WASM بمحلل Rustإنتاجية عالية للمدخلات الكبيرة، لكنها تحتاج لدعم WASM5
js-tiktoken1.0.21JS نقيالبناء الرئيسي يدمج الرتب؛ بناء js-tiktoken/lite يتوقع منك توفير بيانات الرتب، والتي تُجلب عادةً من CDN6

بالنسبة لـ Cloudflare Workers أو Vercel Edge أو أي مسار حساس لسرعة البدء (cold-start)، فإن gpt-tokenizer هو الخيار الافتراضي الأسهل: ليس لديه تبعية WASM ولا تحميل وقت التشغيل، لذا لا يوجد شيء لجلبه ولا شيء ليفشل. tiktoken (حزمة WASM) مبنية حول نواة WASM مجمعة تهدف إلى الإنتاجية العالية في المدخلات الضخمة جداً، على حساب الحاجة لدعم WASM. أما js-tiktoken فيقع في المنتصف - بناؤه الكامل يدمج الرتب أيضاً، لكن متغير lite الذي يتطلب توفير الرتب بنفسك هو الذي يسبب مشاكل في بيئات الـ edge عندما يكون جلب الـ CDN بطيئاً أو محظوراً.

الأخطاء الشائعة وإصلاحها

Error: Model 'gpt-4o-banana' does not support chat. - لقد قمت بتمرير اسم نموذج غير معروف لـ encodeChat. يجب أن يكون اسم النموذج مما يتعرف عليه gpt-tokenizer (مثل gpt-4o، gpt-4.1، gpt-5). قم بتصحيح الخطأ الإملائي أو استورد النسخة المرتبطة بالنموذج، gpt-tokenizer/model/gpt-4o، واستدعِ encodeChat(messages) بدون الوسيط الثاني.

Error: Disallowed special token found: <|endoftext|> - يحتوي نص الإدخال حرفياً على توكن خاص مثل <|endoftext|>، ويرفض encode دمجه بصمت من أجل السلامة. إذا كنت تثق في المدخلات وتريد تحليلها كتوكن خاص، فقم بالموافقة صراحةً:

import { encode } from 'gpt-tokenizer'

const ids = encode('hello <|endoftext|> world'{
  allowedSpecial: new Set(['<|endoftext|>'])})
console.log(ids.length) // 9

ERR_MODULE_NOT_FOUND: Cannot find package 'gpt-tokenizer' - لقد قمت بتشغيل نص برمجي من خارج دليل المشروع، لذا لم يتمكن Node من العثور على node_modules. قم بتشغيل النصوص البرمجية من جذر المشروع (حيث يوجد package.json و node_modules).

أخطاء tsc حول CommonJS تحت verbatimModuleSyntax - لقد نسيت "type": "module" في package.json، لذا يعامل TypeScript ملفات .ts كـ CommonJS ويرفض صيغة ESM import. قم بتشغيل npm pkg set type=module. إذا اشتكى tsc بعد ذلك من استيراد ./trim-to-fit.ts، فتأكد من ضبط allowImportingTsExtensions و noEmit كما هو موضح في الخطوة 1.

العد المحلي الخاص بي لا يتطابق مع التوكنز المحسوبة في فاتورة الـ API. - هذا متوقع، في حدود هامش صغير. يتجاهل العد المحلي مخططات الأدوات/الوظائف (tool/function schemas)، والصور، والملفات، كما يضيف المزودون إطارات عمل خاصة بهم. للحصول على رقم دقيق، استخدم نقطة نهاية العد الخاصة بالمزود (القسم التالي).

عندما لا يكون العد المحلي كافيًا

هناك حالتان تحتاجان إلى الشبكة. أولاً، الأعداد الدقيقة والقابلة للفوترة التي تتضمن مدخلات غير نصية. توفر OpenAI نقطة نهاية لعد توكنز المدخلات (input-token-count) تقبل نفس الحمولة (payload) مثل Responses API وتعيد قيمة input_tokens الدقيقة، والتي تغطي النصوص، والرسائل، والصور، والملفات، والأدوات - وهي الأشياء التي لا يستطيع محلل BPE (BPE tokenizer) المحلي رؤيتها.3 الجأ إليها عندما تهمك الدقة أكثر من زمن الاستجابة (latency)، على سبيل المثال قبل استدعاء مكلف مباشرة أو لقياس الاستخدام.

ثانياً، عائلات النماذج الأخرى. إن gpt-tokenizer و tiktoken مخصصان لـ OpenAI. تستخدم نماذج Claude من Anthropic محللاً (tokenizer) مختلفاً، لذا فإن عد برومبتات Claude باستخدام محلل GPT هو ببساطة خطأ؛ استخدم نقطة نهاية Anthropic المسماة POST /v1/messages/count_tokens (أو client.messages.countTokens()) بدلاً من ذلك.7 كما أن لدى Gemini من Google طريقته الخاصة countTokens أيضاً. القاعدة العامة: المحللات (tokenizers) تختلف حسب المورد، وأحياناً حسب الجيل، لذا طابق العداد مع النموذج الذي تستدعيه بالفعل.

للحصول على صورة أوسع للتكلفة بمجرد تمكنك من العد، راجع أدلتنا حول توفير التوكنز وتحسين البرومبتات و تخزين البرومبت المؤقت (prompt caching) مع Claude في TypeScript، وكلاهما يعتمد على مهارة إدارة الميزانية التي طورتها للتو.

الخطوات التالية

  • أضف بديلاً للتلخيص (summarization fallback) إلى trimToFit: عندما تضطر لحذف دور في المحادثة، قم بدمجه في رسالة نظام تلخيصية مستمرة بدلاً من حذفه نهائياً.
  • اربط العد المحلي بحارس ما قبل التنفيذ (pre-flight guard) يرفض الطلبات التي تتجاوز الميزانية قبل أن تصل إلى الـ API.
  • للقياس الدقيق، استدعِ نقطة نهاية العد الخاصة بالمزود ووفق بينها وبين تقديرك المحلي لمعرفة النفقات الإضافية (overhead) المعتادة لديك.

لديك الآن طريقة سريعة، وتعمل بدون اتصال بالإنترنت، وآمنة من حيث النوع (type-safe) لعد توكنز OpenAI في TypeScript وإبقاء أي محادثة داخل نافذة السياق الخاصة بها - بدون ملف WASM ثنائي، أو جلب من CDN، أو مفتاح API.

Footnotes

  1. gpt-tokenizer on npm - 3.4.0 is the latest dist-tag as of 25 June 2026. Pure-JavaScript BPE tokenizer for OpenAI models. https://www.npmjs.com/package/gpt-tokenizer 2

  2. niieani/gpt-tokenizer (GitHub) - the default export uses the o200k_base encoding, used by gpt-5, gpt-4o, gpt-4.1, and the o-series. https://GitHub.com/niieani/gpt-tokenizer 2

  3. OpenAI - Counting tokens. The input-token-count endpoint accepts the same input as the Responses API and returns the exact input_tokens, including tools and images that local tokenizers can't measure. https://developers.openai.com/API/docs/guides/token-counting 2

  4. OpenAI gpt-4o has a 128,000-token context window and, since the 2024-11-20 update, a 16,384-token maximum output; input and output share the window. https://platform.openai.com/docs/models/gpt-4o

  5. tiktoken on npm (1.0.22) - the WASM binding to OpenAI's Rust tokenizer. https://www.npmjs.com/package/tiktoken

  6. js-tiktoken on npm (1.0.21) - pure-JS port; the js-tiktoken/lite build expects caller-supplied rank data. https://www.npmjs.com/package/js-tiktoken

  7. Anthropic - Token counting. Claude uses a different tokenizer from OpenAI; count via POST /v1/messages/count_tokens or client.messages.countTokens(). https://platform.claude.com/docs/en/build-with-claude/token-counting