---
title: 'حساب الـ Tokens في TypeScript: ملاءمة الـ Context'
description: احسب توكنز OpenAI في TypeScript باستخدام gpt-tokenizer - بدون WASM، وبدون مفتاح API. احسب رسائل الدردشة بدقة وقص السجل ليناسب نافذة سياق النموذج.
url: https://nerdleveltech.com/ar/count-tokens-typescript-context-window
language: ar
date: '2026-06-25'
category: llm-integration
tags:
  - gpt-tokenizer
  - typescript
  - tokens
  - context window
  - openai
  - llms
  - ai-ml
markdown_url: https://nerdleveltech.com/ar/count-tokens-typescript-context-window.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/count-tokens-typescript-context-window'
terms: https://nerdleveltech.com/llms.txt
---

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

لعد الكلمات (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 خالصة لنماذج OpenAI[^gpt-tokenizer]
- إلمام أساسي بهيكل رسائل دردشة OpenAI (`{ role, content }`)

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

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

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

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

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

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

```bash
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`:

```bash
npm pkg set type=module
```

```json
{
  "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`:

```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)
```

قم بتشغيله:

```bash
npx tsx src/count.ts
```

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

```text
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).[^GitHub] سنتعامل مع النماذج الأقدم في الخطوة 5.

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

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

```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
```

المخرجات:

```text
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`:

```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)
```

قم بتشغيله:

```bash
npx tsx src/count-chat.ts
```

المخرجات:

```text
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` كثيرًا، فاستورد نسخة مرتبطة بالنموذج مرة واحدة بدلاً من تكرار سلسلة اسم النموذج:

```ts
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_base`       | gpt-5, gpt-4o, gpt-4.1, o-series | الاستيراد الافتراضي (أو `gpt-tokenizer/model/gpt-4o`) |
| `cl100k_base`      | gpt-3.5-turbo, gpt-4             | `gpt-tokenizer/encoding/cl100k_base`                  |

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

```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
```

المخرجات:

```text
o200k: 10
cl100k: 14
```

نفس السلسلة النصية، 10 توكنات مقابل 14 - فرق بنسبة 40% ناتج فقط عن اختيار الترميز. إذا قمت بتثبيت `cl100k_base` بدافع العادة بينما تستخدم gpt-4o، فإن كل حسابات الميزانية ستكون مضخمة. طابق الترميز مع النموذج وستتطابق الأرقام. لمزيد من الاستراتيجيات العميقة حول ما يجب وضعه *داخل* نافذة السياق تلك، راجع مقالنا المصاحب حول [تحسين نافذة السياق لنماذج LLMs](https://nerdleveltech.com/posts/mastering-context-window-optimization-for-llms).

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

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

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

```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 توكن حتى يكون القص مرئياً؛ في بيئة الإنتاج ستقوم بتمرير نافذة النموذج الحقيقية:

```ts
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)
```

قم بتشغيله:

```bash
npx tsx src/demo.ts
```

المخرجات:

```text
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`:

```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))
```

قم بتشغيله:

```bash
npx tsx src/chunk.ts
```

المخرجات:

```text
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`:

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

المتوقع:

```text
fits budget? true
```

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

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

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

| المكتبة         | الإصدار | الآلية                        | ملاحظات حول الـ Edge / العمل بدون إنترنت                                                                               |
| --------------- | ------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `gpt-tokenizer` | 3.4.0   | JS نقي، رتب BPE مدمجة داخلياً | لا تحتاج WASM، لا جلب عبر الشبكة - تعمل على أي بيئة تشغيل[^gpt-tokenizer]                                              |
| `tiktoken`      | 1.0.22  | ربط WASM بمحلل Rust           | إنتاجية عالية للمدخلات الكبيرة، لكنها تحتاج لدعم WASM[^tiktoken]                                                       |
| `js-tiktoken`   | 1.0.21  | JS نقي                        | البناء الرئيسي يدمج الرتب؛ بناء `js-tiktoken/lite` يتوقع منك توفير بيانات الرتب، والتي تُجلب عادةً من CDN[^jstiktoken] |

بالنسبة لـ 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` دمجه بصمت من أجل السلامة. إذا كنت تثق في المدخلات وتريد تحليلها كتوكن خاص، فقم بالموافقة صراحةً:

```ts
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) المحلي رؤيتها.[^openai-count] الجأ إليها عندما تهمك الدقة أكثر من زمن الاستجابة (latency)، على سبيل المثال قبل استدعاء مكلف مباشرة أو لقياس الاستخدام.

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

للحصول على صورة أوسع للتكلفة بمجرد تمكنك من العد، راجع أدلتنا حول [توفير التوكنز وتحسين البرومبتات](https://nerdleveltech.com/posts/saving-tokens-and-optimizing-prompts-the-art-of-efficient-ai-conversations) و [تخزين البرومبت المؤقت (prompt caching) مع Claude في TypeScript](https://nerdleveltech.com/posts/claude-prompt-caching-TypeScript-tutorial)، وكلاهما يعتمد على مهارة إدارة الميزانية التي طورتها للتو.

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

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

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

[^gpt-tokenizer]: `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>

[^GitHub]: 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>

[^openai-count]: 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>

[^context]: 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>

[^tiktoken]: `tiktoken` on npm (1.0.22) - the WASM binding to OpenAI's Rust tokenizer. <https://www.npmjs.com/package/tiktoken>

[^jstiktoken]: `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>

[^anthropic]: 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>
