درخواست کامیاب رہی۔ جواب درست JSON تھا۔ SDK خاموش رہا۔ اور پھر بھی ایپلی کیشن مکمل طور پر تباہ ہو گئی۔

یہ اس بات کی کہانی ہے کہ کیا ہوتا ہے جب آپ LLM فراہم کنندہ (provider) کی تبدیلی کو ایک ساختی جوا (structural gamble) سمجھنے کے بجائے محض ایک کنفیگریشن کی تبدیلی سمجھ لیتے ہیں۔ آپ ایک نیا base URL پیسٹ کرتے ہیں، API key تبدیل کرتے ہیں، اور request body کو بالکل ویسا ہی رکھتے ہیں کیونکہ دستاویزات ایک OpenAI-compatible endpoint کا وعدہ کرتی ہیں۔ ایک بنیادی "hello world" پرامپٹ کے لیے، یہ کام کرتا ہے۔ آپ خوشی مناتے ہیں۔ پھر اصل ٹریفک آتی ہے، اور دراڑیں نظر آنے لگتی ہیں۔

وائر کمپیٹیبلٹی (Wire Compatibility) کا دھوکہ

HTTP لیئر پر مطابقت (compatibility) سطحی ہوتی ہے۔ 200 status code اور JSON body کا مطلب ہے کہ سرور نے آپ کا پیغام قبول کر لیا ہے۔ اس کا مطلب یہ نہیں کہ سرور بالکل اسی طرح سوچتا ہے جیسے پچھلا سرور سوچتا تھا۔ OpenAI-compatible endpoints ایک جیسی request shape تو رکھتے ہیں، لیکن ان کا طرزِ عمل (behavioral contract) ایک جیسا نہیں ہوتا۔ دو فراہم کنندہ ایک جیسے payloads وصول کر سکتے ہیں لیکن ایسے جوابات دے سکتے ہیں جو باریک اور تباہ کن طریقوں سے ایک دوسرے سے مختلف ہوں۔

آپ کا کوڈ مفروضے قائم کرتا ہے۔ آپ فرض کر لیتے ہیں کہ message.content ایک string ہے کیونکہ پہلے ہمیشہ ایسا ہی ہوتا تھا۔ آپ فرض کرتے ہیں کہ tool call کے ساتھ صاف ستھرا، parseable JSON آئے گا۔ آپ فرض کرتے ہیں کہ finish_reason وہی اشارہ دیتا ہے جو آپ سمجھتے ہیں۔ یہ مفروضے تب تک نظر نہیں آتے جب تک کہ وہ مہلک (fatal) ثابت نہ ہو جائیں۔

اس کریش پر غور کریں جس نے یہ سب شروع کیا:

const text = response.choices[0].message.content.trim();

یہ لائن معصوم لگتی ہے۔ یہ ہفتوں تک کام کرتی رہی۔ پھر نئے فراہم کنندہ نے ایک tool call واپس کیا۔ اس لمحے میں، message.content ایک خالی string نہیں تھا۔ بلکہ وہ null تھا۔ اصل payload message.tool_calls کے اندر تھا، لیکن پارسر پہلے ہی آگے بڑھ چکا تھا اور وہ کسی خالی چیز پر .trim() کال کر رہا تھا۔ API نے کوئی error نہیں دی، نیٹ ورک لیئر نے کوئی شکایت نہیں کی، بلکہ آپ کے اپنے پارسر نے درخواست کو ختم کر دیا۔

جہاں فراہم کنندہ خاموشی سے الگ ہو جاتے ہیں

یہ فرق changelogs میں ظاہر نہیں ہوتے۔ یہ response object کے حاشیوں میں بیٹھے ہوتے ہیں اور edge cases کا انتظار کرتے ہیں۔

Tool-call formatting. ایک فراہم کنندہ tool arguments کو ایک پہلے سے ویلیڈیٹ شدہ JSON object کے طور پر بھیجتا ہے۔ دوسرا انہیں ایک فیلڈ کے اندر escaped string کے طور پر بھیجتا ہے۔ تیسرا شاید ایک طویل tool call کو متعدد streaming deltas میں تقسیم کر دے، جس سے آپ کو یہ دیکھنے سے پہلے کہ کیا ڈھانچہ درست ہے، چنکس (chunks) کو buffer کرنا پڑے گا۔ اگر آپ کی ایپلی کیشن ایک ہی parseable blob کی توقع رکھتی ہے، تو وہ رک جائے گی۔

Finish reasons. OpenAI مخصوص strings استعمال کرتا ہے جیسے "stop", "length", "tool_calls", اور "content_filter"۔ ایک compatible provider "end_turn"واپس کر سکتا ہے یا جب ماڈل ٹوکن کی حد (token ceiling) تک پہنچ جائے تو اس فیلڈ کو مکمل طور پر چھوڑ سکتا ہے۔ اگر آپ کا retry یا fallback logic کٹوتی (truncation) کا پتہ لگانے کے لیے"length"` کا انتظار کرتا ہے، تو وہ بیکار بیٹھا رہے گا جبکہ صارف کو ایک ادھورا جواب نظر آئے گا۔

Usage fields. کچھ فراہم کنندہ لیٹنسی (latency) کو کم کرنے کے لیے streaming responses سے ٹوکن کی تعداد نکال دیتے ہیں۔ دوسرے usage کو صرف آخری chunk کے ساتھ جوڑتے ہیں، یا non-streaming calls میں اسے مکمل طور پر چھوڑ دیتے ہیں۔ اگر آپ صارفین سے فی ٹوکن چارج کرتے ہیں اور آپ کا اکاؤنٹنگ کوڈ ہر response object میں usage.total_tokens کی موجودگی کی توقع رکھتا ہے، تو آپ کا بلنگ پائپ لائن خاموشی سے زیرو (zeros) ریکارڈ کرے گا۔

Streaming behavior. Server-sent events کو معیاری ہونا چاہیے، پھر بھی فراہم کنندہ مختلف فریکوئنسی پر buffers کو flush کرتے ہیں۔ ایونٹ کی حدود (boundaries) مختلف ہوتی ہیں۔ ایک فراہم کنندہ [DONE] سگنل کے ساتھ stream کو ختم کرتا ہے۔ دوسرا بغیر کسی نشان (sentinel) کے کنکشن کو صاف ستھرا ختم کر دیتا ہے۔ اگر آپ کا کلائنٹ کسی مخصوص کلوزنگ مارکر کا انتظار کرتے ہوئے بلاک ہو جاتا ہے، تو وہ ہینگ ہو جائے گا۔

Errors and timeouts. ایک فراہم کنندہ سے ریٹ لمٹ (rate limit) 429 اور retry-after ہیڈر کے ساتھ آ سکتی ہے، جبکہ دوسرے سے یہ ایک مبہم 502 ہو سکتی ہے۔ کچھ فراہم کنندہ درخواست قبول کرتے ہیں اور پھر نیٹ ورک ٹائم آؤٹ سے پہلے دو منٹ تک خاموش ہو جاتے ہیں۔ OpenAI SDK جادوئی طور پر ان کو ان exception types میں تبدیل نہیں کرے گا جن کی آپ کے logs توقع کرتے ہیں۔

غیر متوقع ڈھانچوں کے لیے دفاعی پارسنگ (Defensive Parsing)

حل اسکیما (schema) پر بھروسہ کرنا نہیں ہے۔ حل یہ ہے کہ ہر جواب کو ایک مشکوک چیز کے طور پر لیا جائے۔

یہ فرض نہ کریں کہ content ایک string ہے۔ اسے چھونے سے پہلے چیک کریں۔

const content = response.choices?.[0]?.message?.content;
const text = typeof content === "string" ? content.trim() : "";

یہ فرض نہ کریں کہ tool arguments درست JSON ہیں۔ ماڈل ایکشن کی تجویز دیتا ہے۔ آپ کے کوڈ کو یہ فیصلہ کرنا چاہیے کہ آیا وہ تجویز عمل درآمد کے لیے کافی محفوظ ہے یا نہیں۔ ہر tool argument parse کو try-catch میں لپیٹ دیں۔ اگر JSON.parse کوئی error دے، تو tool call کو غلط (malformed) کچرا سمجھیں اور اسے failure handler کی طرف بھیج دیں۔ ایک غلط بریکٹ یا غائب کوٹیشن مارک کبھی بھی an unhandled exception کے طور پر سامنے نہیں آنا چاہیے۔

اگر tool_calls موجود ہے لیکن content غائب ہے، تو آپ کی ایپلی کیشن کو ایک حالت کی تبدیلی (state transition) کو پہچاننا چاہیے۔ صارف کو چیٹ کا جواب نہیں ملا، بلکہ سسٹم کو ایک ورک آرڈر ملا ہے۔ یہ دو مختلف راستے ہیں، اور آپ کے روٹر کو اسٹرنگ مینیپولیشن کی کوشش کرنے سے پہلے ان کا فرق معلوم ہونا چاہیے۔

ڈیپلائے کرنے سے پہلے طرزِ عمل کے ٹیسٹ (Behavioral Tests)

اینڈ پوائنٹ کو "hi" پیغام کے ساتھ پنگ (ping) کرنا یہ ثابت کرتا ہے کہ نیٹ ورک کام کر رہا ہے۔ یہ آپ کی ایپلی کیشن کے بارے میں کچھ بھی ثابت نہیں کرتا۔

پروڈکشن ٹریفک کو ری ڈائریکٹ کرنے سے پہلے، نئے فراہم کنندہ (provider) کے خلاف ایک ہدف شدہ بیہیویئرل ٹیسٹ سویٹ (behavioral test suite) چلائیں:

  • عام ٹیکسٹ ریسپانس۔ تصدیق کریں کہ content موجود ہے، ایک اسٹرنگ ہے، اور کاسٹنگ ایررز (casting errors) کے بغیر آپ کے سینٹائزیشن پائپ لائن (sanitization pipeline) سے گزر سکتا ہے۔
  • زبردستی ٹول کال (Forced tool call)۔ tool_choice کو required پر سیٹ کریں۔ تصدیق کریں کہ فراہم کنندہ اس کی پاسداری کرتا ہے، اور چیک کریں کہ آیا content بطور null، خالی اسٹرنگ، یا کسی گمشدہ کی (missing key) کے طور پر آتا ہے۔ ان میں سے ہر حالت کے لیے اپنا الگ ہینڈلر (handler) درکار ہوتا ہے۔
  • غلط فارمیٹ والے ٹول آرگیومنٹس۔ ایسے منظرنامے شامل کریں جہاں ماڈل ٹول آرگیومنٹس کے اندر ٹوٹا ہوا JSON واپس کرتا ہے۔ اس بات کو یقینی بنائیں کہ آپ کا پارسر ورکر کو کریش کرنے کے بجائے انہیں شائستگی سے مسترد کر دے۔
  • ٹیکن لیمٹ کے قریب ریسپانس۔ کانٹیکسٹ ونڈو (context window) کو اپنی حد تک لے جائیں۔ finish_reason کو چیک کریں۔ اگر ٹرنکیشن (truncation) کے وقت فراہم کنندہ کچھ غیر متوقع واپس کرتا ہے، تو آپ کے خلاصہ کرنے (summarization) یا ری ٹرائی لاجک کو معلوم ہونا چاہیے کہ اس پر کیسے ردعمل دینا ہے۔

یہ انٹیگریشن ٹیسٹ ہیں، یونٹ ٹیسٹ نہیں۔ یہ آپ کے کوڈ اور فراہم کنندہ کی شخصیت کے درمیان حقیقی تعلق کا امتحان لیتے ہیں۔ مائیگریشن مکمل قرار دینے سے پہلے انہیں پاس کریں۔

ایک انٹرنل کنٹریکٹ بنائیں

فراہم کنندہ کے فرق آپ کی نیٹ ورک باؤنڈری پر ہی ختم ہو جانے چاہئیں۔ انہیں بزنس لاجک میں داخل نہ ہونے دیں۔

ایک نارملائزیشن لیئر (normalization layer) بنائیں جو خام SDK ریسپانس کو استعمال کرے اور ایک ایسا آبجیکٹ فراہم کرے جس کا آپ کی ایپلی کیشن اصل میں مالک ہو۔ فراہم کنندہ کی مخصوص انفرادیتوں کو ایک مستحکم انٹرنل فارمیٹ میں میپ (map) کریں۔ اگر فراہم کنندہ A ٹول آرگیومنٹس کو اسٹرنگز کے طور پر واپس کرتا ہے اور فراہم کنندہ B آبجیکٹس کے طور پر، تو آپ کا میپر دونوں کو آپ کے اپنے ToolRequest اسٹرکچر میں ہموار (flatten) کر دے گا۔ اگر یوزج (usage) موجود نہ ہو، تو آپ کا میپر یا تو اس کا اندازہ لگائے گا یا اس کمی کو نشان زد کرے گا، لیکن یہ کبھی بھی undefined کو آپ کے کاسٹ ٹریکنگ ماڈیولز میں داخل نہیں ہونے دے گا۔

اگر finish_reason غیر معیاری ہے، تو اسے اپنے ٹرمینل اسٹیٹس کے انم (enum) میں ترجمہ کریں: COMPLETE, TRUNCATED, TOOL_CALL, FILTERED۔ آپ کی ایپ کو ان صاف تجریدی تصورات (abstractions) کی بنیاد پر فیصلہ کرنا چاہیے، نہ کہ تھرڈ پارٹی سرور سے خام اسٹرنگز کا تجزیہ کر کے۔

یہ لیئر فراہم کنندہ کی تبدیلی کو ایک مشکل اور مسلسل لڑائی کے بجائے صرف ایک فائل کی تبدیلی بنا دیتی ہے۔ آپ میپر کو دوبارہ لکھتے ہیں، بیہیویئرل ٹیسٹ چلاتے ہیں، اور آگے بڑھ جاتے ہیں۔ آپ کی ایپلی کیشن کو کوئی فرق نہیں پڑتا۔

ایک ڈیپینڈینسی اپ گریڈ، نہ کہ صرف ایک کنفیگ تبدیلی

LLM فراہم کنندہ کو تبدیل کرنا CDN اینڈ پوائنٹس کو بدلنے جیسا نہیں ہے۔ یہ اپنے ڈیٹا بیس کو PostgreSQL سے MySQL میں تبدیل کرنے کے زیادہ قریب ہے۔ آپ کبھی یہ فرض نہیں کریں گے کہ ایک ہی کنکشن اسٹرنگ کا مطلب ایک جیسا کوئری رویہ ہے۔ آپ لاکنگ سیمنٹکس (locking semantics)، مائیگریشن پاتھ، اور انڈیکسنگ کی انفرادیتوں کا ٹیسٹ کریں گے۔ LLMs بھی اسی احترام کے مستحق ہیں۔ وہ معیاری APIs کے روپ میں چھپے ہوئے پروببیلسٹک (probabilistic) سسٹمز ہیں، اور ان کے جوابات فارمیٹنگ، ٹرنکیشن، اور کنٹرول فلو کے بارے میں ایسے مفروضے رکھتے ہیں جو نیٹ ورک کا ایک بھی ایرر دکھائے بغیر آپ کی ایپلی کیشن کو تباہ کر سکتے ہیں۔

بگ کبھی کنکشن میں نہیں تھا۔ یہ اس مفروضے میں تھا کہ مطابقت (compatibility) کا مطلب یکسانیت ہے۔ ایسا نہیں ہے۔ شکل (shape) کی تصدیق کریں۔ کناروں (edges) کا ٹیسٹ کریں۔ کنٹریکٹ کی ذمہ داری خود لیں۔


Source: The Bug Only Happened After I Switched LLM Providers

Community: GyaanSetu AI on Telegram