Arm the shared cooldown for rate limits tunneled through HTTP 200
Azure is the only zero-data-retention route for the gpt-5.6 family, so its capacity 429s arrive frequently and OpenRouter forwards them inside an HTTP 200 envelope. Those bypassed the rate controller entirely: a paced run kept sending a request every three seconds into a throttled endpoint, failing row by row. An in-envelope 429 now records the same escalating cooldown as a transport 429, so later acquisitions fail fast until the deadline passes.
This commit is contained in:
@@ -365,6 +365,12 @@ func (c *Client) complete(ctx context.Context, gate *ratelimit.Controller, r com
|
||||
Code int `json:"code"`
|
||||
}
|
||||
_ = json.Unmarshal(envelope.Error, &detail)
|
||||
if detail.Code == http.StatusTooManyRequests {
|
||||
// An upstream rate limit tunneled through HTTP 200 must arm the
|
||||
// same cooldown as a transport 429: later Acquire calls fail fast
|
||||
// instead of pacing more requests into a throttled endpoint.
|
||||
return "", gate.ReportLimit()
|
||||
}
|
||||
if detail.Code != 0 {
|
||||
return "", fmt.Errorf("AI provider reported an error (code %d)", detail.Code)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"finance-duck/internal/domain"
|
||||
"finance-duck/internal/ratelimit"
|
||||
@@ -358,6 +359,30 @@ func TestMalformedEnvelopesRejected(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// An upstream rate limit tunneled inside an HTTP 200 envelope must arm the
|
||||
// shared cooldown like a transport 429: the next classification fails fast
|
||||
// instead of pacing another request into a throttled endpoint.
|
||||
func TestEnvelope429ArmsSharedCooldown(t *testing.T) {
|
||||
f, d := fixture()
|
||||
calls := 0
|
||||
c := mockClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
_, _ = io.WriteString(w, `{"error":{"code":429,"message":"private"},"choices":[]}`)
|
||||
})
|
||||
c.rate.Store(&ratelimit.Controller{InitialBackoff: time.Minute})
|
||||
_, err := c.Classify(context.Background(), f, d, true)
|
||||
var limit *ratelimit.RateLimitError
|
||||
if err == nil || !errors.As(err, &limit) || strings.Contains(err.Error(), "private") {
|
||||
t.Fatalf("envelope 429 not reported as a rate limit: %v", err)
|
||||
}
|
||||
if _, err = c.Classify(context.Background(), f, d, true); err == nil || !errors.As(err, &limit) {
|
||||
t.Fatalf("cooldown not armed: %v", err)
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatalf("throttled endpoint was contacted again: %d calls", calls)
|
||||
}
|
||||
}
|
||||
|
||||
type failingTransport struct{}
|
||||
|
||||
func (failingTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
|
||||
Reference in New Issue
Block a user