Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,26 @@ except PaymentRequiredError as exc:
`TrialExhaustedError` subclasses `PaymentRequiredError`, so an existing
`except PaymentRequiredError` keeps catching every 402 — order the handlers
most-specific-first if you want to tell them apart.

### The two 429s

`RateLimitError` covers two separate limits that want opposite handling.
The class and `.code` (`rate_limit_exceeded`) are identical for both — only
the message tells them apart:

- **Requests per minute** — `Rate limit exceeded: your account allows 60
requests per minute. Rejected requests count toward the limit too, so wait
for the next minute window (up to 60 sec) rather than retrying right away.
To raise your limit, contact info@sonilo.com.` Calls are going out too fast.
The counter runs on a fixed 60-second window, so back off past the window
boundary instead of retrying inside it.
- **Concurrent generations** — `Too many concurrent generations: 5 of 5 in
progress. Wait for one to finish before starting another. To raise your
limit, contact info@sonilo.com.` Every generation slot is busy. Waiting
alone frees nothing — retry when one of your own in-flight generations
finishes, not on a timer.

The numbers are the account's own limits; `account.services()` reports them
as `rpm_limit` and `concurrency_limit`. Email info@sonilo.com to raise
either. `.retry_after` is set only when the server sends a `Retry-After`
header, so treat it as a hint rather than something to depend on.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "sonilo"
version = "0.11.0"
version = "0.11.1"
description = "Official Python client for the Sonilo API"
readme = "README.md"
license = "MIT"
Expand Down
16 changes: 16 additions & 0 deletions sonilo-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,19 @@ Because the summary is on stderr, `sonilo account | jq .trial` still sees clean
Once an endpoint's free runs are used up, calls to it bill at the normal rate — or, if the account
has never been funded, fail with `HTTP 402: ... (trial_exhausted)` until a payment method is added.
That is the one 402 a retry can never fix.

## Rate limits

Two separate limits return `HTTP 429`, and they want opposite handling. The CLI prints the API's
own sentence, so the wording says which one you hit:

sonilo: HTTP 429: Rate limit exceeded: your account allows 60 requests per minute. Rejected requests count toward the limit too, so wait for the next minute window (up to 60 sec) rather than retrying right away. To raise your limit, contact info@sonilo.com. (rate_limit_exceeded)
sonilo: HTTP 429: Too many concurrent generations: 5 of 5 in progress. Wait for one to finish before starting another. To raise your limit, contact info@sonilo.com. (rate_limit_exceeded)

The first means calls are going out too fast. The counter runs on a fixed 60-second window and
rejected calls count toward it too, so wait the window out instead of retrying inside it. The
second means every generation slot is busy — waiting alone frees nothing, a running generation has
to finish first.

`sonilo account` prints the account's own `rpm_limit` and `concurrency_limit`; the numbers above
are the standard-tier defaults. Email info@sonilo.com to raise either.
2 changes: 1 addition & 1 deletion sonilo-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "sonilo-cli"
version = "0.8.0"
version = "0.8.1"
description = "Command-line interface for the Sonilo API: generate music and sound effects from text or video"
readme = "README.md"
license = "MIT"
Expand Down
24 changes: 24 additions & 0 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,30 @@ def test_429_without_header_has_no_retry_after():
assert err.retry_after is None


# Two limits share this status and want opposite handling — slow down, or wait
# for a running generation to finish. The code is identical for both, so the
# message is the only thing that tells them apart: it has to survive whole,
# numbers and contact address included.
@pytest.mark.parametrize(
"message",
[
"Rate limit exceeded: your account allows 60 requests per minute. "
"Rejected requests count toward the limit too, so wait for the next "
"minute window (up to 60 sec) rather than retrying right away. "
"To raise your limit, contact info@sonilo.com.",
"Too many concurrent generations: 5 of 5 in progress. Wait for one to "
"finish before starting another. To raise your limit, contact "
"info@sonilo.com.",
],
)
def test_429_carries_the_message_through_verbatim(message):
err = error_from_response(
make_response(429, {"code": "rate_limit_exceeded", "message": message})
)
assert isinstance(err, RateLimitError)
assert str(err) == f"HTTP 429: {message}"


@pytest.mark.parametrize("status", [400, 413, 422])
def test_4xx_maps_to_bad_request_with_legacy_detail(status):
err = error_from_response(make_response(status, {"detail": "bad input"}))
Expand Down
Loading