A 429 means the provider rejected the request because of rate or capacity
limits. It does not by itself mean your model or server address is wrong.
Azure can report no_capacity even when both addresses support the model.
lm15 keeps the provider's message and adds useful evidence when available:
from lm15 import RateLimitError
try:
response = router.complete(request)
except RateLimitError as error:
print(error) # message, request ID, wait advice, limits
print(error.retry_after) # seconds, or None if no usable advice
print(error.request_id) # reference for provider support
print(dict(error.rate_limit_headers))
# Your application decides whether and when to retry.For the Azure failure that prompted this feature, the headers included:
x-ratelimit-limit-requests: 1
x-ratelimit-remaining-requests: -1
x-ratelimit-reset-requests: 105
retry-after: 39
These numbers disagree with a simple “wait exactly one minute” assumption. They are the provider's reports, not a quota calculation performed by lm15. In our test, longer gaps restored success on both Azure addresses.
retry_after: usable provider advice in seconds. A valid error-body value wins, thenRetry-After, then numericalretry-after-msorx-ms-retry-after-ms. No valid advice meansNone, not zero.rate_limit_headers: a read-only mapping of lowercase header names to tuples of original values. Multiple values and negative balances stay visible. Reset values stay unconverted: providers use seconds, duration strings, or timestamps. Do not treat them all as a number of seconds.request_id: the provider's reference, including Azure'sapim-request-idfallback, useful when reporting an issue.
Wait advice is not a guarantee of success. Neither this feature nor the
error's retryable classification adds automatic retries, increases quota,
purchases capacity, or changes the endpoint. Use bounded retries in your
application when appropriate; model calls can have costs and side effects.
The same diagnostics work for normal calls, async calls, and auxiliary
operations such as file upload or model listing. A rejected stream carries
metadata on the exception. An error inside HTTP 200 carries handshake
evidence in event.error.http_response; saving/reloading that event preserves
it, and response materialization carries it onto the exception. HTTP 200 is
not assigned as the error's status. Handshake headers are not a live quota
feed and may be older than the stream failure.
Only a documented set of rate-limit headers is retained—not cookies,
credentials, arbitrary headers, or x-ratelimit-key. Values are bounded to
four per name and 256 printable ASCII characters each; malformed/oversized
values are omitted, not turned into plausible numbers. Long error displays
show a bounded preview while retained values remain available on the error.
Provider error content still belongs under your application's logging policy.
The cross-language rules and exact header list live in lm15-contract/docs/error-diagnostics.md.