विनंती यशस्वी झाली. प्रतिसाद वैध JSON होता. SDK शांत होते. तरीही ॲप्लिकेशन कोलमडले.

जेव्हा तुम्ही LLM प्रोव्हायडर बदलणे ही एक स्ट्रक्चरल जोखीम समजण्याऐवजी केवळ एक कॉन्फिगरेशन बदल समजता, तेव्हा काय घडते याची ही कथा आहे. तुम्ही नवीन base URL पेस्ट करता, API key बदलता आणि request body तशीच ठेवता कारण डॉक्युमेंटेशनमध्ये OpenAI-compatible endpoint चे आश्वासन दिले जाते. एका साध्या "hello world" प्रॉम्प्टसाठी ते काम करते. तुम्ही आनंद साजरा करता. पण जेव्हा प्रत्यक्ष ट्रॅफिक येते, तेव्हा सर्व काही विस्कळीत होते.

Wire Compatibility चा आभास

HTTP लेयरवरील सुसंगतता अत्यंत वरवरची असते. 200 status code आणि JSON body चा अर्थ असा आहे की सर्व्हरने तुमचा संदेश स्वीकारला आहे. याचा अर्थ असा नाही की सर्व्हर पूर्वीच्या सर्व्हरप्रमाणेच विचार करतो. OpenAI-compatible endpoints मध्ये request चा आकार (shape) सारखा असू शकतो, पण त्यांचा वर्तणुकीचा करार (behavioral contract) सारखा नसतो. दोन प्रोव्हायडर्स सारखेच payloads स्वीकारू शकतात आणि अशा प्रकारे उत्तरे देऊ शकतात जी सूक्ष्म पण विनाशकारी पद्धतीने वेगळी असू शकतात.

तुमचा कोड गृहितके (assumptions) धरतो. तुम्ही असे गृहीत धरता की message.content ही एक string आहे कारण पूर्वी ती नेहमीच असायची. तुम्ही असे गृहीत धरता की tool call मध्ये स्वच्छ, parse करण्यायोग्य JSON येईल. तुम्ही असे गृहीत धरता की finish_reason जे संकेत देतो, त्याचा अर्थ तोच आहे. ही गृहितके घातक ठरण्यापर्यंत अदृश्य असतात.

त्या क्रॅशचा विचार करा ज्याने या सर्वांची सुरुवात केली:

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

ही ओळ निष्पाप वाटते. अनेक आठवडे ती व्यवस्थित चालली. मग नवीन प्रोव्हायडरने एक tool call पाठवला. त्या क्षणी, message.content ही रिकामी string नव्हती, तर ती null होती. प्रत्यक्ष payload message.tool_calls मध्ये होता, पण parser आधीच पुढे गेला होता आणि काहीही नसलेल्या गोष्टीवर .trim() कॉल करत होता. API ने एरर दिला नाही. नेटवर्क लेयरने तक्रार केली नाही. तुमच्या स्वतःच्या parser ने विनंती (request) नष्ट केली.

जिथे प्रोव्हायडर्स शांतपणे वेगळे होतात

हे फरक changelogs मध्ये जाहीर केले जात नाहीत. ते response object च्या कडेला (margins) बसलेले असतात आणि edge cases ची वाट पाहत असतात.

Tool-call formatting. एक प्रोव्हायडर tool arguments हे pre-validated JSON object म्हणून पाठवतो. दुसरा ते एका field मध्ये escaped string म्हणून पाठवतो. तिसरा कदाचित एक मोठा tool call अनेक streaming deltas मध्ये विभागू शकतो, ज्यामुळे स्ट्रक्चर वैध आहे की नाही हे पाहण्यापूर्वीच तुम्हाला chunks buffer करावे लागतील. जर तुमचे ॲप्लिकेशन एकाच parse करण्यायोग्य blob ची अपेक्षा करत असेल, तर ते अडखळते.

Finish reasons. OpenAI "stop", "length", "tool_calls", आणि "content_filter" सारखे विशिष्ट strings वापरते. एक सुसंगत प्रोव्हायडर "end_turn" परत करू शकतो किंवा मॉडेलने token ceiling गाठल्यावर ते field सोडून देऊ शकतो. जर तुमचे retry किंवा fallback logic truncation शोधण्यासाठी "length" ची वाट पाहत असेल, तर युजरला अर्धवट उत्तर दिसत असताना तुमचे लॉजिक रिकामे बसलेले असेल.

Usage fields. काही प्रोव्हायडर्स latency कमी करण्यासाठी streaming responses मधून token counts काढून टाकतात. इतर फक्त शेवटच्या chunk मध्ये usage जोडतात किंवा non-streaming calls मध्ये ते पूर्णपणे वगळतात. जर तुम्ही ग्राहकांकडून प्रति token शुल्क आकारत असाल आणि तुमचा accounting code प्रत्येक response object मध्ये usage.total_tokens असेल अशी अपेक्षा करत असेल, तर तुमचे billing pipeline शांतपणे शून्य (zeros) नोंदवेल.

Streaming behavior. Server-sent events हे standard असायला हवेत, तरीही प्रोव्हायडर्स वेगवेगळ्या वारंवारतेने (frequencies) buffers flush करतात. Event boundaries बदलतात. एक प्रोव्हायडर [DONE] सिग्नलसह stream संपवतो. दुसरा कोणत्याही sentinel शिवाय कनेक्शन स्वच्छपणे तोडतो. जर तुमचा client एखाद्या विशिष्ट closing marker ची वाट पाहत ब्लॉक झाला, तर तो हँग (hang) होतो.

Errors and timeouts. एका प्रोव्हायडरकडून rate limit 429 आणि retry-after header सह येऊ शकते, तर दुसऱ्याकडून अस्पष्ट 502 म्हणून येऊ शकते. काही प्रोव्हायडर्स विनंती स्वीकारतात आणि त्यानंतर नेटवर्क timeout होण्यापूर्वी दोन मिनिटे शांत बसतात. OpenAI SDK या गोष्टी जादूने तुमच्या logs ला अपेक्षित असलेल्या exception types मध्ये रूपांतरित करणार नाही.

अनपेक्षित आकारांसाठी Defensive Parsing

उपाय schema वर विश्वास ठेवणे हा नाही. उपाय प्रत्येक response ला संशयित (suspect) मानणे हा आहे.

content ही string आहे असे गृहीत धरू नका. त्याला वापरण्यापूर्वी तपासा.

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

tool arguments हे वैध JSON आहेत असे गृहीत धरू नका. मॉडेल एक कृती सुचवते. ती सुचवलेली कृती कार्यान्वित करण्यासाठी सुरक्षित आहे की नाही याचा निर्णय तुमच्या कोडने घ्यायला हवा. प्रत्येक tool argument parse ला try-catch मध्ये गुंडाळा (wrap). जर JSON.parse ने error दिला, तर त्या tool call ला चुकीचा (malformed) कचरा मानून failure handler कडे वळवा. एखादा hallucinated bracket किंवा विसरलेला quote कधीही unhandled exception म्हणून वर येऊ नये.

जर tool_calls अस्तित्वात असेल पण content नसेल, तर तुमच्या ॲप्लिकेशनने state transition ओळखले पाहिजे. युजरला चॅट रिप्लाय मिळाला नाही, तर सिस्टमला एक 'work order' मिळाला आहे. हे दोन वेगळे मार्ग आहेत आणि string manipulation करण्यापूर्वी तुमच्या router ला त्यातील फरक माहित असावा.

Deploy करण्यापूर्वी Behavioral Tests

"hi" संदेशासह एंडपॉईंटला (endpoint) पिंग करणे म्हणजे नेटवर्क काम करत आहे हे सिद्ध होते. परंतु, तुमच्या ॲप्लिकेशनबद्दल (application) ते काहीही सिद्ध करत नाही.

प्रोडक्शन ट्रॅफिक (production traffic) रिडायरेक्ट करण्यापूर्वी, नवीन प्रोव्हायडरसाठी (provider) एक लक्ष्यित बिहेवियरल टेस्ट सूट (behavioral test suite) चालवा:

  • सामान्य मजकूर प्रतिसाद (Normal text response). content अस्तित्वात आहे, तो एक स्ट्रिंग (string) आहे आणि कास्टिंग एररशिवाय (casting errors) तुमच्या सॅनिटायझेशन पाइपलाइनमधून (sanitization pipeline) जाऊ शकतो याची खात्री करा.
  • फोर्सड टूल कॉल (Forced tool call). tool_choice ला required वर सेट करा. प्रोव्हायडर त्याचे पालन करतो की नाही याची खात्री करा आणि content हे null, रिकामी स्ट्रिंग (empty string) किंवा मिसिंग की (missing key) म्हणून येते का ते तपासा. यातील प्रत्येक स्थितीसाठी (state) स्वतंत्र हँडलरची (handler) आवश्यकता असते.
  • मॅलफॉर्मड टूल आर्ग्युमेंट्स (Malformed tool arguments). अशा परिस्थिती निर्माण करा जिथे मॉडेल टूल आर्ग्युमेंट्समध्ये तुटलेले (broken) JSON परत करते. तुमचा पार्सर (parser) वर्कर क्रॅश करण्याऐवजी त्यांना व्यवस्थितपणे नाकारतो याची खात्री करा.
  • टोकन मर्यादेच्या जवळ असलेला प्रतिसाद (Response near the token limit). कॉन्टेक्स्ट विंडोला (context window) मर्यादेपर्यंत ढकलून पहा. finish_reason तपासा. जर ट्रंकेशन (truncation) होताना प्रोव्हायडर काही अनपेक्षित परत करत असेल, तर तुमच्या समरायझेशन (summarization) किंवा रिट्राय लॉजिकला (retry logic) त्यावर कशी प्रतिक्रिया द्यायची हे माहित असणे आवश्यक आहे.

हे इंटिग्रेशन टेस्ट्स (integration tests) आहेत, युनिट टेस्ट्स (unit tests) नाहीत. ते तुमच्या कोड आणि प्रोव्हायडरच्या व्यक्तिमत्त्वामधील (personality) वास्तविक संबंधांची चाचणी घेतात. मायग्रेशन पूर्ण झाले असे म्हणण्यापूर्वी या चाचण्या यशस्वी करा.

एक अंतर्गत करार (Internal Contract) तयार करा

प्रोव्हायडरमधील फरक तुमच्या नेटवर्क बाउंड्रीवरच (network boundary) थांबले पाहिजेत. त्यांना बिझनेस लॉजिकमध्ये (business logic) मिसळू देऊ नका.

एक नॉर्मलायझेशन लेयर (normalization layer) तयार करा जो रॉ SDK रिस्पॉन्स (raw SDK response) स्वीकारतो आणि तुमच्या ॲप्लिकेशनच्या मालकीचे एक ऑब्जेक्ट (object) तयार करतो. प्रोव्हायडर-विशिष्ट वैशिष्ट्यांना (eccentricities) एका स्थिर अंतर्गत फॉरमॅटमध्ये मॅप करा. जर प्रोव्हायडर A टूल आर्ग्युमेंट्स स्ट्रिंग म्हणून परत करत असेल आणि प्रोव्हायडर B ऑब्जेक्ट्स परत करत असेल, तर तुमचा मॅपर (mapper) या दोन्हीना तुमच्या स्वतःच्या ToolRequest स्ट्रक्चरमध्ये रूपांतरित करतो. जर युसेज (usage) माहिती नसेल, तर तुमचा मॅपर एकतर त्याचा अंदाज घेतो किंवा त्रुटी दर्शवतो, परंतु तो कधीही undefined तुमच्या कॉस्ट-ट्रॅकिंग मॉड्यूल्समध्ये (cost-tracking modules) शिरू देणार नाही.

जर finish_reason नॉन-स्टँडर्ड (nonstandard) असेल, तर त्याचे तुमच्या स्वतःच्या टर्मिनल स्टेट्सच्या (terminal states) एनममध्ये (enum) रूपांतर करा: COMPLETE, TRUNCATED, TOOL_CALL, FILTERED. तुमच्या ॲपला थर्ड-पार्टी सर्व्हरवरून रॉ स्ट्रिंग्स शोधण्याऐवजी, या स्वच्छ ॲब्स्ट्रॅक्शन्सवर (abstractions) आधारित काय करायचे हे ठरवले पाहिजे.

हा लेयर प्रोव्हायडर बदलण्याच्या प्रक्रियेला (provider swaps) गोंधळात टाकणाऱ्या खेळाऐवजी केवळ एका फाईलमध्ये होणारा बदल बनवतो. तुम्ही मॅपर पुन्हा लिहिता, बिहेवियरल टेस्ट्स चालवता आणि पुढे जाता. तुमचे ॲप्लिकेशन तसे तसे राहते.

कॉन्फिग ट्वीक (Config Tweak) नाही, तर डिपेंडन्सी अपग्रेड (Dependency Upgrade)

LLM प्रोव्हायडर बदलणे म्हणजे CDN एंडपॉईंट्स बदलण्यासारखे नाही. ते तुमचे डेटाबेस PostgreSQL वरून MySQL मध्ये बदलण्यासारखे आहे. एकच कनेक्शन स्ट्रिंग (connection string) म्हणजे समान क्वेरी बिहेवियर (query behavior) असेल असे तुम्ही कधीही गृहीत धरणार नाही. तुम्ही लॉकिंग सिमेंटिक्स (locking semantics), मायग्रेशन पाथ्स (migration paths) आणि इंडेक्सिंग क्वर्क्स (indexing quirks) तपासाल. LLMs देखील त्याच आदरास पात्र आहेत. ते स्टँडर्ड API म्हणून काम करणारे संभाव्य (probabilistic) सिस्टम्स आहेत, आणि त्यांच्या प्रतिसादांमध्ये फॉरमॅटिंग, ट्रंकेशन आणि कंट्रोल फ्लोबद्दल असे गृहितक असतात जे कोणताही नेटवर्क एरर न देता तुमचे ॲप्लिकेशन उद्ध्वस्त करू शकतात.

बग कधीही कनेक्शनमध्ये नव्हता. तो 'सुसंगतता म्हणजे समानता' (compatibility means sameness) या गृहितकात होता. तसे नसते. स्ट्रक्चरची (shape) पडताळणी करा. कडांची (edges) चाचणी घ्या. कराराची (contract) जबाबदारी घ्या.


Source: The Bug Only Happened After I Switched LLM Providers

Community: GyaanSetu AI on Telegram