HyphenHyphen
HomeConsole
  • Get Started
  • Connect your coding agent
  • Build with Hyphen
  • Chat UIs & automation
  • Recipes
  • Guides
  • API Reference
Streaming chatTool callingStructured JSONAgent loopHandling 429Long context
powered by Zudoku
Recipes

Handling the 429 cap

Hyphen returns HTTP 429 for two different reasons and they need opposite responses.

KindMeaningWhat to do
BudgetMonthly cap spent on this key.Stop retrying. Switch keys or wait.
Fair useToo many requests or tokens per minute.Back off a few seconds and retry.

Retrying a budget 429 is pointless. It will keep failing until the window resets, and every attempt is wasted latency. Tell them apart before you retry.

What a budget 429 looks like

Code
HTTP/1.1 429 Too Many Requests Retry-After: 1209600 Content-Type: application/json
Code
{ "error": { "message": "Budget has been exceeded! Monthly budget resets on 2026-08-01.", "type": "budget_exceeded", "code": "429" } }

type is budget_exceeded and the reset date is in the message. A fair-use 429 has neither, and its Retry-After is seconds, not weeks.

The full handler

Detects the kind, reads the reset date, backs off on fair-use limits, and falls through to a prepaid credit key when the subscription key is spent.

Code
import os import random import re import time from datetime import date from openai import OpenAI, APIStatusError BASE_URL = "https://api.hyphen-solution.com/v1" PRIMARY = os.environ["HYPHEN_API_KEY"] # subscription key, monthly cap CREDIT = os.environ.get("HYPHEN_CREDIT_KEY") # prepaid credit key, optional def error_body(err: APIStatusError) -> dict: try: return err.response.json().get("error", {}) or {} except Exception: return {} def is_budget_error(err: APIStatusError) -> bool: body = error_body(err) if body.get("type") == "budget_exceeded": return True # Belt and braces: match the message if the type field ever moves. return "budget has been exceeded" in str(body.get("message", "")).lower() def reset_date(err: APIStatusError) -> date | None: match = re.search(r"(\d{4})-(\d{2})-(\d{2})", str(error_body(err).get("message", ""))) return date(*(int(g) for g in match.groups())) if match else None def backoff_seconds(err: APIStatusError, attempt: int) -> float: """Prefer the server's Retry-After. Fall back to exponential plus jitter.""" header = err.response.headers.get("retry-after") if header and header.isdigit(): return min(float(header), 60.0) return min(2**attempt, 30) + random.random() def complete(messages, model="minimax-m3", max_attempts=5): keys = [("subscription", PRIMARY)] + ([("credit", CREDIT)] if CREDIT else []) for label, key in keys: client = OpenAI(base_url=BASE_URL, api_key=key) for attempt in range(max_attempts): try: return client.chat.completions.create( model=model, messages=messages, max_tokens=2000 ) except APIStatusError as err: if err.status_code != 429: raise if is_budget_error(err): when = reset_date(err) print(f"{label} key: budget spent, resets {when or 'unknown'}") break # retrying this key will not help. Next key. wait = backoff_seconds(err, attempt) print(f"{label} key: fair-use limit, retrying in {wait:.1f}s") time.sleep(wait) raise RuntimeError("every key is capped or rate limited") print(complete([{"role": "user", "content": "Say hi."}]).choices[0].message.content)

Run it with both keys exported:

TerminalCode
export HYPHEN_API_KEY="sk-...subscription..." export HYPHEN_CREDIT_KEY="sk-...credit..."

If HYPHEN_CREDIT_KEY is unset the helper just raises when the subscription key caps. That is the correct behaviour for a script you do not want spending money you did not budget.

Node version

Code
import OpenAI from "openai"; const BASE_URL = "https://api.hyphen-solution.com/v1"; const keys = [process.env.HYPHEN_API_KEY, process.env.HYPHEN_CREDIT_KEY].filter(Boolean); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); function isBudgetError(err) { const body = err?.error?.error ?? err?.error ?? {}; return ( body.type === "budget_exceeded" || String(body.message ?? "").toLowerCase().includes("budget has been exceeded") ); } function resetDate(err) { const body = err?.error?.error ?? err?.error ?? {}; return String(body.message ?? "").match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? null; } export async function complete(messages, model = "minimax-m3", maxAttempts = 5) { for (const apiKey of keys) { const client = new OpenAI({ baseURL: BASE_URL, apiKey, maxRetries: 0 }); for (let attempt = 0; attempt < maxAttempts; attempt++) { try { return await client.chat.completions.create({ model, messages, max_tokens: 2000 }); } catch (err) { if (err.status !== 429) throw err; if (isBudgetError(err)) { console.warn(`budget spent, resets ${resetDate(err) ?? "unknown"}`); break; } const retryAfter = Number(err.headers?.get?.("retry-after")); const wait = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 60) * 1000 : Math.min(2 ** attempt, 30) * 1000 + Math.random() * 1000; console.warn(`rate limited, retrying in ${Math.round(wait)}ms`); await sleep(wait); } } } throw new Error("every key is capped or rate limited"); }

maxRetries: 0 turns off the SDK's own retry logic. Leave it on and the SDK will silently retry budget 429s for you, which wastes seconds and hides the real error.

Checking the cap before you start

Cheaper than finding out mid-run. Send one tiny request and look at the status.

Code
from openai import OpenAI, APIStatusError def key_is_live(key: str) -> bool: client = OpenAI(base_url="https://api.hyphen-solution.com/v1", api_key=key, max_retries=0) try: client.chat.completions.create( model="minimax-m2.5", messages=[{"role": "user", "content": "hi"}], max_tokens=2000, ) return True except APIStatusError as err: return err.status_code != 429

Use minimax-m2.5 for the probe. Same rate as the flagship, and it is the speed-and-volume tier.

Things not to do

  • Do not retry a budget 429 in a tight loop. It will not clear until the window resets.
  • Do not treat 429 as fatal without checking the type. A fair-use 429 is routine and clears in seconds.
  • Do not shrink max_tokens to save budget. It truncates the answer without reducing reasoning cost. Trim the input instead. See Choosing a model.
  • Do not swallow the reset date. Surface it. Your users need to know when the thing comes back.

Related

  • Rate limits & caps: how the budget works.
  • A small agent loop: where this helper belongs.
  • Models: the rate card, so you know what is spending your cap.
Last modified on July 28, 2026
Agent loopLong context
On this page
  • What a budget 429 looks like
  • The full handler
  • Node version
  • Checking the cap before you start
  • Things not to do
  • Related
JSON
Javascript