Skip to content
Open
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
125 changes: 125 additions & 0 deletions internal/proxy/failover_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import (
"strings"
"sync"
"testing"
"time"

"workweave/router/internal/providers"
"workweave/router/internal/providers/anthropic"
"workweave/router/internal/providers/openai"
"workweave/router/internal/providers/openaicompat"
"workweave/router/internal/proxy"
"workweave/router/internal/proxy/usage"
"workweave/router/internal/router"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -525,3 +527,126 @@ func TestProxyMessages_GeminiNon400NotRetried(t *testing.T) {

assert.Len(t, client.bodies, 1, "a 503 is not a VALIDATED-schema 400 — no AUTO retry")
}

// TestProxyMessages_PreemptiveSubscriptionFailoverWarnsUser guards the
// pre-emptive path (an observer snapshot already reads exhausted before
// dispatch): the turn must serve on the deployment key AND the client must see
// the billable-failover warning marker instead of the normal routing marker.
Comment on lines +531 to +534

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TestProxyMessages_PreemptiveSubscriptionFailoverWarnsUser guards the
// pre-emptive path (an observer snapshot already reads exhausted before
// dispatch): the turn must serve on the deployment key AND the client must see
// the billable-failover warning marker instead of the normal routing marker.
// TestProxyMessages_PreemptiveSubscriptionFailoverWarnsUser: observer already
// reads exhausted — turn must serve on the deployment key and emit the
// billable-failover warning instead of the normal routing marker.

Was 4 lines narrating what the function name + assertions already convey; the why fits in 3.

func TestProxyMessages_PreemptiveSubscriptionFailoverWarnsUser(t *testing.T) {
fr := &fakeRouter{decision: router.Decision{Provider: providers.ProviderAnthropic, Model: bypassScorerPickMdl}}
p := &fakeProvider{proxyResponse: func(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"role\":\"assistant\",\"content\":[],\"model\":\""+bypassScorerPickMdl+"\",\"usage\":{\"input_tokens\":1,\"output_tokens\":0}}}\n\n")
_, _ = io.WriteString(w, "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n")
_, _ = io.WriteString(w, "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"hi\"}}\n\n")
_, _ = io.WriteString(w, "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n")
_, _ = io.WriteString(w, "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":1}}\n\n")
_, _ = io.WriteString(w, "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n")
}}
// Observer seeded EXHAUSTED on the weekly window, mirroring
// TestSubscriptionExhausted_ServesOnDeploymentKey.
obs := usage.NewObserver([]byte("salt"), 10*time.Minute, time.Now)
obs.Record(obs.Key([]byte(bypassSubToken)), usage.Snapshot{
Secondary: usage.Window{UsedPercent: 1.0, WindowMinutes: 10080},
})
telemetry := newCaptureTelemetry()
svc := proxy.NewService(fr, map[string]providers.Client{providers.ProviderAnthropic: p}, nil, false, nil, nil, false, providers.ProviderAnthropic, bypassScorerPickMdl, telemetry).
WithSubscriptionAwareRouting(obs, 0.05, 2.0).
WithDeploymentKeyedProviders(map[string]struct{}{providers.ProviderAnthropic: {}})

rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/v1/messages", strings.NewReader(""))
body := []byte(`{"model":"` + bypassScorerPickMdl + `","stream":true,"messages":[{"role":"user","content":"hi"}]}`)

// bypassCtx sets the usage-bypass gate; the routed path is what we're
// exercising here, so use a plain subscription context instead.
ctx := context.WithValue(context.Background(), proxy.AnthropicSubscriptionContextKey{}, bypassSubToken)
ctx = context.WithValue(ctx, proxy.InstallationIDContextKey{}, "11111111-1111-1111-1111-111111111111")
require.NoError(t, svc.ProxyMessages(ctx, body, rec, req))

require.Len(t, p.proxyCreds, 1, "the turn must be dispatched once, on the deployment key")
creds := p.proxyCreds[0]
if creds != nil {
assert.False(t, creds.OAuth, "the exhausted subscription must not be forwarded")
}
respBody := rec.Body.String()
assert.Contains(t, respBody, "your Claude subscription hit its usage limit",
"the client must see the billable-failover warning, not the normal routing marker")
assert.NotContains(t, respBody, "· "+"best pick for this turn",
"the failover warning replaces the routing marker, it doesn't append to it")
row := telemetry.firstRow(t)
assert.NotEqual(t, "subscription", row.CredentialSource,
"the telemetry row must not attribute this billable turn to the spent subscription")
}

// TestProxyMessages_ReactiveSubscriptionFailoverWarnsUser guards the reactive
// path: a subscription-served Anthropic turn hits a live retryable error
// (429), so the router retries the SAME model on the Weave key. The retry
// must carry the billable-failover warning marker, not the original routing
// marker, and the retry must authenticate via x-api-key (not the spent OAuth
// bearer).
Comment on lines +583 to +588

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TestProxyMessages_ReactiveSubscriptionFailoverWarnsUser guards the reactive
// path: a subscription-served Anthropic turn hits a live retryable error
// (429), so the router retries the SAME model on the Weave key. The retry
// must carry the billable-failover warning marker, not the original routing
// marker, and the retry must authenticate via x-api-key (not the spent OAuth
// bearer).
// TestProxyMessages_ReactiveSubscriptionFailoverWarnsUser: live 429 on the
// subscription triggers a retry on the Weave key — retry must carry the
// billable-failover marker and authenticate via x-api-key, not OAuth bearer.

Was 6 lines; same content fits in 3.

func TestProxyMessages_ReactiveSubscriptionFailoverWarnsUser(t *testing.T) {
var (
mu sync.Mutex
calls int
)
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
calls++
attempt := calls
mu.Unlock()
if attempt <= 3 {
// Attempts 1-3: subscription OAuth bearer. dispatchWithFallback
// retries a sole binding in place up to maxSameBindingRetries (2)
// before giving up, so the 429 must persist through all three
// same-binding attempts to reach the subscription-failover retry.
Comment on lines +600 to +603

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Attempts 1-3: subscription OAuth bearer. dispatchWithFallback
// retries a sole binding in place up to maxSameBindingRetries (2)
// before giving up, so the 429 must persist through all three
// same-binding attempts to reach the subscription-failover retry.
// Attempts 1-3: dispatchWithFallback retries the same binding up to
// maxSameBindingRetries (2), so 429 must persist all three attempts
// before the subscription-failover retry fires.

Was 4 lines; same non-obvious WHY fits in 3.

assert.Equal(t, "Bearer "+bypassSubToken, r.Header.Get("Authorization"),
"same-binding retries must still authenticate with the subscription bearer")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
_, _ = io.WriteString(w, `{"type":"error","error":{"type":"rate_limit_error","message":"5h limit reached"}}`)
return
}
// Attempt 4: the subscription-failover retry, on the deployment/BYOK
// key rather than the spent subscription bearer.
assert.Empty(t, r.Header.Get("Authorization"), "the retry must not reuse the spent subscription bearer")
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_recovered\",\"role\":\"assistant\",\"content\":[],\"model\":\""+bypassScorerPickMdl+"\",\"usage\":{\"input_tokens\":5,\"output_tokens\":0}}}\n\n")
_, _ = io.WriteString(w, "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n")
_, _ = io.WriteString(w, "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"recovered\"}}\n\n")
_, _ = io.WriteString(w, "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n")
_, _ = io.WriteString(w, "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":1}}\n\n")
_, _ = io.WriteString(w, "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n")
}))
defer upstream.Close()

telemetry := newCaptureTelemetry()
svc := proxy.NewService(
&fakeRouter{decision: router.Decision{Provider: providers.ProviderAnthropic, Model: bypassScorerPickMdl}},
map[string]providers.Client{providers.ProviderAnthropic: anthropic.NewClient("test-key", upstream.URL)},
nil, false, nil, nil, false, providers.ProviderAnthropic, bypassScorerPickMdl, telemetry,
).WithDeploymentKeyedProviders(map[string]struct{}{providers.ProviderAnthropic: {}})

rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/v1/messages", strings.NewReader(""))
req.Header.Set("Authorization", "Bearer "+bypassSubToken)
body := []byte(`{"model":"` + bypassScorerPickMdl + `","stream":true,"messages":[{"role":"user","content":"hi"}]}`)

ctx := context.WithValue(context.Background(), proxy.InstallationIDContextKey{}, "11111111-1111-1111-1111-111111111111")
require.NoError(t, svc.ProxyMessages(ctx, body, rec, req))

mu.Lock()
assert.Equal(t, 4, calls, "3 same-binding attempts on the subscription, then 1 subscription-failover retry on the Weave key")
mu.Unlock()
assert.Equal(t, http.StatusOK, rec.Code)
respBody := rec.Body.String()
assert.Contains(t, respBody, "recovered", "the client receives the retried attempt's content")
assert.Contains(t, respBody, "your Claude subscription hit its usage limit",
"the retry must carry the billable-failover warning")
assert.NotContains(t, respBody, "rate_limit_error", "the failed subscription attempt must never commit bytes")
row := telemetry.firstRow(t)
assert.NotEqual(t, "subscription", row.CredentialSource,
"the telemetry row must attribute this billable retry to the Weave key, not the spent subscription")
}
39 changes: 34 additions & 5 deletions internal/proxy/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2453,8 +2453,10 @@ func (s *Service) ProxyMessages(ctx context.Context, body []byte, w http.Respons
// another turn on it (429 until reset). Suppress the spent token so
// resolution falls through to the deployment/BYOK key — the turn serves on
// the Weave key (full cost) instead of hard-failing. Only fires once the
// observer has recorded exhaustion and a fallback key exists.
if s.claudeSubscriptionExhausted(ctx, r.Header) {
// observer has recorded exhaustion and a fallback key exists. Recorded so
// the marker built below can surface the billable-failover warning.
subscriptionFailingOver := s.claudeSubscriptionExhausted(ctx, r.Header)
if subscriptionFailingOver {
ctx = withSuppressedClaudeSubscription(ctx)
}
ctx = resolveAndInjectCredentials(ctx, decision.Provider, r.Header)
Expand Down Expand Up @@ -2535,6 +2537,12 @@ func (s *Service) ProxyMessages(ctx context.Context, body []byte, w http.Respons
// turn reaching here is served free and should carry the top-up CTA.
if billing.SubscriptionOnlyFromContext(ctx) {
marker = subscriptionOnlyWarningMarker
} else if subscriptionFailingOver {
// Subscription hit its 5h/7d limit pre-dispatch: the turn is about to
// serve on the billable Weave/BYOK key instead. Not gated by the
// routing-marker opt-out — same billing-state-change rule as the
// subscription-only warning above.
Comment on lines +2541 to +2544

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Subscription hit its 5h/7d limit pre-dispatch: the turn is about to
// serve on the billable Weave/BYOK key instead. Not gated by the
// routing-marker opt-out — same billing-state-change rule as the
// subscription-only warning above.
// Not gated by the routing-marker opt-out — billing state change,
// same rule as subscriptionOnlyWarningMarker above.

Was 4 lines; the one non-obvious point (opt-out exemption) fits in 2.

marker = subscriptionFailoverWarningMarker

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failover warning ignores routed provider

High Severity

The preemptive path sets subscriptionFailoverWarningMarker whenever claudeSubscriptionExhausted is true, without checking that decision.Provider is Anthropic. After a limit hit, subsidy drops and routing often picks OSS/Gemini, so those unrelated turns still get the billable Claude-failover warning even though no subscription→Weave Anthropic failover occurred.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 78a62f4. Configure here.

}
// toolValidator compiles the request's tool schemas once (LRU-cached);
// translators validate/repair model tool calls against it. Nil if no tools.
Expand Down Expand Up @@ -2875,7 +2883,10 @@ func (s *Service) ProxyMessages(ctx context.Context, body []byte, w http.Respons
"model", decision.Model,
"err", proxyErr,
"upstream_status", upstreamStatus(proxyErr))
subAttempt := s.anthropicNativeAttempt(env, r, subPrep, sink, preludeBuf, marker, setExtractor)
// Swap in the billable-failover warning for this retry attempt — safe
// because reaching here already required preludeBuf.Committed() ==
// false, so nothing under the original marker has hit the wire yet.
Comment on lines +2886 to +2888

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Swap in the billable-failover warning for this retry attempt — safe
// because reaching here already required preludeBuf.Committed() ==
// false, so nothing under the original marker has hit the wire yet.
// Safe: preludeBuf.Committed() == false here, so no bytes are committed under the original marker.

Was 3 lines; the invariant fits on one.

subAttempt := s.anthropicNativeAttempt(env, r, subPrep, sink, preludeBuf, subscriptionFailoverWarningMarker, setExtractor)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning misstates non-limit failovers

Medium Severity

The reactive retry always swaps in subscriptionFailoverWarningMarker, whose copy claims a Claude usage-limit hit and automatic window reset. That path also runs for any IsRetryable fault (5xx, 408, stalls, transport) and for OAuth authentication_error/permission_error, so users can be told they burned plan quota when the cause was a transient outage or rejected token.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 78a62f4. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Usage-limit warning misclassifies failures

This retry also runs for generic retryable errors and OAuth credential rejections, but it always injects text saying that the Claude subscription “hit its usage limit.” A recovered 503, timeout, transport failure, or OAuth 401 can therefore be shown to the customer as quota exhaustion even when no usage limit was involved. Reserve quota-specific copy for positively identified subscription exhaustion and use neutral failover wording for other recovery paths.

Artifacts

Focused hermetic ProxyMessages test source for 503 and OAuth rejection

  • The executed test configures a real ProxyMessages request and Anthropic HTTP client with local upstream responses for non-quota failures, then checks the returned SSE marker; it documents the reproducible coverage.

Baseline quota-exhaustion subscription failover test passed

  • The existing 429 subscription failover test was executed from `/home/user/repo` and passed after retrying with suppressed OAuth credentials; it confirms the baseline quota flow.

503 and OAuth rejection retries emit quota-exhaustion wording

  • The focused hermetic test was executed from `/home/user/repo` and failed as expected: both a successful 503 recovery and a successful OAuth-401 recovery sent the client the quota-exhaustion marker, confirming the bug.

View artifacts

T-Rex Ran code and verified through T-Rex

crossFormat = false
respSummary = translate.ResponseSummary{}
reqStats = providers.RequestMutationStats{}
Expand Down Expand Up @@ -2944,6 +2955,11 @@ func (s *Service) ProxyMessages(ctx context.Context, body []byte, w http.Respons

in, out := extractor.Tokens()
cacheCreation, cacheRead := extractor.CacheTokens()
// Computed here (rather than only at the later Postgres-telemetry site)
// so credential.source and the combined subscription-failover flag reach
// the router.call OTLP log record — WorkWeave's ingest reads that record,
// not this router's own Postgres telemetry table.
Comment on lines +2958 to +2961

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Computed here (rather than only at the later Postgres-telemetry site)
// so credential.source and the combined subscription-failover flag reach
// the router.call OTLP log record — WorkWeave's ingest reads that record,
// not this router's own Postgres telemetry table.
// Computed here so credential.source and the subscription-failover flag
// reach the router.call OTLP log record — WorkWeave’s ingest reads that,
// not this router’s Postgres telemetry table.

Was 4 lines; the non-obvious why (OTLP vs. Postgres telemetry site) fits in 3.

_, _, credSourceForAttrs := s.credentialKeyParts(ctx)
upstreamBuilder := otel.NewAttrBuilder(40).
String("request_id", requestID).
String("external_id", externalID).
Expand Down Expand Up @@ -2978,7 +2994,16 @@ func (s *Service) ProxyMessages(ctx context.Context, body []byte, w http.Respons
Int64("dispatch.fallback_attempts", int64(winnerIdx)).
Bool("dispatch.failover_used", finalProvider != primaryProvider || subscriptionFailoverUsed).
Bool("dispatch.baseline_failover", baselineFailoverUsed).
Bool("dispatch.subscription_failover", subscriptionFailoverUsed)
Bool("dispatch.subscription_failover", subscriptionFailoverUsed).
// Combined pre-emptive (subscriptionFailingOver, an exhausted observer
// snapshot skipping dispatch entirely) OR reactive
// (subscriptionFailoverUsed, a live 429/OAuth-rejection retry) signal —
// either means this turn is billed on the Weave/BYOK key instead of the
// caller's own subscription. dispatch.subscription_failover above only
// covers the reactive branch; this one is the authoritative bit for
// admin alerting downstream.
Comment on lines +2998 to +3004

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Combined pre-emptive (subscriptionFailingOver, an exhausted observer
// snapshot skipping dispatch entirely) OR reactive
// (subscriptionFailoverUsed, a live 429/OAuth-rejection retry) signal —
// either means this turn is billed on the Weave/BYOK key instead of the
// caller's own subscription. dispatch.subscription_failover above only
// covers the reactive branch; this one is the authoritative bit for
// admin alerting downstream.
// Combined pre-emptive (subscriptionFailingOver) OR reactive
// (subscriptionFailoverUsed) signal — dispatch.subscription_failover above
// covers only the reactive branch; this is the authoritative bit for alerting.

Was 7 lines; the one non-obvious point (covers both paths, unlike the field above) fits in 3.

Bool("dispatch.subscription_failover_billable", subscriptionFailingOver || subscriptionFailoverUsed).
String("credential.source", credSourceForAttrs)
applyPlannerAttrs(upstreamBuilder, routeRes)
applyRoutingStateAttrs(upstreamBuilder, routeRes, decision.Model, sessionKey)
addTimingAttrs(ctx, upstreamBuilder)
Expand Down Expand Up @@ -4736,6 +4761,9 @@ func (s *Service) ProxyOpenAIChatCompletion(ctx context.Context, body []byte, w

in, out := extractor.Tokens()
cacheCreation, cacheRead := extractor.CacheTokens()
// See ProxyMessages for why this is computed before the builder rather
// than only at the later Postgres-telemetry site.
_, _, openaiCredSourceForAttrs := s.credentialKeyParts(ctx)
openaiUpstreamBuilder := otel.NewAttrBuilder(40).
String("request_id", requestID).
String("external_id", externalID).
Expand Down Expand Up @@ -4766,7 +4794,8 @@ func (s *Service) ProxyOpenAIChatCompletion(ctx context.Context, body []byte, w
String("dispatch.primary_provider", primaryProvider).
String("dispatch.final_provider", finalProvider).
Int64("dispatch.fallback_attempts", int64(winnerIdx)).
Bool("dispatch.failover_used", finalProvider != primaryProvider)
Bool("dispatch.failover_used", finalProvider != primaryProvider).
String("credential.source", openaiCredSourceForAttrs)
applyPlannerAttrs(openaiUpstreamBuilder, routeRes)
applyRoutingStateAttrs(openaiUpstreamBuilder, routeRes, decision.Model, sessionKey)
addTimingAttrs(ctx, openaiUpstreamBuilder)
Expand Down
9 changes: 9 additions & 0 deletions internal/proxy/usage_bypass.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,15 @@ const subscriptionOnlyWarningMarkerCodex = routingMarkerPrefix +
"your Weave router credits are depleted, so this turn is running on your own ChatGPT (Codex) subscription and paid model fallback is disabled. Add credits to restore full routing: " +
topUpURL + "\n\n"

// subscriptionFailoverWarningMarker is prepended when a subscription-served
// Anthropic turn fails over to the billable Weave/BYOK key mid-session (5h/7d
// unified rate-limit rejection, or an OAuth credential rejection). Distinct
// from subscriptionOnlyWarningMarker: that one signals a permanent, org-level
// state (credits depleted, until top-up); this one is a per-turn, self-healing
// condition, so the copy says "this turn" rather than pointing at a CTA.
Comment on lines +216 to +221

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// subscriptionFailoverWarningMarker is prepended when a subscription-served
// Anthropic turn fails over to the billable Weave/BYOK key mid-session (5h/7d
// unified rate-limit rejection, or an OAuth credential rejection). Distinct
// from subscriptionOnlyWarningMarker: that one signals a permanent, org-level
// state (credits depleted, until top-up); this one is a per-turn, self-healing
// condition, so the copy says "this turn" rather than pointing at a CTA.
// subscriptionFailoverWarningMarker is prepended when a subscription turn fails
// over to the billable Weave/BYOK key (5h/7d rate-limit or OAuth rejection).
// Unlike subscriptionOnlyWarningMarker (permanent org-level state), this is
// per-turn and self-healing — no CTA.

Was 6 lines; the permanent-vs-per-turn contrast fits in 4.

const subscriptionFailoverWarningMarker = routingMarkerPrefix +
"your Claude subscription hit its usage limit, so this turn is running on the Weave router key and will be billed. Full routing resumes automatically once your subscription window resets.\n\n"
Comment on lines +222 to +223

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 BYOK fallback misidentified as Weave

After suppressing the subscription credential, credential resolution can select the customer’s Anthropic BYOK key. The retry then authenticates with that customer key, but this fixed message says that it is running on the Weave router key and that the turn will be billed. Select the warning based on the resolved fallback credential source so BYOK retries accurately describe credential ownership and billing.

Artifacts

Focused hermetic ProxyMessages failover test source

  • Authored test drives subscription 429 retries followed by a customer Anthropic BYOK fallback and asserts the customer-visible marker, establishing the executable scenario.

Subscription failover runtime output with customer BYOK fallback

  • Executed `go test` output shows three subscription attempts, a successful fourth BYOK-authenticated retry, and the retained Weave-billing marker; the mismatch is confirmed.

Diff whitespace validation output

  • Executed `git diff --check` completed with exit code 0 after adding only the focused test, confirming no patch formatting errors.

View artifacts

T-Rex Ran code and verified through T-Rex


// ErrCreditsExhaustedSubscriptionUnavailable is returned by ProxyMessages and
// ProxyOpenAIChatCompletion when the org is in subscription-only mode but the
// turn cannot be served on the caller's own
Expand Down
Loading