feat(gateway): rate limits, retries, breakers and fallback for Bud voice deployments - #10
Merged
Merged
Conversation
…ice deployments
A Bud voice deployment's Rate limiting and Resilience settings (FRD-022) now apply in WaaV,
using the resil crate budgateway uses (pinned by rev, redis 0.27 adapter).
- bud-auth: VoiceEndpoint carries the deployment policy parsed from voice_table, leniently
(a bad policy block drops the policy, never the endpoint); contract fixture updated.
- Rate limits and max_concurrent hold cluster-wide across replicas: each replica admits from
credit reserved in Redis, with no Redis call per request. 429s are JSON with Retry-After and
X-RateLimit-* headers.
- /v1/audio/speech and /v1/audio/transcriptions retry retryable vendor failures and fail over
along the deployment's fallback chain. Each fallback has its own limits. Two-tier breakers
cover the deployment and the vendor, and a vendor 429 carrying Retry-After opens the breaker
for that long. Caller errors (400/404/413/422) are neither retried nor counted.
- Responses carry x-bud-endpoint-id, x-bud-fallback and x-bud-voice-substituted; spans carry
served endpoint, fallback source, retry count and rate-limit outcome.
- Per-vendor TTS concurrency is a bounded wait (WAAV_TTS_MAX_CONCURRENT_PER_VENDOR, default
64; it was a hard-coded 4 with an unbounded queue). A saturated vendor returns 503 instead
of hanging.
- Connection slots release on drop, so realtime and /ws sessions no longer leak the per-IP
slot. A per-IP cap of 0 means off. /ready returns 503 while draining.
Built on spec 021's voice analytics: every hop runs in the turn's vendor scope, a failed call is
classified through VoiceFailure (SynthesisError gains Vendor{status, retry_after} and Saturated
beside Rejected/Failed), and cost is recorded at the SERVED deployment's price.
Tested: 7013 lib tests (4 core::onnx tests need libonnxruntime.so, absent in the builder), 171
bud-auth tests, the voice span contract, and scripts/deployment_policy_e2e.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
resil renamed its retry profiles after what they are (BudEcosystem/resil#1): `RetryPolicy::waav` is now `RetryPolicy::interactive` (250 ms base, full jitter). Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
Author
|
Companion PR: BudEcosystem/bud-runtime#3058 (budapp publishes the policy; budadmin shows the panels). Depends on BudEcosystem/resil#1. |
This was referenced Sep 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A Bud voice deployment's Rate limiting and Resilience settings (FRD-022, bud-runtime
specs/022-gateway-rate-limit-resilience/) are now enforced by WaaV. It uses the same limiter as budgateway: BudEcosystem/resil, pinned by rev with theredis-0-27adapter.voice_table:bud-authparsesrate_limits,max_concurrent,retry_configandfallback_modelsleniently (a bad policy block drops the policy, never the endpoint). The contract fixture is byte-identical to budapp's.Retry-AfterandX-RateLimit-*headers;max_concurrentrejections returnconcurrency_limit_exceeded./v1/audio/speechand/v1/audio/transcriptions:Retry-Afteropens the breaker for that long.x-bud-endpoint-id(the deployment that served),x-bud-fallback,x-bud-voice-substituted.bud.voice.served_endpoint_id,bud.voice.fallback_from,bud.voice.retry_count,bud.rate_limit.outcome. Cost is recorded at the served deployment's price.SynthesisErrorgainsVendor{status, retry_after}andSaturated.WAAV_TTS_MAX_CONCURRENT_PER_VENDOR, default 64; it was a hard-coded 4 behind an unbounded queue), returning 503 when saturated./wsper-IP slot leak. A per-IP cap of 0 means off./readyanswers 503 while draining.WAAV_RATE_LIMIT_LAST_MILE(sync|local),WAAV_RATE_LIMIT_ON_STORE_UNAVAILABLE(deny|allow),WAAV_TTS_MAX_CONCURRENT_PER_VENDOR.Dependencies and merge order
9be62f18ac968cb98e4ccc621c42cd73e960f9ba).An older WaaV ignores the new
voice_tablefields, and this WaaV treats their absence as "no policy", so either side can deploy first. Only the budadmin panels would be inert until this image ships.No secret, IAM or Dapr changes.
deny.tomlallows the resil git source.Testing
core::onnxtests needlibonnxruntime.so, which isn't in the builder image; this change doesn't touch them.MutexGuardheld across await in tests).max_concurrentRetry-After; breakerOpenForLive on pde-ditto (image
dittops/waav:resil-1, 2026-09-27):max_concurrent.scribe-v2fallback (x-bud-fallback: true), and the deployment breaker opened after 5 failures.VoiceTurnFactrows carry the served deployment, fallback source and rate-limit outcome.🤖 Generated with Claude Code