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
33 changes: 26 additions & 7 deletions internal/proxy/codex_http_request_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,9 @@ type CodexPreparedHTTPRequest struct {
Frozen *CodexFrozenRequest
Lifecycle CodexHTTPRequestLifecycle

leaseHandle *CodexLeaseRequestHandle
receipt *codexTurnReceiptHandle
leaseHandle *CodexLeaseRequestHandle
receipt *codexTurnReceiptHandle
portableQuotaRetry bool
}

// CodexHTTPRequestPlanErrorCode classifies preparation failures without
Expand Down Expand Up @@ -576,6 +577,13 @@ func (factory *CodexHTTPRequestPlanFactory) buildOnce(ctx context.Context, input
AcceptedRevision: input.AcceptedRevision,
Now: now,
}
// A continuation cannot move its response or turn state to another account.
// Keep its exhausted binding available for one quota probe instead of
// removing it from the candidate set and reporting a routing failure.
boundQuotaProbe := boundAccountKey != "" && containsCodexHTTPRequestAccountKey(snapshot.QuotaExhaustedAccountKeys, boundAccountKey)
if boundQuotaProbe {
dispatchInput.UnavailableAccountKeys = excludeCodexHTTPRequestAccountKeys(dispatchUnavailable, []codex.AccountKey{boundAccountKey})
}
dispatch, err := factory.buildDispatch(ctx, dispatchInput)
if err != nil {
emitCodexTrace(ctx, CodexTraceEvent{Phase: "route_selection", Outcome: "error", Reason: string(codexRequestFailureReason(err))})
Expand Down Expand Up @@ -651,27 +659,37 @@ func (factory *CodexHTTPRequestPlanFactory) buildOnce(ctx context.Context, input
quotaExhaustionProbe := containsCodexHTTPRequestAccountKey(snapshot.QuotaExhaustedAccountKeys, choice.AccountKey)
if policyDecision.Status == PolicyDecisionSelected {
available := excludeCodexHTTPRequestAccountKeys(policyDecision.Allowed, dispatchUnavailable)
if len(available) != 0 {
policyDecision.Allowed = available
} else if quotaExhaustionProbe {
if quotaExhaustionProbe {
policyDecision.Allowed = []codex.AccountKey{choice.AccountKey}
} else if len(available) != 0 {
policyDecision.Allowed = available
}
}
if boundAccountKey == "" && codexHTTPRequestAccountUnavailablePortable(protocol) {
dispatch.accountUnavailablePortable = true
}
if len(resetInventory.Accounts) > 1 {
resetPlan := dispatch
if boundAccountKey != "" {
resetInput := dispatchInput
resetInput.UnavailableAccountKeys = dispatchUnavailable
resetInput.ProbeUnavailableWhenAll = false
resetInput.Inventory = resetInventory
resetInput.AffinityAccountKey = ""
resetInput.AffinityEffectiveModel = ""
resetInput.BoundAccountKey = ""
resetPlan, err = factory.buildDispatch(ctx, resetInput)
if err != nil {
return result, newCodexHTTPRequestPlanError(CodexHTTPRequestPlanDispatch, err)
var exhausted *CachedUsageLimitError
if !errors.As(err, &exhausted) {
return result, newCodexHTTPRequestPlanError(CodexHTTPRequestPlanDispatch, err)
}
// No reset route remains, but the bound attempt is still valid.
resetPlan = CodexFrozenDispatchPlan{}
}
}
dispatch = dispatch.withAccountUnavailableResetCandidates(resetPlan, choice)
if accountUnavailablePortable {
if accountUnavailablePortable && !quotaExhaustionProbe {
dispatch = dispatch.withAccountUnavailableFallbacks(resetPlan, choice)
}
}
Expand Down Expand Up @@ -729,6 +747,7 @@ func (factory *CodexHTTPRequestPlanFactory) buildOnce(ctx context.Context, input
}
emitCodexTrace(ctx, CodexTraceEvent{Phase: "lease_begin", Outcome: "success", AccountHint: codexTraceAccountHint(choice.AccountKey)})

result.portableQuotaRetry = codexHTTPRequestAccountUnavailablePortable(protocol)
result.Dispatch = dispatch
result.Frozen = frozen
result.leaseHandle = handle
Expand Down
107 changes: 107 additions & 0 deletions internal/proxy/codex_http_request_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2693,3 +2693,110 @@ func assertCodexHTTPRequestPlanError(t *testing.T, err error, code CodexHTTPRequ
}
}
}

func TestCodexHTTPRequestPlanFactoryProbesQuotaExhaustedBoundAccount(t *testing.T) {
now := time.Unix(1_700_000_000, 0).UTC()
for _, test := range []struct {
name string
previous bool
turnState bool
expected bool
restartable bool
allExhausted bool
authenticated bool
zeroAlternate bool
}{
{name: "previous response", previous: true},
{name: "zero capacity alternate", previous: true, zeroAlternate: true},
{name: "authenticated continuation", previous: true, authenticated: true},
{name: "turn state", turnState: true},
{name: "expected binding", expected: true},
{name: "restartable portable", restartable: true},
{name: "all exhausted continuation", previous: true, allExhausted: true},
} {
t.Run(test.name, func(t *testing.T) {
inventory := codex.Inventory{Accounts: []codex.LogicalAccount{
frozenDispatchTestLogicalAccount("account-a", frozenDispatchCandidate("account-a", "candidate-a", "revision-a", codex.SourceSystem, false, now.Add(time.Hour))),
frozenDispatchTestLogicalAccount("account-b", frozenDispatchCandidate("account-b", "candidate-b", "revision-b", codex.SourceExternal, false, now.Add(time.Hour))),
}}
identity := CodexJournalRecordIdentity{LaneDigest: "lane", TurnDigest: "turn", ModeEpoch: 1, Authoritative: true}
snapshot := CodexLeaseRouteSnapshot{
Classification: CodexRestoredLaneCurrent, JournalGeneration: 2,
BoundAccountKey: "account-a", BoundIdentity: identity, BoundRecordGeneration: 1,
BoundChoice: RouteChoice{AccountKey: "account-a", EffectiveModel: "gpt-5", RequiredBuckets: []CapacityBucket{CapacityBucketBase}},
RestartableFailedHead: test.restartable,
UnavailableAccountKeys: []codex.AccountKey{"account-a"},
QuotaExhaustedAccountKeys: []codex.AccountKey{"account-a"},
}
if test.allExhausted {
snapshot.UnavailableAccountKeys = append(snapshot.UnavailableAccountKeys, "account-b")
snapshot.QuotaExhaustedAccountKeys = append(snapshot.QuotaExhaustedAccountKeys, "account-b")
}
runtime := &codexHTTPRequestPlanTestRuntime{handle: &CodexLeaseRequestHandle{account: "account-a"}}
factory := &CodexHTTPRequestPlanFactory{
Inventory: &codexHTTPRequestPlanTestInventory{inventory: inventory},
Routes: &codexHTTPRequestPlanTestSnapshotter{snapshot: snapshot}, Runtime: runtime,
DefaultAccountKey: "account-a", Authority: CodexLeaseAuthorityPolicy{ModeEpoch: 1, Authoritative: true},
Now: func() time.Time { return now },
}
if test.zeroAlternate {
capacity := NewCodexCapacityLedger(func() time.Time { return now }, time.Hour)
frozenDispatchObserveCapacity(t, capacity, "account-b", CapacityBucketBase, 0, now)
factory.Capacity = capacity
}
key := []byte("01234567890123456789012345678901")
factory.SessionPolicy = NewSessionPolicyResolver(key, routingPolicyV2ForTest(RoutingPolicyV1{
SchemaVersion: 1, AuthorityGeneration: 1, RoutingGeneration: 1, EffectiveGeneration: 1,
Pools: []AccountPoolV1{{Name: "team", Members: []codex.AccountKey{"account-a", "account-b"}}},
SessionBindings: []SessionBindingV1{{SessionDigest: keyedSessionDigest(key, []byte("session")), Pool: "team"}},
}))
permits := &sessionPolicyPermitRecorder{}
factory.DispatchPermits = permits
ctx := withRuntimeCallerAuthority(context.Background(), RuntimeCallerAuthorityV1{Domain: NormalCallerLocal, SubjectID: "local-caller", ConsumptionDigest: strings.Repeat("a", 64)})
if test.authenticated {
ctx = withRuntimeCallerAuthority(ctx, RuntimeCallerAuthorityV1{Domain: NormalCallerCodex, SubjectID: "account-a", IndexEpoch: 1, ConsumptionDigest: strings.Repeat("a", 64)})
ctx = withRuntimeCallerIdentity(ctx, "account-a\x00candidate-a\x00revision-a")
}
input := CodexHTTPRequestPlanInput{Encoded: frozenRequestBody("gpt-5", CodexRequestTurn, "private-body")}
if test.previous {
input.Encoded = []byte(strings.TrimSuffix(string(input.Encoded), "}") + `,"previous_response_id":"response-a"}`)
}
if test.turnState {
input.Headers = http.Header{"X-Codex-Turn-State": {"private-turn-state"}}
}
if test.expected {
input.ExpectedBound = &CodexLeaseBoundExpectation{Identity: identity, AccountKey: "account-a", RecordGeneration: 1}
}
prepared, err := factory.Build(ctx, input)
if err != nil {
t.Fatal(err)
}
defer prepared.Frozen.Release()
if prepared.portableQuotaRetry != (!test.previous && !test.turnState) {
t.Fatalf("portable quota retry = %t", prepared.portableQuotaRetry)
}
if len(permits.requests) != 1 || !slices.Equal(permits.requests[0].AllowedAccounts, []codex.AccountKey{"account-a"}) {
t.Fatalf("bound probe permit scope = %#v, want only account-a", permits.requests)
}
accounts := prepared.Dispatch.Accounts()
if len(accounts) != 1 || accounts[0].Choice().AccountKey != "account-a" {
t.Fatalf("bound probe accounts = %#v, want only account-a", accounts)
}
if !runtime.plan.QuotaExhaustionProbe {
t.Fatal("bound account was not a quota probe")
}
for _, slot := range runtime.plan.Slots {
if slot.AccountKey != "account-a" {
t.Fatalf("quota probe contains alternate slot: %#v", slot)
}
}
wantReset := []codex.AccountKey{"account-b"}
if test.allExhausted || test.zeroAlternate {
wantReset = nil
}
if got := prepared.Dispatch.AccountUnavailableResetCandidates(); !slices.Equal(got, wantReset) {
t.Fatalf("reset candidates = %v, want %v", got, wantReset)
}
})
}
}
5 changes: 4 additions & 1 deletion internal/proxy/codex_http_request_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,8 @@ type CodexHTTPRequestSessionResult struct {
Choice RouteChoice
Attempt CandidateAttempt
Lifecycle CodexHTTPRequestLifecycle

quotaExhausted bool
}

// CodexHTTPAttemptSlotPlan is one raw-free bridge entry for the durable lease
Expand Down Expand Up @@ -741,7 +743,7 @@ accountsLoop:
result.Attempt = retainedAttempt
return result, nil
}
if (authRejected || hardRejected) && plan.TerminalError() != nil && codexHTTPRequestCanRecordAccountUnavailable(plan, result.Lifecycle) {
if authRejected && plan.TerminalError() != nil && codexHTTPRequestCanRecordAccountUnavailable(plan, result.Lifecycle) {
discardCodexHTTPRequestResponse(ctx, response)
result.Response = nil
next, finishErr := codexHTTPRequestRecordAccountUnavailable(ctx, result.Lifecycle, 0, hardRejected)
Expand All @@ -763,6 +765,7 @@ accountsLoop:
return result, finishErr
}
result.Lifecycle = next
result.quotaExhausted = hardRejected && codexHTTPRequestCanRecordAccountUnavailable(plan, result.Lifecycle)
return result, nil
}
}
Expand Down
6 changes: 3 additions & 3 deletions internal/proxy/codex_lease_v2_cas.go
Original file line number Diff line number Diff line change
Expand Up @@ -759,18 +759,18 @@ func (store *CodexLeaseStore) buildCodexLeaseRecordAfterImage(old CodexJournalRe
if old.HasEncryptedState && !input.HasEncryptedState {
return CodexJournalRecordV2{}, 0, false, fmt.Errorf("%w: encrypted-state authority was cleared", ErrCodexLeaseInvalidMutation)
}
if old.HasTurnState && !input.HasTurnState {
if old.HasTurnState && !input.HasTurnState && !bindingReassignment {
return CodexJournalRecordV2{}, 0, false, fmt.Errorf("%w: turn-state authority was cleared", ErrCodexLeaseInvalidMutation)
}
if !old.HasTurnState && input.HasTurnState && !input.TurnStateLatchCurrent {
return CodexJournalRecordV2{}, 0, false, fmt.Errorf("%w: first turn-state authority is missing current latch marker", ErrCodexLeaseInvalidMutation)
}
validLatchMigration := migrateTurnStateLatch && beginRequest && old.HasTurnState && !old.TurnStateLatchCurrent && input.TurnStateLatchCurrent && old.EverAdmitted &&
constantTimeCodexLeaseDigestEqual(old.AccountHash, input.AccountHash) && codexLeaseRuntimeCanBeginRequest(old)
if old.TurnStateLatchCurrent != input.TurnStateLatchCurrent && !validLatchMigration && !(old.HasTurnState == false && input.HasTurnState && input.TurnStateLatchCurrent) {
if old.TurnStateLatchCurrent != input.TurnStateLatchCurrent && !validLatchMigration && !(bindingReassignment && !input.HasTurnState && !input.TurnStateLatchCurrent) && !(old.HasTurnState == false && input.HasTurnState && input.TurnStateLatchCurrent) {
return CodexJournalRecordV2{}, 0, false, fmt.Errorf("%w: turn-state latch marker changed outside admission or migration", ErrCodexLeaseInvalidMutation)
}
if old.HasResponseAnchor && (!input.HasResponseAnchor || input.CorrelationHash == "") {
if old.HasResponseAnchor && (!input.HasResponseAnchor || input.CorrelationHash == "") && !bindingReassignment {
return CodexJournalRecordV2{}, 0, false, fmt.Errorf("%w: response anchor was cleared", ErrCodexLeaseInvalidMutation)
}
result.RecordGeneration = old.RecordGeneration + 1
Expand Down
22 changes: 19 additions & 3 deletions internal/proxy/codex_lease_v2_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,8 @@ func (runtime *CodexLeaseRuntime) BeginRequestContext(ctx context.Context, plan
return nil, err
}
handle.newTurn = newTurn
handle.relatchTurnStateOnAdmission = runtime.canRelatchAuthenticatedTurnState(current.Record, selected.AccountKey, plan.Evidence, plan.authenticatedCallerContinuity)
handle.relatchTurnStateOnAdmission = runtime.canRelatchAuthenticatedTurnState(current.Record, selected.AccountKey, plan.Evidence, plan.authenticatedCallerContinuity) ||
(current.Record.HasTurnState && codexLeasePortableUnavailableContinuation(current.Record, plan.Evidence))
return handle, nil
}

Expand Down Expand Up @@ -1488,6 +1489,9 @@ func (handle *CodexLeaseRequestHandle) applyAccountUnavailableRebind(desired *Co
return ErrCodexLeaseTransition
}
desired.AccountHash = accountHash
// The previous account's response ID cannot authorise this account.
desired.CorrelationHash = ""
desired.HasResponseAnchor = false
return nil
}

Expand Down Expand Up @@ -2030,14 +2034,15 @@ func (runtime *CodexLeaseRuntime) validateRequestContinuity(restored CodexRestor
if newTurn && authenticatedCallerContinuity && (!found || !authority.Record.Authoritative) {
return true, nil
}
portableUnavailable := found && !newTurn && codexLeasePortableUnavailableContinuation(authority.Record, evidence)
if !newTurn && found {
ingress := firstIngressContinuity(ingressContinuity)
canMigrateLatch := runtime.canMigrateAuthenticatedTurnStateLatch(authority.Record, selected, evidence, authenticatedCallerContinuity)
if ingress != nil && (ingress.kind == codexLeaseIngressContinuityInvalid || !runtime.validIngressContinuityTarget(restored, requestIdentity, selected, ingress)) && !canMigrateLatch {
return false, &codexContinuityError{reason: codexContinuityTurnStateMismatch}
}
missingAuthenticatedState := authenticatedCallerContinuity && authority.Record.HasTurnState && !evidence.HasTurnState
if authority.Record.HasTurnState != evidence.HasTurnState && !missingAuthenticatedState {
if authority.Record.HasTurnState != evidence.HasTurnState && !missingAuthenticatedState && !portableUnavailable {
return false, &codexContinuityError{reason: codexContinuityTurnStatePresenceMismatch}
}
if evidence.HasTurnState && !constantTimeCodexLeaseDigestEqual(authority.Record.TurnStateHash, runtime.store.hash("turn-state", evidence.TurnState)) &&
Expand All @@ -2051,7 +2056,7 @@ func (runtime *CodexLeaseRuntime) validateRequestContinuity(restored CodexRestor
}
}
requiresAccount := authenticatedCallerContinuity || evidence.PreviousResponseID != "" || evidence.HasTurnState || (found && !newTurn && codexLeaseRecordRequiresAccount(authority.Record))
if found && codexLeaseCurrentAttemptState(authority.Record) == CodexAttemptAccountUnavailable && evidence.PreviousResponseID == "" && !evidence.HasTurnState {
if portableUnavailable {
requiresAccount = false
}
if requiresAccount && (!found || authority.Record.AccountHash == "" || !constantTimeCodexLeaseDigestEqual(authority.Record.AccountHash, runtime.store.hash("account", string(selected)))) {
Expand All @@ -2060,6 +2065,12 @@ func (runtime *CodexLeaseRuntime) validateRequestContinuity(restored CodexRestor
return requiresAccount, nil
}

// A full create after a drained account rejection can establish new provider
// state. Keep the old admission evidence until the replacement is admitted.
func codexLeasePortableUnavailableContinuation(record CodexJournalRecordV2, evidence CodexLeaseRequestEvidence) bool {
return evidence.PreviousResponseID == "" && !evidence.HasTurnState && codexLeaseAccountUnavailableCanBeginRequest(record)
}

func (runtime *CodexLeaseRuntime) canMigrateAuthenticatedTurnStateLatch(record CodexJournalRecordV2, selected codex.AccountKey, evidence CodexLeaseRequestEvidence, authenticatedCallerContinuity bool) bool {
return runtime != nil && runtime.store != nil && authenticatedCallerContinuity && evidence.HasTurnState && record.HasTurnState && !record.TurnStateLatchCurrent &&
record.AccountHash != "" && constantTimeCodexLeaseDigestEqual(record.AccountHash, runtime.store.hash("account", string(selected))) &&
Expand Down Expand Up @@ -2139,6 +2150,11 @@ func (handle *CodexLeaseRequestHandle) applyAdmissionEvidence(record *CodexJourn
return fmt.Errorf("%w: invalid HTTP admission evidence", ErrCodexLeaseInvalidMutation)
}
if !evidence.HasTurnState {
if codexLeaseAccountUnavailableAdmission(handle.record, *record) {
record.TurnStateHash = ""
record.HasTurnState = false
record.TurnStateLatchCurrent = false
}
return nil
}
if record.HasTurnState && !handle.relatchTurnStateOnAdmission {
Expand Down
Loading