<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: AI ROUTER</title>
    <description>The latest articles on DEV Community by AI ROUTER (@ai-router).</description>
    <link>https://dev.to/ai-router</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4016045%2F3417618b-edaa-4046-a907-5315b7c9399c.png</url>
      <title>DEV Community: AI ROUTER</title>
      <link>https://dev.to/ai-router</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ai-router"/>
    <language>en</language>
    <item>
      <title>How to estimate ChatGPT API costs before moving a coding agent to production</title>
      <dc:creator>AI ROUTER</dc:creator>
      <pubDate>Tue, 01 Sep 2026 17:20:45 +0000</pubDate>
      <link>https://dev.to/ai-router/how-to-estimate-chatgpt-api-costs-before-moving-a-coding-agent-to-production-5bo7</link>
      <guid>https://dev.to/ai-router/how-to-estimate-chatgpt-api-costs-before-moving-a-coding-agent-to-production-5bo7</guid>
      <description>&lt;h1&gt;
  
  
  How to estimate ChatGPT API costs before moving a coding agent to production
&lt;/h1&gt;

&lt;p&gt;Subscription pricing and API pricing are different products. A coding agent can feel inexpensive in a chat subscription and still produce a very different bill when it starts sending long context, tool results, retries, and parallel requests through an API. The safest migration is to measure the workflow first, then choose an endpoint and package that match the observed usage.&lt;/p&gt;

&lt;p&gt;This checklist is provider-neutral. It works for a direct provider endpoint or an independent relay that exposes an OpenAI-compatible interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Measure a real task, not a hello-world request
&lt;/h2&gt;

&lt;p&gt;Capture a small sample of the tasks your agent actually performs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;input tokens, including system instructions and retrieved files&lt;/li&gt;
&lt;li&gt;output tokens, including tool arguments and structured responses&lt;/li&gt;
&lt;li&gt;model ID and whether the request uses Chat Completions or Responses&lt;/li&gt;
&lt;li&gt;number of tool calls and the size of each tool result&lt;/li&gt;
&lt;li&gt;retries, timeouts, and requests that were cancelled halfway through a stream&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The basic estimate is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;estimated cost = input tokens × input rate
               + output tokens × output rate
               + cache/tool charges, if applicable
               + retry and background-job cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not compare a subscription allowance with a token rate as if they were the same unit. Record the units beside every number in your spreadsheet or dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Treat retries as part of the workload
&lt;/h2&gt;

&lt;p&gt;A failed request is not automatically free. A retry may resend the full conversation, and a tool loop can multiply the context several times. Track a request ID and an attempt number so that you can answer three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How often did the first attempt fail?&lt;/li&gt;
&lt;li&gt;How many tokens were sent again?&lt;/li&gt;
&lt;li&gt;Did the second attempt return a complete result or only a partial stream?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For streaming clients, persist only safe metadata such as status, duration, model ID, and token counters. Never put API keys or authorization headers in logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Verify compatibility before changing the base URL
&lt;/h2&gt;

&lt;p&gt;“It returned HTTP 200” is not a complete compatibility test. A useful smoke test checks the exact path your application needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authenticated model discovery, such as &lt;code&gt;GET /v1/models&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the request shape used by your SDK&lt;/li&gt;
&lt;li&gt;streaming delimiters and the &lt;code&gt;[DONE]&lt;/code&gt; event, if your client streams&lt;/li&gt;
&lt;li&gt;tool calls, JSON arguments, and finish reasons&lt;/li&gt;
&lt;li&gt;distinct handling for 401, 403, 404, 408, 429, and 5xx responses&lt;/li&gt;
&lt;li&gt;usage visibility in the provider dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the same fixture against both endpoints and compare the parsed fields, not just the HTTP status. Keep the model ID account-specific; a model name shown in one account may not be enabled in another.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep the application change small
&lt;/h2&gt;

&lt;p&gt;For an OpenAI-shaped client, the first experiment should usually change only the key and base URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AI_ROUTER_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AI_ROUTER_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.ai-router.dev/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AI_ROUTER_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run one compatibility smoke test.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the key in an environment variable or secret manager. Start with a small quota, set an application-side budget, and make the base URL configurable so that rollback is one deployment setting rather than a code rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Decide whether a relay is useful for your workflow
&lt;/h2&gt;

&lt;p&gt;An independent relay can be useful when a team wants one API-key workflow, usage visibility, package-based spending limits, or access to more than one model family through a consistent integration surface. It is still a separate service: verify its current model catalog, limits, data handling, support path, and prices before sending production traffic.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on AI-ROUTER, an independent service that provides a ChatGPT and Claude API relay. It is not OpenAI or Anthropic, and this article is not an endorsement by either provider. Developers can review the current endpoint, package information, and account controls on the &lt;a href="https://ai-router.dev/" rel="noopener noreferrer"&gt;ChatGPT and Claude API relay&lt;/a&gt; homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Use a go/no-go checklist
&lt;/h2&gt;

&lt;p&gt;Before moving a coding agent beyond a small trial, confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the observed input/output mix fits the budget you set&lt;/li&gt;
&lt;li&gt;retry and tool-call behavior has been measured&lt;/li&gt;
&lt;li&gt;streaming and structured-output fixtures pass&lt;/li&gt;
&lt;li&gt;model IDs are available to the account that will run the job&lt;/li&gt;
&lt;li&gt;API-key usage and balance can be inspected without exposing secrets&lt;/li&gt;
&lt;li&gt;a timeout, retry limit, and rollback URL are configured&lt;/li&gt;
&lt;li&gt;the team knows which service owns the endpoint and where to report an outage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach gives you a defensible cost estimate and a reversible migration path. It also prevents the common mistake of choosing an API solely because its headline price looks lower while the actual agent workload, retries, and limits remain unknown.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>chatgpt</category>
      <category>devops</category>
      <category>apigateway</category>
    </item>
    <item>
      <title>Как быстро проверить ChatGPT API в существующем OpenAI-клиенте</title>
      <dc:creator>AI ROUTER</dc:creator>
      <pubDate>Fri, 28 Aug 2026 09:31:52 +0000</pubDate>
      <link>https://dev.to/ai-router/kak-bystro-provierit-chatgpt-api-v-sushchiestvuiushchiem-openai-kliientie-1jjn</link>
      <guid>https://dev.to/ai-router/kak-bystro-provierit-chatgpt-api-v-sushchiestvuiushchiem-openai-kliientie-1jjn</guid>
      <description>&lt;p&gt;Если у вас уже есть приложение или скрипт с OpenAI-compatible клиентом, для первого теста часто достаточно заменить endpoint и ключ, не переписывая всю интеграцию.&lt;/p&gt;

&lt;p&gt;Практический маршрут:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Зарегистрировать аккаунт.&lt;/li&gt;
&lt;li&gt;Выбрать небольшой дневной или недельный пакет либо пополнить баланс.&lt;/li&gt;
&lt;li&gt;Создать собственный API key.&lt;/li&gt;
&lt;li&gt;Указать endpoint и key в клиенте согласно документации.&lt;/li&gt;
&lt;li&gt;Выполнить тестовый запрос и проверить расход в кабинете.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Начните с небольшого объёма: так проще проверить именно ваш сценарий и фактическое потребление. Документация с примером первого запроса: &lt;a href="https://ai-router.dev/ru/docs/getting-started/first-request/?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=ru_activation_2026q3" rel="noopener noreferrer"&gt;https://ai-router.dev/ru/docs/getting-started/first-request/?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=ru_activation_2026q3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI-ROUTER — сторонний сервис-посредник для ChatGPT API с OpenAI-compatible способом вызова. Это не официальный сервис OpenAI. Доступные модели, лимиты, сроки действия пакетов и условия указаны на актуальных страницах сервиса.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>OpenAI-compatible API: как проверить endpoint, лимиты и первый запрос</title>
      <dc:creator>AI ROUTER</dc:creator>
      <pubDate>Fri, 28 Aug 2026 04:57:49 +0000</pubDate>
      <link>https://dev.to/ai-router/openai-compatible-api-kak-provierit-endpoint-limity-i-piervyi-zapros-2c3k</link>
      <guid>https://dev.to/ai-router/openai-compatible-api-kak-provierit-endpoint-limity-i-piervyi-zapros-2c3k</guid>
      <description>&lt;h2&gt;
  
  
  Короткий путь к первому запросу
&lt;/h2&gt;

&lt;p&gt;При подключении ChatGPT API к прототипу чаще всего ломается не модель, а конфигурация: лишний &lt;code&gt;/v1&lt;/code&gt;, неверный model ID, потерянный заголовок Authorization или неожиданный лимит тарифа.&lt;/p&gt;

&lt;p&gt;Ниже — небольшой чек-лист для Python, Node.js, curl и других клиентов с OpenAI-compatible интерфейсом.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Зафиксируйте четыре значения
&lt;/h3&gt;

&lt;p&gt;Перед запуском запишите:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;базовый URL;&lt;/li&gt;
&lt;li&gt;имя переменной с API key;&lt;/li&gt;
&lt;li&gt;точный model ID из доступного каталога;&lt;/li&gt;
&lt;li&gt;квоту и срок действия выбранного тарифа.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Если библиотека сама добавляет &lt;code&gt;/chat/completions&lt;/code&gt; и &lt;code&gt;/models&lt;/code&gt;, ей обычно нужен корневой адрес:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.ai-router.dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Если клиент ожидает OpenAI SDK Base URL и сам формирует &lt;code&gt;/v1/chat/completions&lt;/code&gt;, используйте:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.ai-router.dev/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Не добавляйте &lt;code&gt;/v1&lt;/code&gt; дважды: путь &lt;code&gt;/v1/v1/chat/completions&lt;/code&gt; даст 404 даже при правильном ключе.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Проверьте каталог и авторизацию
&lt;/h3&gt;

&lt;p&gt;Храните ключ только в переменной окружения или секрет-хранилище:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AI_ROUTER_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"замените-на-свой-ключ"&lt;/span&gt;

curl https://api.ai-router.dev/models &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AI_ROUTER_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Смотрите на фактическое поле &lt;code&gt;data[].id&lt;/code&gt;. Не угадывайте имя модели по названию тарифа и не вставляйте ключ в репозиторий, issue, URL или frontend bundle.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Сделайте минимальный smoke-test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.ai-router.dev/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AI_ROUTER_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "gpt-5.4-mini",
    "messages": [
      {"role": "user", "content": "Ответь одним предложением: соединение работает?"}
    ],
    "max_tokens": 64
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;В рабочем приложении используйте model ID из каталога, доступного именно вашему ключу и тарифу.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Обрабатывайте ошибки по классам
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;401&lt;/strong&gt; — проверьте ключ и заголовок Authorization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;404&lt;/strong&gt; — сравните фактический URL с правилом &lt;code&gt;/v1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;429&lt;/strong&gt; — проверьте квоту, баланс, RPM/TPM и Retry-After; ограничьте параллелизм, а не запускайте бесконечный retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Обрыв stream&lt;/strong&gt; — не доказывает, что запрос не был принят. Повторяйте только операции, для которых replay действительно безопасен.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;После первого запроса откройте usage, баланс и срок действия в кабинете. Начинайте с небольшого дневного или недельного объёма, чтобы проверить именно свой workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Что проверить перед production
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Каталог возвращает ожидаемый model ID.&lt;/li&gt;
&lt;li&gt;Короткий запрос завершается успешным ответом.&lt;/li&gt;
&lt;li&gt;401/404/429 попадают в понятные метрики.&lt;/li&gt;
&lt;li&gt;Таймауты и обрывы не вызывают небезопасные повторы.&lt;/li&gt;
&lt;li&gt;Для разных приложений используются отдельные ключи.&lt;/li&gt;
&lt;li&gt;Квота и срок тарифа соответствуют реальной нагрузке.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AI-ROUTER в этом примере — независимый ChatGPT API relay с OpenAI-compatible способом вызова, а не официальный сервис OpenAI. Актуальные русскоязычные примеры endpoint и первого запроса: &lt;a href="https://ai-router.dev/ru/docs/getting-started/first-request/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=ru_api_first_request_202608" rel="noopener noreferrer"&gt;https://ai-router.dev/ru/docs/getting-started/first-request/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=ru_api_first_request_202608&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Если у вас есть другой совместимый клиент, полезно начать с тех же двух проверок: какой URL он формирует и какой model ID реально видит ваш API key.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Safe Retries for OpenAI-Compatible APIs: HTTP Errors, Retry-After, and Partial SSE Output</title>
      <dc:creator>AI ROUTER</dc:creator>
      <pubDate>Tue, 28 Jul 2026 11:17:56 +0000</pubDate>
      <link>https://dev.to/ai-router/safe-retries-for-openai-compatible-apis-http-errors-retry-after-and-partial-sse-output-3l7p</link>
      <guid>https://dev.to/ai-router/safe-retries-for-openai-compatible-apis-http-errors-retry-after-and-partial-sse-output-3l7p</guid>
      <description>&lt;p&gt;Retrying an API request sounds simple until the request generates text, invokes a tool, or consumes billable model capacity. A disconnected &lt;code&gt;POST /v1/chat/completions&lt;/code&gt; is different from a failed &lt;code&gt;GET /v1/models&lt;/code&gt;: the server may have accepted and executed the generation before the client saw an error. A stream that has already emitted output is different again, because replaying the whole request can duplicate visible text, tool calls, side effects, or cost.&lt;/p&gt;

&lt;p&gt;This guide builds a conservative error and retry boundary for OpenAI-compatible APIs in Node.js and TypeScript. It covers four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What kind of failure occurred?&lt;/li&gt;
&lt;li&gt;What diagnostic data can be logged without retaining request or response bodies?&lt;/li&gt;
&lt;li&gt;Did a streaming response already produce meaningful output?&lt;/li&gt;
&lt;li&gt;Does the caller know that replay is safe within its attempt and time budgets?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is not to build another API client. It is to make the decision inputs explicit before any retry loop runs.&lt;/p&gt;

&lt;p&gt;The examples use the published &lt;a href="https://www.npmjs.com/package/@ai-router/openai-compatible-errors" rel="noopener noreferrer"&gt;&lt;code&gt;@ai-router/openai-compatible-errors@0.1.0&lt;/code&gt;&lt;/a&gt;. Its source, tests, and release workflow are public on &lt;a href="https://github.com/airouter-dev/openai-compatible-errors" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;retryable: true&lt;/code&gt; is incomplete
&lt;/h2&gt;

&lt;p&gt;Most retry logic starts with status codes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retry 429 because it indicates rate limiting.&lt;/li&gt;
&lt;li&gt;Retry 502, 503, and 504 because an upstream service may recover.&lt;/li&gt;
&lt;li&gt;Retry a transport timeout because the network may recover.&lt;/li&gt;
&lt;li&gt;Do not retry 400 or 401 because the request or credential needs correction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are useful error classifications, but they do not establish replay safety.&lt;/p&gt;

&lt;p&gt;Consider a generation &lt;code&gt;POST&lt;/code&gt; that times out while awaiting response headers. Three different events can look identical to the caller:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The request never left the process.&lt;/li&gt;
&lt;li&gt;The request reached a proxy, but the proxy never sent it upstream.&lt;/li&gt;
&lt;li&gt;The model completed the request, while the response was lost on the return path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only the first case is obviously safe to replay. A generic network error often cannot distinguish the three. Returning &lt;code&gt;retryable: true&lt;/code&gt; collapses “the failure may be transient” and “repeating the operation is safe” into one dangerous bit.&lt;/p&gt;

&lt;p&gt;A safer model separates them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Transience&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;permanent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ReplaySafety&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;safe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unsafe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RetryAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;do_not_retry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_decision&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An automated retry should require both a transient error and explicit replay safety. Unknown evidence should stay unknown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify the error before planning a retry
&lt;/h2&gt;

&lt;p&gt;OpenAI-compatible services can expose failure evidence in several places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP status: 401, 403, 408, 413, 429, 5xx.&lt;/li&gt;
&lt;li&gt;JSON body: &lt;code&gt;error.message&lt;/code&gt;, &lt;code&gt;error.type&lt;/code&gt;, &lt;code&gt;error.code&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Headers: &lt;code&gt;Retry-After&lt;/code&gt;, &lt;code&gt;x-request-id&lt;/code&gt;, &lt;code&gt;request-id&lt;/code&gt;, correlation IDs.&lt;/li&gt;
&lt;li&gt;SDK error: &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;statusCode&lt;/code&gt;, &lt;code&gt;request_id&lt;/code&gt;, &lt;code&gt;responseHeaders&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Transport exception: &lt;code&gt;AbortError&lt;/code&gt;, timeout, DNS, connection reset.&lt;/li&gt;
&lt;li&gt;SSE event: &lt;code&gt;{ "error": ... }&lt;/code&gt; or a Responses-style &lt;code&gt;response.failed&lt;/code&gt; event inside an HTTP 200 stream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Status alone is not always enough. A 429 can mean a temporary request-rate limit or an exhausted quota that will not recover after a two-second delay. Check structured error codes and types before assigning the category.&lt;/p&gt;

&lt;p&gt;A useful taxonomy can remain small:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Transience&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;401, invalid API key&lt;/td&gt;
&lt;td&gt;Permanent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission&lt;/td&gt;
&lt;td&gt;403, access denied&lt;/td&gt;
&lt;td&gt;Permanent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit&lt;/td&gt;
&lt;td&gt;429 + rate-limit signal&lt;/td&gt;
&lt;td&gt;Transient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quota&lt;/td&gt;
&lt;td&gt;insufficient quota, billing/credit signal&lt;/td&gt;
&lt;td&gt;Permanent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;400/409/422&lt;/td&gt;
&lt;td&gt;Permanent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;408, timeout exception&lt;/td&gt;
&lt;td&gt;Transient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;DNS, connection reset, Fetch failure&lt;/td&gt;
&lt;td&gt;Transient but replay may be unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upstream&lt;/td&gt;
&lt;td&gt;502/503/504&lt;/td&gt;
&lt;td&gt;Transient but replay may be unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream&lt;/td&gt;
&lt;td&gt;premature EOF&lt;/td&gt;
&lt;td&gt;Transient only before output and when replay is safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;insufficient evidence&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The category should be a stable application contract. Keep raw vendor strings out of that contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the decision without an API key
&lt;/h2&gt;

&lt;p&gt;The following fixture runs entirely on loopback. It returns a synthetic 429 with &lt;code&gt;Retry-After: 2&lt;/code&gt;, then evaluates the same error twice: once when replay is explicitly safe, and once when replay safety is unknown.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @ai-router/openai-compatible-errors@0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;once&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:events&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;decideOpenAICompatibleRetry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;normalizeOpenAICompatibleResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@ai-router/openai-compatible-errors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retry-after&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;req_loopback_demo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rate_limit_error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rate_limit_exceeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Synthetic fixture; excluded from the result by default.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;listening&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;address&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://127.0.0.1:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/demo`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;normalizeOpenAICompatibleResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Expected a synthetic API error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http_error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;safePlan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;decideOpenAICompatibleRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;replaySafety&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;safe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Safe only for this side-effect-free fixture.&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;unknownPlan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;decideOpenAICompatibleRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;replaySafety&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The safe result is &lt;code&gt;retry&lt;/code&gt; with a 2,000 ms server-directed delay. The unknown result is &lt;code&gt;manual_decision&lt;/code&gt;, even though both plans are based on the same transient 429. A complete, defensively checked copy of this fixture is available in the &lt;a href="https://github.com/airouter-dev/openai-compatible-errors/blob/main/examples/loopback-rate-limit.mjs" rel="noopener noreferrer"&gt;repository examples&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build diagnostics that are safe by construction
&lt;/h2&gt;

&lt;p&gt;Logging the original SDK error is convenient, but it can retain more than a status and message. Depending on the SDK and wrapper, the object may contain full headers, request body values, response text, nested causes, or a stack whose message includes provider text.&lt;/p&gt;

&lt;p&gt;Redaction after the fact is fragile. A stronger default is to construct a new error object that never stores those fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SafeApiError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OpenAICompatibleError&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// A fixed message owned by the normalizer.&lt;/span&gt;
  &lt;span class="nl"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai_sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai_sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;code&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;retryAfterMs&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default &lt;code&gt;message&lt;/code&gt; should be something predictable such as &lt;code&gt;Rate limit exceeded&lt;/code&gt;, not a copy of &lt;code&gt;error.message&lt;/code&gt;. Provider messages can echo an API key, prompt fragment, file name, customer identifier, or upstream HTML. If a provider message is genuinely needed, make it an explicit opt-in, redact common credential forms, truncate it, and document that secret redaction is not PII anonymization.&lt;/p&gt;

&lt;p&gt;For arbitrary diagnostic objects, use a bounded sanitizer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never invoke user-defined getters or &lt;code&gt;toJSON&lt;/code&gt; hooks while traversing unknown objects.&lt;/li&gt;
&lt;li&gt;Detect circular references.&lt;/li&gt;
&lt;li&gt;Limit depth, node count, array items, object keys, and string length.&lt;/li&gt;
&lt;li&gt;Replace oversized strings rather than preserving a prefix that might contain a partial secret.&lt;/li&gt;
&lt;li&gt;Redact authorization, API-key, cookie, password, token, prompt, message-list, and request/response body fields.&lt;/li&gt;
&lt;li&gt;Redact Bearer/Basic values, known key shapes, JWTs, URL userinfo, and sensitive query parameters in free text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even a strong sanitizer is defense in depth. The best way to protect a prompt is not to send it to the logger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honor &lt;code&gt;Retry-After&lt;/code&gt; without shortening it
&lt;/h2&gt;

&lt;p&gt;HTTP defines &lt;code&gt;Retry-After&lt;/code&gt; as either a delay in seconds or an HTTP date. Some SDKs and gateways also expose a millisecond hint such as &lt;code&gt;retry-after-ms&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseRetryAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nowMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trimmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetMs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetMs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not clamp a one-hour server delay down to a local ten-second maximum and retry early. If the server hint exceeds the caller's single-delay or total-time budget, the safe result is to stop retrying. A local maximum is a budget, not permission to ignore the server's lower-bound advice.&lt;/p&gt;

&lt;p&gt;When there is no valid server hint, capped exponential backoff with jitter is reasonable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseDelayMs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;maxDelayMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delayMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Full jitter.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the attempt budget and elapsed-time budget before returning a retry plan. Count the initial request as attempt one so the policy is unambiguous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat SSE output as a replay boundary
&lt;/h2&gt;

&lt;p&gt;Server-sent events are framed as lines. Multiple &lt;code&gt;data:&lt;/code&gt; lines form one event, events end with a blank line, and a UTF-8 character or JSON object can be split across arbitrary network chunks. Searching each chunk for &lt;code&gt;"error"&lt;/code&gt; is therefore not a parser.&lt;/p&gt;

&lt;p&gt;A stream inspector needs incremental state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;StreamState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;eventsSeen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;malformedEvents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;hasOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;unexpectedEof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;SafeApiError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Chat Completions, meaningful output includes non-empty &lt;code&gt;choices[].delta.content&lt;/code&gt;, tool-call deltas, or function-call arguments. For Responses-style streams, output text or function-call argument delta events cross the same boundary.&lt;/p&gt;

&lt;p&gt;Once &lt;code&gt;hasOutput&lt;/code&gt; is true, a whole-request retry should default to &lt;code&gt;do_not_retry&lt;/code&gt;. This remains true even if the terminal error is a normally transient 429 or 503. Transience describes the failure; it does not erase partial output.&lt;/p&gt;

&lt;p&gt;An early SSE error can arrive inside an HTTP 200 response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event: error
data: {"error":{"code":"rate_limit_exceeded","message":"..."}}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error contract should normalize this event the same way as a non-2xx JSON body while recording &lt;code&gt;source: "sse"&lt;/code&gt;. It should not retain the raw event.&lt;/p&gt;

&lt;p&gt;Premature EOF also needs an explicit state. If the stream emitted data but never sent &lt;code&gt;[DONE]&lt;/code&gt; or a completion event, mark &lt;code&gt;unexpectedEof&lt;/code&gt;. Whether that is retryable still depends on &lt;code&gt;hasOutput&lt;/code&gt; and replay safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fail-closed retry decision
&lt;/h2&gt;

&lt;p&gt;The decision order matters. Evaluate the strongest reasons to stop first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Caller abort: do not retry.&lt;/li&gt;
&lt;li&gt;Partial SSE output: do not retry the whole request.&lt;/li&gt;
&lt;li&gt;Completed request: do not retry.&lt;/li&gt;
&lt;li&gt;Permanent error: do not retry.&lt;/li&gt;
&lt;li&gt;Replay explicitly unsafe: do not retry.&lt;/li&gt;
&lt;li&gt;Unknown phase or replay safety: require a manual decision.&lt;/li&gt;
&lt;li&gt;Unknown error category: require a manual decision.&lt;/li&gt;
&lt;li&gt;Attempt or time budget exhausted: do not retry.&lt;/li&gt;
&lt;li&gt;Server delay exceeds the budget: do not retry; never shorten it.&lt;/li&gt;
&lt;li&gt;Only now may a transient, replay-safe failure return &lt;code&gt;retry&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In code with &lt;code&gt;@ai-router/openai-compatible-errors&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;decideOpenAICompatibleRetry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;normalizeOpenAICompatibleResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@ai-router/openai-compatible-errors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;normalizeOpenAICompatibleResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decideOpenAICompatibleRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http_error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;replaySafety&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delayMs&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Caller may schedule a retry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delayMs&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Do not automatically replay this request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library never schedules the retry. Keeping execution outside the package makes rate budgets, cancellation, observability, and endpoint-specific semantics visible to the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should set &lt;code&gt;replaySafety&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;The method is evidence, not the complete answer. GET and HEAD are commonly designed to be replay-safe, while a generation POST is often ambiguous. An idempotency key helps only when the endpoint contract says how the key is scoped, retained, and applied to the operation. The mere presence of a header called &lt;code&gt;Idempotency-Key&lt;/code&gt; is not proof.&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;replaySafety: "safe"&lt;/code&gt; only when the application owns enough evidence, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The operation is read-only and the endpoint contract supports replay.&lt;/li&gt;
&lt;li&gt;The service documents idempotent behavior for this exact operation and key.&lt;/li&gt;
&lt;li&gt;The failure occurred before the request could be sent, and the request phase is trustworthy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;code&gt;unknown&lt;/code&gt; when a proxy, timeout, or connection reset makes acceptance ambiguous. Use &lt;code&gt;unsafe&lt;/code&gt; after observable side effects or when replay can duplicate user-visible work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation checklist
&lt;/h2&gt;

&lt;p&gt;Before trusting an error boundary, test behavior rather than descriptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;429 transient rate limit versus 429 exhausted quota.&lt;/li&gt;
&lt;li&gt;Retry-After seconds, HTTP date, invalid value, past date, and value beyond budget.&lt;/li&gt;
&lt;li&gt;401, 403, 413, 422, 502, 503, and 504.&lt;/li&gt;
&lt;li&gt;Loopback raw Fetch and pinned OpenAI Node SDK client catch paths, plus OpenAI/AI SDK-style structural errors.&lt;/li&gt;
&lt;li&gt;A raw Fetch &lt;code&gt;Response&lt;/code&gt; whose original body remains unread.&lt;/li&gt;
&lt;li&gt;A body that is text or HTML rather than JSON.&lt;/li&gt;
&lt;li&gt;Error messages containing synthetic API keys, Bearer tokens, JWTs, URL credentials, prompts, and response fragments.&lt;/li&gt;
&lt;li&gt;Circular objects, throwing getters, revoked proxies, deep arrays, and oversized strings.&lt;/li&gt;
&lt;li&gt;SSE frames split across bytes inside a UTF-8 character.&lt;/li&gt;
&lt;li&gt;An error before output, an error after output, malformed JSON, &lt;code&gt;[DONE]&lt;/code&gt;, and premature EOF.&lt;/li&gt;
&lt;li&gt;Attempt, elapsed-time, and per-delay budget boundaries.&lt;/li&gt;
&lt;li&gt;ESM, CommonJS, and TypeScript declaration imports from the packed tarball.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fixtures should use synthetic canaries, never copied production credentials or customer prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a new dependency is not justified
&lt;/h2&gt;

&lt;p&gt;Use the error types already provided by the official OpenAI SDK when one SDK and endpoint cover your application. Use a multi-provider normalizer when your primary problem is one taxonomy across OpenAI, Anthropic, Gemini, and other native APIs. Use provider utilities when you are implementing an AI SDK provider and already depend on that stack.&lt;/p&gt;

&lt;p&gt;Add a separate boundary only when you need its narrower guarantees: default body-free diagnostics, multiple OpenAI-compatible error shapes, SSE output tracking, and an explicit replay-safety decision. Otherwise, another package increases maintenance without reducing risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-retry-after" rel="noopener noreferrer"&gt;HTTP Semantics: Retry-After&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://html.spec.whatwg.org/multipage/server-sent-events.html" rel="noopener noreferrer"&gt;HTML Living Standard: Server-sent events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/openai-node#handling-errors" rel="noopener noreferrer"&gt;OpenAI Node SDK error handling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@ai-sdk/openai-compatible" rel="noopener noreferrer"&gt;&lt;code&gt;@ai-sdk/openai-compatible&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/llm-errors" rel="noopener noreferrer"&gt;&lt;code&gt;llm-errors&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install the package from &lt;a href="https://www.npmjs.com/package/@ai-router/openai-compatible-errors" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, inspect the &lt;a href="https://github.com/airouter-dev/openai-compatible-errors" rel="noopener noreferrer"&gt;public source and test matrix&lt;/a&gt;, or report a suspected security issue through the repository's &lt;a href="https://github.com/airouter-dev/openai-compatible-errors/security/advisories/new" rel="noopener noreferrer"&gt;private advisory form&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;AI ROUTER maintains &lt;code&gt;@ai-router/openai-compatible-errors&lt;/code&gt; as an independent, provider-neutral developer utility. It is not affiliated with or endorsed by OpenAI.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>api</category>
      <category>security</category>
    </item>
    <item>
      <title>Point an OpenAI-compatible SDK at a ChatGPT API relay</title>
      <dc:creator>AI ROUTER</dc:creator>
      <pubDate>Sun, 05 Jul 2026 10:43:18 +0000</pubDate>
      <link>https://dev.to/ai-router/point-an-openai-compatible-sdk-at-a-chatgpt-api-relay-198n</link>
      <guid>https://dev.to/ai-router/point-an-openai-compatible-sdk-at-a-chatgpt-api-relay-198n</guid>
      <description>&lt;p&gt;When a project already uses the OpenAI SDK shape, the lowest-friction way to test a relay service is to change two things only:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the API base URL&lt;/li&gt;
&lt;li&gt;the API key&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AI ROUTER provides an OpenAI-compatible ChatGPT API relay endpoint at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.ai-router.dev/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means a small prototype, coding-agent workflow, internal tool, or automation script can keep the same request pattern while using a relay account that exposes API key management, usage tracking, quota, balance, and subscription status.&lt;/p&gt;

&lt;h2&gt;
  
  
  cURL smoke test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.ai-router.dev/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AI_ROUTER_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "gpt-5.4-mini",
    "messages": [
      { "role": "user", "content": "Reply with a short API smoke test." }
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AI_ROUTER_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.ai-router.dev/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write one test sentence.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Node.js
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AI_ROUTER_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.ai-router.dev/v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-5.4-mini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Write one test sentence.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What to check before scaling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Confirm the model name is available in your account.&lt;/li&gt;
&lt;li&gt;Start with a small daily or weekly quota plan.&lt;/li&gt;
&lt;li&gt;Watch API key usage, balance, quota, and subscription status from the dashboard.&lt;/li&gt;
&lt;li&gt;Keep retry and timeout handling in your application code.&lt;/li&gt;
&lt;li&gt;Avoid treating any relay as an official OpenAI service unless the provider explicitly says so.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also published curl, Python, and Node.js examples here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/airouter-dev/chatgpt-api-relay-examples" rel="noopener noreferrer"&gt;https://github.com/airouter-dev/chatgpt-api-relay-examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Product page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ai-router.dev" rel="noopener noreferrer"&gt;https://ai-router.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>chatgpt</category>
      <category>llm</category>
      <category>openai</category>
    </item>
  </channel>
</rss>
