ਰਿਕਵੈਸਟ ਸਫਲ ਰਹੀ। ਰਿਸਪਾਂਸ ਵੈਲਿਡ JSON ਸੀ। SDK ਚੁੱਪ ਰਿਹਾ। ਫਿਰ ਵੀ ਐਪਲੀਕੇਸ਼ਨ ਡਿੱਗ ਗਈ।

ਇਹ ਉਸ ਘਟਨਾ ਦੀ ਕਹਾਣੀ ਹੈ ਜੋ ਉਦੋਂ ਵਾਪਰਦੀ ਹੈ ਜਦੋਂ ਤੁਸੀਂ LLM ਪ੍ਰੋਵਾਈਡਰ ਨੂੰ ਬਦਲਣ ਨੂੰ ਇੱਕ ਢਾਂਚਾਗਤ ਜੁਏ (structural gamble) ਦੀ ਬਜਾਏ ਸਿਰਫ਼ ਇੱਕ ਕੌਂਫਿਗਰੇਸ਼ਨ ਤਬਦੀਲੀ ਵਾਂਗ ਮੰਨ ਲੈਂਦੇ ਹੋ। ਤੁਸੀਂ ਇੱਕ ਨਵਾਂ 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 ਨੂੰ ਪ੍ਰਾਪਤ ਕਰ ਸਕਦੇ ਹਨ ਅਤੇ ਅਜਿਹੇ ਜਵਾਬ ਦੇ ਸਕਦੇ ਹਨ ਜੋ ਬਹੁਤ ਹੀ ਸੂਖਮ ਅਤੇ ਵਿਨਾਸ਼ਕਾਰੀ ਤਰੀਕਿਆਂ ਨਾਲ ਵੱਖਰੇ ਹੋ ਸਕਦੇ ਹਨ।

ਤੁਹਾਡਾ ਕੋਡ ਕਈ ਅੰਦਾਜ਼ੇ ਲਗਾਉਂਦਾ ਹੈ। ਤੁਸੀਂ ਮੰਨ ਲੈਂਦੇ ਹੋ ਕਿ message.content ਇੱਕ string ਹੈ ਕਿਉਂਕਿ ਪਹਿਲਾਂ ਹਮੇਸ਼ਾ ਇਹੀ ਹੁੰਦਾ ਸੀ। ਤੁਸੀਂ ਮੰਨ ਲੈਂਦੇ ਹੋ ਕਿ tool call ਸਾਫ਼ ਅਤੇ parseable JSON ਦੇ ਨਾਲ ਆਉਂਦਾ ਹੈ। ਤੁਸੀਂ ਮੰਨ ਲੈਂਦੇ ਹੋ ਕਿ finish_reason ਉਹੀ ਸੰਕੇਤ ਦਿੰਦਾ ਹੈ ਜੋ ਤੁਸੀਂ ਸਮਝਦੇ ਹੋ। ਇਹ ਅੰਦਾਜ਼ੇ ਉਦੋਂ ਤੱਕ ਅਦਿੱਖ ਰਹਿੰਦੇ ਹਨ ਜਦੋਂ ਤੱਕ ਉਹ ਘਾਤਕ ਨਹੀਂ ਬਣ ਜਾਂਦੇ।

ਉਸ ਕ੍ਰੈਸ਼ (crash) 'ਤੇ ਵਿਚਾਰ ਕਰੋ ਜਿਸ ਨੇ ਇਹ ਸਭ ਸ਼ੁਰੂ ਕੀਤਾ:

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

ਇਹ ਲਾਈਨ ਬਹੁਤ ਮਾਸੂਮ ਲੱਗਦੀ ਹੈ। ਇਹ ਹਫ਼ਤਿਆਂ ਤੱਕ ਕੰਮ ਕਰਦੀ ਰਹੀ। ਫਿਰ ਨਵੇਂ ਪ੍ਰੋਵਾਈਡਰ ਨੇ ਇੱਕ tool call ਵਾਪਸ ਕੀਤਾ। ਉਸ ਸਮੇਂ, message.content ਇੱਕ ਖਾਲੀ string ਨਹੀਂ ਸੀ। ਇਹ null ਸੀ। ਅਸਲ payload message.tool_calls ਦੇ ਅੰਦਰ ਸੀ, ਪਰ parser ਪਹਿਲਾਂ ਹੀ ਅੱਗੇ ਵਧ ਚੁੱਕਾ ਸੀ, ਅਤੇ ਕੁਝ ਵੀ ਨਾ ਹੋਣ ਦੇ ਬਾਵਜੂਦ .trim() ਕਾਲ ਕਰ ਰਿਹਾ ਸੀ। API ਨੇ ਕੋਈ error ਨਹੀਂ ਦਿੱਤੀ। network layer ਨੇ ਕੋਈ ਸ਼ਿਕਾਇਤ ਨਹੀਂ ਕੀਤੀ। ਤੁਹਾਡੇ ਆਪਣੇ parser ਨੇ ਹੀ ਰਿਕਵੈਸਟ ਨੂੰ ਖਤਮ ਕਰ ਦਿੱਤਾ।

ਜਿੱਥੇ ਪ੍ਰੋਵਾਈਡਰ ਚੁੱਪਚਾਪ ਵੱਖਰੇ ਹੋ ਜਾਂਦੇ ਹਨ

ਇਹ ਅੰਤਰ changelogs ਵਿੱਚ ਸਾਫ਼ ਨਹੀਂ ਹੁੰਦੇ। ਉਹ response object ਦੇ ਕਿਨਾਰਿਆਂ 'ਤੇ ਬੈਠੇ ਹੁੰਦੇ ਹਨ, edge cases ਦੀ ਉਡੀਕ ਕਰਦੇ ਹੋਏ।

Tool-call formatting. ਇੱਕ ਪ੍ਰੋਵਾਈਡਰ tool arguments ਨੂੰ ਇੱਕ pre-validated JSON object ਵਜੋਂ ਭੇਜਦਾ ਹੈ। ਦੂਜਾ ਉਹਨਾਂ ਨੂੰ ਇੱਕ field ਦੇ ਅੰਦਰ ਇੱਕ escaped string ਵਜੋਂ ਭੇਜਦਾ ਹੈ। ਤੀਜਾ ਪ੍ਰੋਵਾਈਡਰ ਇੱਕ ਲੰਬੇ tool call ਨੂੰ ਕਈ streaming deltas ਵਿੱਚ ਵੰਡ ਸਕਦਾ ਹੈ, ਜਿਸ ਨਾਲ ਤੁਹਾਨੂੰ ਇਹ ਦੇਖਣ ਤੋਂ ਪਹਿਲਾਂ ਕਿ ਢਾਂਚਾ ਵੈਲਿਡ ਹੈ ਜਾਂ ਨਹੀਂ, chunks ਨੂੰ buffer ਕਰਨਾ ਪੈਂਦਾ ਹੈ। ਜੇਕਰ ਤੁਹਾਡੀ ਐਪਲੀਕੇਸ਼ਨ ਇੱਕ ਸਿੰਗਲ parseable blob ਦੀ ਉਮੀਦ ਕਰਦੀ ਹੈ, ਤਾਂ ਇਹ ਫਸ ਜਾਂਦੀ ਹੈ।

Finish reasons. OpenAI "stop", "length", "tool_calls", ਅਤੇ "content_filter" ਵਰਗੇ ਖਾਸ strings ਦੀ ਵਰਤੋਂ ਕਰਦਾ ਹੈ। ਇੱਕ compatible ਪ੍ਰੋਵਾਈਡਰ "end_turn" ਵਾਪਸ ਕਰ ਸਕਦਾ ਹੈ ਜਾਂ ਜਦੋਂ ਮਾਡਲ token ceiling 'ਤੇ ਪਹੁੰਚਦਾ ਹੈ ਤਾਂ ਸਿਰਫ਼ ਉਸ field ਨੂੰ ਛੱਡ ਸਕਦਾ ਹੈ। ਜੇਕਰ ਤੁਹਾਡਾ retry ਜਾਂ fallback logic truncation ਦਾ ਪਤਾ ਲਗਾਉਣ ਲਈ "length" ਦੀ ਉਡੀਕ ਕਰਦਾ ਹੈ, ਤਾਂ ਇਹ ਉਦੋਂ ਤੱਕ ਵਿਹਲਾ ਬੈਠਾ ਰਹੇਗਾ ਜਦੋਂ ਤੱਕ ਯੂਜ਼ਰ ਅਧੂਰਾ ਜਵਾਬ ਦੇਖ ਰਿਹਾ ਹੁੰਦਾ ਹੈ।

Usage fields. ਕੁਝ ਪ੍ਰੋਵਾਈਡਰ latency ਘਟਾਉਣ ਲਈ streaming responses ਤੋਂ token counts ਹਟਾ ਦਿੰਦੇ ਹਨ। ਹੋਰ usage ਨੂੰ ਸਿਰਫ਼ ਆਖਰੀ chunk ਵਿੱਚ ਜੋੜਦੇ ਹਨ, ਜਾਂ non-streaming calls ਵਿੱਚ ਇਸਨੂੰ ਪੂਰੀ ਤਰ੍ਹਾਂ ਛੱਡ ਦਿੰਦੇ ਹਨ। ਜੇਕਰ ਤੁਸੀਂ ਗਾਹਕਾਂ ਤੋਂ ਪ੍ਰਤੀ token ਚਾਰਜ ਕਰਦੇ ਹੋ ਅਤੇ ਤੁਹਾਡਾ accounting code ਹਰ response object ਵਿੱਚ usage.total_tokens ਦੀ ਉਮੀਦ ਕਰਦਾ ਹੈ, ਤਾਂ ਤੁਹਾਡਾ billing pipeline ਚੁੱਪਚਾਪ ਜ਼ੀਰੋ ਰਿਕਾਰਡ ਕਰੇਗਾ।

Streaming behavior. Server-sent events ਨੂੰ standard ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ, ਫਿਰ ਵੀ ਪ੍ਰੋਵਾਈਡਰ ਵੱਖ-ਵੱਖ ਫ੍ਰੀਕੁਐਂਸੀਆਂ 'ਤੇ buffers ਨੂੰ flush ਕਰਦੇ ਹਨ। Event boundaries ਵੱਖ-ਵੱਖ ਹੁੰਦੀਆਂ ਹਨ। ਇੱਕ ਪ੍ਰੋਵਾਈਡਰ [DONE] signal ਦੇ ਨਾਲ stream ਨੂੰ ਖਤਮ ਕਰਦਾ ਹੈ। ਦੂਜਾ ਬਿਨਾਂ ਕਿਸੇ sentinel ਦੇ ਕਨੈਕਸ਼ਨ ਨੂੰ ਸਾਫ਼ ਤਰੀਕੇ ਨਾਲ ਕੱਟ ਦਿੰਦਾ ਹੈ। ਜੇਕਰ ਤੁਹਾਡਾ client ਕਿਸੇ ਖਾਸ closing marker ਦੀ ਉਡੀਕ ਵਿੱਚ ਬਲੌਕ ਹੋ ਜਾਂਦਾ ਹੈ, ਤਾਂ ਇਹ ਹੈਂਗ (hang) ਹੋ ਜਾਂਦਾ ਹੈ।

Errors and timeouts. ਇੱਕ ਪ੍ਰੋਵਾਈਡਰ ਤੋਂ rate limit ਇੱਕ 429 error ਅਤੇ retry-after header ਦੇ ਰੂਪ ਵਿੱਚ ਆ ਸਕਦੀ ਹੈ, ਅਤੇ ਦੂਜੇ ਤੋਂ ਇੱਕ ਅਸਪਸ਼ਟ 502 ਦੇ ਰੂਪ ਵਿੱਚ। ਕੁਝ ਪ੍ਰੋਵਾਈਡਰ ਰਿਕਵੈਸਟ ਨੂੰ ਸਵੀਕਾਰ ਕਰਦੇ ਹਨ ਅਤੇ ਫਿਰ network timeout ਤੋਂ ਪਹਿਲਾਂ ਦੋ ਮਿੰਟਾਂ ਲਈ ਚੁੱਪ ਹੋ ਜਾਂਦੇ ਹਨ। OpenAI SDK ਜਾਦੂਈ ਤਰੀਕੇ ਨਾਲ ਇਹਨਾਂ ਨੂੰ ਉਹਨਾਂ exception types ਵਿੱਚ ਨਹੀਂ ਬਦਲੇਗਾ ਜਿਨ੍ਹਾਂ ਦੀ ਤੁਹਾਡੇ logs ਉਮੀਦ ਕਰਦੇ ਹਨ।

ਅਣਪਛਾਤੇ ਢਾਂਚਿਆਂ ਲਈ Defensive Parsing

ਇਸਦਾ ਹੱਲ schema 'ਤੇ ਭਰੋਸਾ ਕਰਨਾ ਨਹੀਂ ਹੈ। ਹੱਲ ਇਹ ਹੈ ਕਿ ਹਰ ਰਿਸਪਾਂਸ ਨੂੰ ਇੱਕ ਸ਼ੱਕੀ (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 ਵਿੱਚ ਰੱਖੋ। ਜੇਕਰ JSON.parse error ਦਿੰਦਾ ਹੈ, ਤਾਂ tool call ਨੂੰ ਗਲਤ (malformed) ਸਮਝੋ ਅਤੇ ਇਸਨੂੰ failure handler ਵੱਲ ਭੇਜ ਦਿਓ। ਇੱਕ ਗਲਤ bracket ਜਾਂ ਗੁੰਮ ਹੋਇਆ quote ਕਦੇ ਵੀ unhandled exception ਵਜੋਂ ਉੱਪਰ ਨਹੀਂ ਆਉਣਾ ਚਾਹੀਦਾ।

ਜੇਕਰ tool_calls ਮੌਜੂਦ ਹੈ ਪਰ content ਗੁੰਮ ਹੈ, ਤਾਂ ਤੁਹਾਡੀ ਐਪਲੀਕੇਸ਼ਨ ਨੂੰ state transition ਨੂੰ ਪਛਾਣਨਾ ਚਾਹੀਦਾ ਹੈ। ਯੂਜ਼ਰ ਨੂੰ ਚੈਟ ਦਾ ਜਵਾਬ ਨਹੀਂ ਮਿਲਿਆ। ਸਿਸਟਮ ਨੂੰ ਇੱਕ ਕੰਮ ਦਾ ਆਰਡਰ (work order) ਮਿਲਿਆ ਹੈ। ਇਹ ਦੋ ਵੱਖਰੇ ਰਸਤੇ ਹਨ, ਅਤੇ ਤੁਹਾਡੇ router ਨੂੰ string manipulation ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਇਹ ਅੰਤਰ ਪਤਾ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ।

ਡਿਪਲੋਏ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਵਿਵਹਾਰਕ ਟੈਸਟ (Behavioral Tests)

"hi" ਮੈਸੇਜ ਨਾਲ ਐਂਡਪੁਆਇੰਟ (endpoint) ਨੂੰ ਪਿੰਗ ਕਰਨਾ ਇਹ ਸਾਬਤ ਕਰਦਾ ਹੈ ਕਿ ਨੈੱਟਵਰਕ ਕੰਮ ਕਰ ਰਿਹਾ ਹੈ। ਇਹ ਤੁਹਾਡੀ ਐਪਲੀਕੇਸ਼ਨ ਬਾਰੇ ਕੁਝ ਵੀ ਸਾਬਤ ਨਹੀਂ ਕਰਦਾ।

ਪ੍ਰੋਡਕਸ਼ਨ ਟ੍ਰੈਫਿਕ (production traffic) ਨੂੰ ਰੀਡਾਇਰੈਕਟ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ, ਨਵੇਂ ਪ੍ਰੋਵਾਈਡਰ (provider) ਵਿਰੁੱਧ ਇੱਕ ਟਾਰਗੇਟਡ ਬਿਹੇਵੀਅਰਲ ਟੈਸਟ ਸੂਟ (behavioral test suite) ਚਲਾਓ:

  • ਨਾਰਮਲ ਟੈਕਸਟ ਰਿਸਪਾਂਸ (Normal text response)। ਪੁਸ਼ਟੀ ਕਰੋ ਕਿ content ਮੌਜੂਦ ਹੈ, ਇੱਕ string ਹੈ, ਅਤੇ casting errors ਤੋਂ ਬਿਨਾਂ ਤੁਹਾਡੇ sanitization pipeline ਵਿੱਚੋਂ ਲੰਘ ਸਕਦਾ ਹੈ।
  • ਫੋਰਸਡ ਟੂਲ ਕਾਲ (Forced tool call)। tool_choice ਨੂੰ required 'ਤੇ ਸੈੱਟ ਕਰੋ। ਪੁਸ਼ਟੀ ਕਰੋ ਕਿ ਪ੍ਰੋਵਾਈਡਰ ਇਸ ਦਾ ਪਾਲਣ ਕਰਦਾ ਹੈ, ਅਤੇ ਚੈੱਕ ਕਰੋ ਕਿ content null, ਇੱਕ ਖਾਲੀ string, ਜਾਂ ਇੱਕ missing key ਵਜੋਂ ਆ ਰਿਹਾ ਹੈ। ਇਹਨਾਂ ਵਿੱਚੋਂ ਹਰੇਕ ਸਟੇਟ (state) ਲਈ ਆਪਣੇ ਵੱਖਰੇ ਹੈਂਡਲਰ (handler) ਦੀ ਲੋੜ ਹੁੰਦੀ ਹੈ।
  • ਮੈਲਫਾਰਮਡ ਟੂਲ ਆਰਗੂਮੈਂਟਸ (Malformed tool arguments)। ਅਜਿਹੇ ਸਥਿਤੀਆਂ (scenarios) ਬਣਾਓ ਜਿੱਥੇ ਮਾਡਲ ਟੂਲ ਆਰਗੂਮੈਂਟਸ ਦੇ ਅੰਦਰ ਟੁੱਟਿਆ ਹੋਇਆ JSON ਵਾਪਸ ਕਰਦਾ ਹੈ। ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਤੁਹਾਡਾ ਪਾਰਸਰ (parser) ਵਰਕਰ ਨੂੰ ਕ੍ਰੈਸ਼ ਕਰਨ ਦੀ ਬਜਾਏ ਉਹਨਾਂ ਨੂੰ ਸਹੀ ਤਰੀਕੇ ਨਾਲ ਰੱਦ ਕਰ ਦਿੰਦਾ ਹੈ।
  • ਟੋਕਨ ਲਿਮਿਟ ਦੇ ਨੇੜੇ ਰਿਸਪਾਂਸ (Response near the token limit)। context window ਨੂੰ ਪੂਰਾ ਵਰਤੋ। finish_reason ਦੀ ਜਾਂਚ ਕਰੋ। ਜੇਕਰ truncation ਹੋਣ ਵੇਲੇ ਪ੍ਰੋਵਾਈਡਰ ਕੁਝ ਅਣਕਿਆਸਿਆ ਵਾਪਸ ਕਰਦਾ ਹੈ, ਤਾਂ ਤੁਹਾਡੀ summarization ਜਾਂ retry logic ਨੂੰ ਪਤਾ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ ਕਿ ਕਿਵੇਂ ਪ੍ਰਤੀਕਿਰਿਆ ਦੇਣੀ ਹੈ।

ਇਹ ਇੰਟੀਗ੍ਰੇਸ਼ਨ ਟੈਸਟ (integration tests) ਹਨ, ਯੂਨਿਟ ਟੈਸਟ (unit tests) ਨਹੀਂ। ਇਹ ਤੁਹਾਡੇ ਕੋਡ ਅਤੇ ਪ੍ਰੋਵਾਈਡਰ ਦੇ ਸੁਭਾਅ (personality) ਵਿਚਕਾਰ ਅਸਲ ਸਬੰਧ ਦੀ ਜਾਂਚ ਕਰਦੇ ਹਨ। ਮਾਈਗ੍ਰੇਸ਼ਨ (migration) ਪੂਰਾ ਹੋਣ ਦਾ ਦਾਅਵਾ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਇਹਨਾਂ ਨੂੰ ਪਾਸ ਕਰੋ।

ਇੱਕ ਅੰਦਰੂਨੀ ਕੰਟਰੈਕਟ (Internal Contract) ਬਣਾਓ

ਪ੍ਰੋਵਾਈਡਰ ਦੇ ਅੰ