Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
181 commits
Select commit Hold shift + click to select a range
95343d3
fix(auth): scope model suspension reasons to prevent stale sibling re…
Aug 20, 2026
dfd71aa
fix(auth): resume model_not_supported suspensions on own model success
Aug 20, 2026
6a41515
fix(auth): ignore id-only tool-call scaffolds in empty completion check
Aug 20, 2026
6d38c5f
fix(auth): recognize action in responses tool call
Aug 20, 2026
86e53eb
fix(translator): preserve thoughtSignature in non-stream gemini to cl…
Aug 20, 2026
d8eff4e
fix(translator): support message.reasoning in responses non-stream
Aug 20, 2026
52d01e1
chore: trigger ci build
Aug 20, 2026
53f5ef5
fix(translator/antigravity): support max_completion_tokens in openai …
Aug 20, 2026
d1b4c4c
fix(claude): use canonical reasoning_content in non-stream response
Aug 20, 2026
5c3bee5
fix(translator): use deterministic tool call IDs in Gemini request tr…
Aug 20, 2026
5fb14e2
fix(translator): preserve strict thought predicate and signature binding
Aug 20, 2026
6b89f2e
fix(translator): prevent tool ID collision with explicit client IDs
Aug 20, 2026
61ef95c
fix(auth): rotate on in-stream provider errors during bootstrap
Aug 20, 2026
448bd12
fix(translator): isolate thinking blocks per signature and clear pend…
Aug 20, 2026
074465e
fix(translator): isolate consecutive stream thought signatures and pr…
Aug 20, 2026
b54a03b
fix(translator): preserve streaming signature across all response states
Aug 20, 2026
22ac811
fix(translator): preserve stream thought signatures for visible text …
Aug 20, 2026
df719cc
fix(auth): floor quota cooldown at the escalating ladder
Aug 21, 2026
571e264
fix(auth): keep failure bookkeeping when cooling is disabled
Aug 21, 2026
165e932
fix(auth): preserve zero-delay cooldown for non-quota 429 retries
Aug 21, 2026
32a6ada
style: format gemini claude response test
Aug 21, 2026
c79da2b
style: format responses response test
Aug 21, 2026
b9953ef
style: format antigravity request test
Aug 21, 2026
592ae37
style: format claude openai response test
Aug 21, 2026
5c39793
test(auth): restore logrus hooks after the capture test
Aug 21, 2026
c6ec29e
fix(auth): keep provider hint for transient 429s
Aug 21, 2026
8c07697
fix(auth): carry 429 classification through stream and token count
Aug 21, 2026
b3f4bc1
fix(auth): finalize stream bootstrap at EOF before reporting a clean …
Aug 21, 2026
d9edde2
fix(antigravity): mark short-cooldown 429s as transient rate limits
Aug 21, 2026
193a4ae
fix(claude): classify ordinary 429s as transient rate limits
Aug 21, 2026
48444c8
fix(auth): bypass the quota ladder for transient 429s without a hint
Aug 21, 2026
85206e9
fix(openai): sanitize websocket errors before request logging
Aug 21, 2026
7d7fe41
fix(auth): keep credentials available when transient cooldowns are off
Aug 21, 2026
9c1b9f2
fix(auth): restore availability fields when skipping a disabled trans…
Aug 21, 2026
6143d45
fix(antigravity): treat reasoned RATE_LIMIT_EXCEEDED without RetryInf…
Aug 21, 2026
4b73e20
test(antigravity): tolerate one stray dial per wave boundary in pooli…
Aug 21, 2026
31ae940
test(qoder): raise usage-record wait timeout to 30s for CI jitter
Aug 21, 2026
7a618ed
fix(auth): lower transient error cooldown default to 10s
warelik Aug 21, 2026
835eb05
fix(auth): rotate API-key model pools for all configured providers
warelik Aug 21, 2026
bca037a
fix(antigravity): restore inline image attachment for sibling tool re…
warelik Aug 21, 2026
ba044ea
fix(auth): allow body-only request-scoped error rules
warelik Aug 21, 2026
07edc93
docs(auth): document force-cooldown use of transientErrorCooldown
warelik Aug 21, 2026
46bbccc
test(translator): add GeminiCLI translator path tests
warelik Aug 21, 2026
e22af09
fix(sdk/auth): do not count thoughtSignature as content
warelik Aug 21, 2026
1692713
fix(translator/gemini): replay or bypass Claude thinking signatures
warelik Aug 21, 2026
d6efbc0
fix(translator/claude): preserve thinking signatures in Gemini output
warelik Aug 21, 2026
71c3933
feat(translator/gemini-cli): preserve thought signatures in Claude ou…
warelik Aug 21, 2026
4762eb9
fix(signature): sanitize preserved thinking blocks with fallback
warelik Aug 21, 2026
9a1ed57
fix(gemini-cli/gemini): preserve sibling inlineData in tool responses
warelik Aug 21, 2026
b63c24e
test(antigravity): use table fields for model calls and extra checks
warelik Aug 21, 2026
8d0bb60
fix(signature): reject foreign signatures before Claude fallback
warelik Aug 21, 2026
f8aabc1
fix(auth): handle non-OpenAI pools in Home model prediction
warelik Aug 21, 2026
fe042e0
fix(signature): keep placeholder signatures and reject unknown prefixes
warelik Aug 21, 2026
fae3762
fix(signature): validate payload behind claude# prefixes
warelik Aug 21, 2026
97b11f1
style(gemini-cli/gemini): fix misplaced doc comments
warelik Aug 21, 2026
5885fe6
fix(signature): reject residual '#' in claude-prefixed short signatures
warelik Aug 21, 2026
299699e
fix(config): preserve body-only OAuth request-scoped error rules
warelik Aug 21, 2026
ab16fa8
fix(signature): keep empty signature member on retained compat thinki…
warelik Aug 21, 2026
90d56e0
ci: retrigger after flaky executor test
warelik Aug 21, 2026
e73fcf3
fix(signature): reject unprovenanced E/R signatures in compat fallback
warelik Aug 21, 2026
b9c3a0e
feat(api): add management endpoints for oauth-request-scoped-errors
warelik Aug 21, 2026
7ca1f90
feat(diff,docs): wire oauth-request-scoped-errors diff and add exampl…
warelik Aug 21, 2026
6307bdb
fix(auth): recognize Interactions finish events in empty-completion d…
warelik Aug 21, 2026
2c03282
fix(signature): reject unprovenanced E/R signatures except short synt…
warelik Aug 21, 2026
0389870
fix(signature): make short-signature fallback a cheap short-synthetic…
warelik Aug 21, 2026
e70ec2e
test(auth): add e2e failover doctrine harness
warelik Aug 21, 2026
7d9b9d7
fix(executor): restore compat thinking cache after signature sanitiza…
warelik Aug 21, 2026
cee2b13
test(auth): correct in-stream error rotation assertion
warelik Aug 21, 2026
2bd76fa
test(auth): fix aliased model pool test scripting
warelik Aug 21, 2026
9951c30
fix(signature,executor): thread replay-cache provenance through sanit…
warelik Aug 21, 2026
e92014a
fix(auth): retain session affinity on transient errors
warelik Aug 21, 2026
0c342a8
fix(executor,signature): replay restore before MCP remap and strip cl…
warelik Aug 21, 2026
7f282be
test(auth): restore transient cooldown via t.Cleanup
warelik Aug 21, 2026
8b9d339
fix(signature): normalize whitespace-padded short synthetic signatures
warelik Aug 21, 2026
bdae62f
fix(executor): normalize cached tool-use parts before replay match
warelik Aug 21, 2026
2ea02c9
fix(executor): restore cached signatures on echoed signed thinking bl…
warelik Aug 21, 2026
62c67ac
fix(executor): credential-scoped replay fallback for sessionless Clau…
warelik Aug 21, 2026
72db541
fix(executor): conversation-scoped replay fallback for sessionless cl…
warelik Aug 21, 2026
5d070f0
fix(executor): recursively strip cache_control for non-Anthropic embe…
warelik Aug 21, 2026
e531d50
fix(executor): include caller identity in sessionless replay keys
warelik Aug 21, 2026
b4ba72f
refactor(executor): move sessionless replay key helper to helps/
warelik Aug 21, 2026
01e4f9f
fix(executor): delimit caller fields in sessionless replay key hash
warelik Aug 21, 2026
e2b4e6f
ci: trigger re-run for known flake TestAntigravityConcurrentRequestsR…
warelik Aug 21, 2026
3bb799b
fix(cloak): cache-user-id:false gives fresh random device id per request
warelik Aug 21, 2026
94ad987
fix(executor): deterministic cache-miss user_id fallback and cache-us…
warelik Aug 21, 2026
76fe7ce
fix(replay): sessionless replay fixes for tools, headers, non-tool re…
warelik Aug 21, 2026
8413f2d
merge: combine #213 cache-user-id and #214 strip-cache-control
warelik Aug 21, 2026
2bdf8f3
merge: rebase-base (#213 + #214) into #209
warelik Aug 21, 2026
cc728ae
fix(executor): chronological replay match and gate replay obfuscation…
warelik Aug 21, 2026
e0e1d51
feat(translator): propagate prompt cache hints and service tier acros…
warelik Aug 21, 2026
1e0eee2
fix(executor): retain prior signed replay turns after unsigned responses
warelik Aug 21, 2026
d6cc6f7
fix(executor): align replay matches after truncated history
warelik Aug 21, 2026
3d3a89a
fix(executor): recursively strip cache_control for non-Anthropic embe…
warelik Aug 21, 2026
e0253c0
Merge PR #188: fix(auth): scope model suspension reasons to prevent s…
warelik Aug 21, 2026
ee407d6
Merge PR #189: fix(auth): ignore id-only tool-call scaffolds in empty…
warelik Aug 21, 2026
43e5cf9
Merge PR #190: fix(translator): preserve thoughtSignature in non-stre…
warelik Aug 21, 2026
3b4859e
Merge PR #191: fix(translator): support message.reasoning fallback in…
warelik Aug 21, 2026
1e9c82f
Merge PR #192: fix(translator/antigravity): support max_completion_to…
warelik Aug 21, 2026
6cf57c1
Merge PR #193: fix(claude): use canonical reasoning_content in non-st…
warelik Aug 21, 2026
4342f57
Merge PR #194: fix(translator): use deterministic tool call IDs in Ge…
warelik Aug 21, 2026
feb0c55
Merge PR #195: fix(auth): rotate on in-stream provider errors during …
warelik Aug 21, 2026
a21f538
Merge PR #198: fix(auth): floor quota cooldown at the escalating ladder
warelik Aug 21, 2026
c75a7d6
feat(executor): carry prior reasoning into system instructions
warelik Aug 21, 2026
710eb31
Merge PR #200: fix(auth): keep failure bookkeeping when cooling is di…
warelik Aug 21, 2026
d6ddbf4
Merge PR #203: fix(openai): sanitize websocket errors before request …
warelik Aug 21, 2026
2c91375
Merge PR #205: fix(auth): lower transient error cooldown default to 10s
warelik Aug 21, 2026
601d3aa
Merge PR #206: fix(antigravity): restore inline image attachment and …
warelik Aug 21, 2026
1847f62
Merge PR #207: fix(auth): allow body-only request-scoped error rules
warelik Aug 21, 2026
faa1909
Merge PR #208: fix(auth): rotate API-key model pools for all configur…
warelik Aug 21, 2026
97486b5
Merge PR #209: fix(signature): harden cross-provider signature bounda…
warelik Aug 21, 2026
44aee54
Merge PR #210: fix(auth): recognize Interactions finish events in emp…
warelik Aug 21, 2026
54d5955
Merge PR #211: test(auth): e2e failover doctrine harness
warelik Aug 21, 2026
e449f63
Merge PR #212: fix(auth): retain session affinity on transient errors
warelik Aug 21, 2026
d7ebceb
Merge PR #214: fix(executor): recursively strip cache_control for non…
warelik Aug 21, 2026
e745e85
Merge PR #215: feat(translator): propagate prompt cache hints and ser…
warelik Aug 21, 2026
b5f6f0e
feat(auth,config): add quota-cooldown-floor-seconds and transient-coo…
warelik Aug 21, 2026
539b3f9
fix(executor,helps,cache): keep replay scope stable across history co…
warelik Aug 21, 2026
b356a16
test(degradation): add e2e doctrine harness for translator registry
warelik Aug 21, 2026
410dcb9
fix(integration): reconcile transient cooldown default and empty-comp…
warelik Aug 21, 2026
0d97093
refactor(helps): route claude carry-over through registry
warelik Aug 21, 2026
bc12f76
fix(cache,executor): persist, bound, and delimit replay aliases
warelik Aug 21, 2026
8abe165
Merge PR #209: fix(signature): harden cross-provider signature bounda…
warelik Aug 21, 2026
de336cc
Merge PR #216: feat(executor): carry prior reasoning into system inst…
warelik Aug 21, 2026
1da4181
Merge PR #217: feat(auth,config): configurable quota cooldown floor a…
warelik Aug 21, 2026
a36ec6f
Merge PR #218: test(degradation): e2e doctrine harness for translator…
warelik Aug 21, 2026
eae2248
fix(cache,executor): bound home KV aliases and preserve distinct shar…
warelik Aug 21, 2026
881b24b
merge round-24 replay alias fixes
warelik Aug 21, 2026
d23be7f
test(auth,translator): make doctrine-harness skips conditional
warelik Aug 22, 2026
b1c51b5
Merge pull request #3 from warelik/ao/airouters-19-conditional-doctri…
warelik Aug 22, 2026
f39ec16
test(auth): harden in-stream 195 doctrine skip probe
warelik Aug 22, 2026
9d1e354
fix(cache,executor): atomic alias updates, reject ties, anchor duplic…
warelik Aug 22, 2026
25725c6
fix(cache,executor): atomic alias updates, reject ties, anchor duplic…
warelik Aug 22, 2026
07cd8f6
fix(cache): credential-scoped alias cap and atomic eviction
warelik Aug 22, 2026
881525f
fix(cache): credential-scoped alias cap and atomic eviction
warelik Aug 22, 2026
b87b02e
fix(cache,executor,helps): conversation nonce for replay, rollback al…
warelik Aug 22, 2026
e8e2182
test(cache): replay-alias provenance doctrines
warelik Aug 22, 2026
9552176
fix(translator): drop invalid reasoning signatures in interactions→Re…
warelik Aug 22, 2026
5ba7df3
fix(cache): alias rollback conditional on committed value
warelik Aug 22, 2026
2a826d0
fix(executor,helps): ignore whitespace-only headers in replay session…
warelik Aug 22, 2026
ea0578b
fix(cache): recheck evicted alias value before delete
warelik Aug 22, 2026
2fa45e3
fix(cache): roll back alias value on failed registration
warelik Aug 22, 2026
e9b0391
fix(executor,helps): session-scope replay fixes for nonce and no-nonc…
warelik Aug 22, 2026
6f014b2
fix(executor): cache signed turns from cross-format Claude streams
warelik Aug 22, 2026
e62cfca
fix(executor): refuse partial duplicate-turn anchors
warelik Aug 22, 2026
03abf0a
fix(executor): accumulate citation deltas in cross-format replay
warelik Aug 22, 2026
2f44448
fix(executor): register replay aliases only for content-derived scopes
warelik Aug 22, 2026
1adc471
fix(cache): enforce alias byte limit, LRU per-key eviction, and throt…
warelik Aug 22, 2026
e75db9a
fix(executor): accumulate restored MCP tool names in cross-format rep…
warelik Aug 22, 2026
84eb982
fix(cache): account alias key bytes once per key
warelik Aug 22, 2026
1ba3af4
fix(executor): anchor replay to cached suffix and refuse fallback on …
warelik Aug 22, 2026
40a628c
fix(cache): evict aliases by identity, not stale slice index
warelik Aug 22, 2026
4e50b84
Merge PR #223: test(cache): replay-alias provenance doctrines
warelik Aug 22, 2026
9e13414
Merge PR #225: fix(translator): drop invalid reasoning signatures in …
warelik Aug 22, 2026
00af183
fix(executor): mirror #5150 replay suffix and stream accumulation fixes
warelik Aug 22, 2026
c6ea314
Merge branch 'ao/airouters-11-fix-signature-boundary-plus' of https:/…
warelik Aug 22, 2026
c74f86a
fix(cache,executor): avoid reserving Home KV tombstones for no-nonce …
warelik Aug 22, 2026
d427f05
refactor(executor,helps): move claude replay helpers to helps
warelik Aug 22, 2026
fe48f9a
fix(executor,helps): clear SSE replay state and normalize string shor…
warelik Aug 22, 2026
c7bfea8
fix(executor): avoid reserving replay state for all caller-controlled…
warelik Aug 22, 2026
c2f4d46
fix(helps): carry matched request offset into replay restoration
warelik Aug 22, 2026
fb48ab8
fix(helps): match cached turns across unsigned assistant gaps
warelik Aug 22, 2026
f9aa2b4
fix(executor): bound per-request replay alias registration
warelik Aug 22, 2026
e660e45
fix(helps): read duplicate case-insensitive headers deterministically
warelik Aug 22, 2026
d3bd112
fix(helps): reject duplicate request-side replay anchors
warelik Aug 22, 2026
bdd0902
fix(auth): CAS-protect cold session-affinity binding
warelik Aug 22, 2026
c9cce71
fix(auth,helps): honor occupied aliases and reject multi-turn duplica…
warelik Aug 22, 2026
2547bb2
fix(auth,executor): attach free aliases to winner and reject duplicat…
warelik Aug 22, 2026
4de4af7
fix(auth): atomic SetAliasesIfNoConflict for cold cache-miss binding
warelik Aug 22, 2026
e2d32fe
fix(cache): atomic CAS for replay snapshots, alias eviction, and roll…
warelik Aug 22, 2026
2ef90a1
fix(cache): mark absent local IfExists replay snapshots as loaded
warelik Aug 22, 2026
1100eca
fix(executor): cap replay aliases before resolving Home replay scopes
warelik Aug 22, 2026
1f9bee5
fix(helps): reject ambiguous full cached suffix matches
warelik Aug 22, 2026
2f4d776
fix(helps): reject per-turn duplicate replay candidates
warelik Aug 22, 2026
74649aa
fix(executor): register response aliases only after successful cache …
warelik Aug 22, 2026
ff7fb25
fix(cache): rollback unindexed alias to prior value
warelik Aug 22, 2026
744721d
fix(helps): reject multiple retained replay candidates and normalize …
warelik Aug 22, 2026
1cebb90
fix(executor/auth): reject multiple retained Kimi replay candidates a…
warelik Aug 22, 2026
0bb7dda
fix(cache): expire evicted Claude alias tombstones with a short TTL
warelik Aug 22, 2026
16fb83b
fix(auth): conditional unavailable-auth rebind to prevent concurrent …
warelik Aug 22, 2026
dd8fda1
fix(auth/replay): merge same-auth groups on rebind and require visibl…
warelik Aug 22, 2026
bda3d43
fix(auth): preserve requested aliases when compacting during conditio…
warelik Aug 22, 2026
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
2 changes: 2 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,8 @@ func main() {
redisqueue.SetRetentionSeconds(cfg.RedisUsageQueueRetentionSeconds)
coreauth.SetQuotaCooldownDisabled(cfg.DisableCooling)
coreauth.SetTransientErrorCooldownSeconds(cfg.TransientErrorCooldownSeconds)
coreauth.SetQuotaCooldownFloorSeconds(cfg.QuotaCooldownFloorSeconds)
coreauth.SetTransientCooldownByStatus(cfg.TransientCooldownByStatus)

if err = logging.ConfigureLogOutput(cfg); err != nil {
log.Errorf("failed to configure log output: %v", err)
Expand Down
60 changes: 59 additions & 1 deletion config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,23 @@ save-cooldown-status: false

# Cooldown duration in seconds for transient upstream errors (408/500/502/503/504).
# Set to 0 to keep the legacy 60-second cooldown; set to -1 to disable transient error cooldowns.
# After PR #205 (fix/auth-lower-transient-cooldown), 0 means 10 s.
transient-error-cooldown-seconds: 0

# Per-status overrides for transient error cooldowns.
# Statuses not listed fall back to transient-error-cooldown-seconds.
# Example:
# transient-cooldown-by-status:
# - status: 408
# cooldown-seconds: 2
# - status: 503
# cooldown-seconds: 10

# Minimum base in seconds for the quota cooldown ladder.
# Sub-second Retry-After hints are never allowed below this floor. Default 1.
# Stacks on PR #198 (fix/quota-backoff-hint-floor) / router-for-me/CLIProxyAPI#5130.
quota-cooldown-floor-seconds: 1

# When true, globally disable Claude request cloaking (the Claude Code CLI disguise and
# system prompt replacement), so the original system prompt is passed through to Claude as-is.
# Individual credentials can still override this: a claude-api-key entry via its "cloak.mode",
Expand Down Expand Up @@ -306,7 +321,7 @@ nonstream-keepalive-interval: 0
# disable-cooling: false # optional override: true disables cooling, false enables it; omit to inherit global
# request-retry: 3 # optional: per-auth override of the global request-retry; 0 disables retries; omit or set < 0 to use the global value
# request-scoped-errors: # optional: custom rules to classify upstream errors by status and body patterns
# - status: 400 # HTTP status code to match
# - status: 400 # HTTP status code to match; omit or set to 0 to match any status (body-only rule)
# match: # optional: string contains matching
# - "maximum_context_length"
# - "context_length_exceeded"
Expand Down Expand Up @@ -654,6 +669,49 @@ nonstream-keepalive-interval: 0
# - "kilo-claude-opus-4-6" # exclude specific models (exact match)
# - "*:free" # wildcard matching suffix (e.g. all free models)

# OAuth provider request-scoped error rules (custom error classification for OAuth credentials)
# Supported actions: "stop", "stop-and-cooldown", "continue", "continue-and-cooldown"
# The status field is optional; omit it or set it to 0 to match any HTTP status (body-only rule).
# oauth-request-scoped-errors:
# vertex:
# - status: 400
# match:
# - "maximum_context_length"
# - "context_length_exceeded"
# match-regexr:
# - "maximum_context_length$"
# - "^context_length_exceeded"
# action: "stop"
# aistudio:
# - status: 400
# match:
# - "invalid_argument"
# action: "stop"
# antigravity:
# - status: 500
# match:
# - "internal_server_error"
# action: "stop-and-cooldown"
# claude:
# - match:
# - "prompt is too long"
# action: "stop"
# codex:
# - status: 400
# match:
# - "context_window_exceeded"
# action: "stop"
# kimi:
# - status: 400
# match:
# - "length_limit"
# action: "stop"
# xai:
# - status: 400
# match:
# - "max_tokens_exceeded"
# action: "stop"

# OpenAI compatibility providers
# openai-compatibility:
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
Expand Down
119 changes: 119 additions & 0 deletions internal/api/handlers/management/config_lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,103 @@ func (h *Handler) DeleteOAuthModelAlias(c *gin.Context) {
h.persist(c)
}

// oauth-request-scoped-errors: map[string][]RequestScopedErrorRule
func (h *Handler) GetOAuthRequestScopedErrors(c *gin.Context) {
c.JSON(200, gin.H{"oauth-request-scoped-errors": sanitizedOAuthRequestScopedErrors(h.cfg.OAuthRequestScopedErrors)})
}

func (h *Handler) PutOAuthRequestScopedErrors(c *gin.Context) {
data, err := c.GetRawData()
if err != nil {
c.JSON(400, gin.H{"error": "failed to read body"})
return
}
var entries map[string][]config.RequestScopedErrorRule
if err = json.Unmarshal(data, &entries); err != nil {
var wrapper struct {
Items map[string][]config.RequestScopedErrorRule `json:"items"`
}
if err2 := json.Unmarshal(data, &wrapper); err2 != nil {
c.JSON(400, gin.H{"error": "invalid body"})
return
}
entries = wrapper.Items
}
h.cfg.OAuthRequestScopedErrors = sanitizedOAuthRequestScopedErrors(entries)
h.persist(c)
}

func (h *Handler) PatchOAuthRequestScopedErrors(c *gin.Context) {
var body struct {
Provider *string `json:"provider"`
Channel *string `json:"channel"`
Rules []config.RequestScopedErrorRule `json:"rules"`
}
if errBindJSON := c.ShouldBindJSON(&body); errBindJSON != nil {
c.JSON(400, gin.H{"error": "invalid body"})
return
}
channelRaw := ""
if body.Channel != nil {
channelRaw = *body.Channel
} else if body.Provider != nil {
channelRaw = *body.Provider
}
channel := strings.ToLower(strings.TrimSpace(channelRaw))
if channel == "" {
c.JSON(400, gin.H{"error": "invalid channel"})
return
}

normalizedMap := sanitizedOAuthRequestScopedErrors(map[string][]config.RequestScopedErrorRule{channel: body.Rules})
normalized := normalizedMap[channel]
if len(normalized) == 0 {
if h.cfg.OAuthRequestScopedErrors == nil {
c.JSON(404, gin.H{"error": "channel not found"})
return
}
if _, ok := h.cfg.OAuthRequestScopedErrors[channel]; !ok {
c.JSON(404, gin.H{"error": "channel not found"})
return
}
delete(h.cfg.OAuthRequestScopedErrors, channel)
if len(h.cfg.OAuthRequestScopedErrors) == 0 {
h.cfg.OAuthRequestScopedErrors = nil
}
h.persist(c)
return
}
if h.cfg.OAuthRequestScopedErrors == nil {
h.cfg.OAuthRequestScopedErrors = make(map[string][]config.RequestScopedErrorRule)
}
h.cfg.OAuthRequestScopedErrors[channel] = normalized
h.persist(c)
}

func (h *Handler) DeleteOAuthRequestScopedErrors(c *gin.Context) {
channel := strings.ToLower(strings.TrimSpace(c.Query("channel")))
if channel == "" {
channel = strings.ToLower(strings.TrimSpace(c.Query("provider")))
}
if channel == "" {
c.JSON(400, gin.H{"error": "missing channel"})
return
}
if h.cfg.OAuthRequestScopedErrors == nil {
c.JSON(404, gin.H{"error": "channel not found"})
return
}
if _, ok := h.cfg.OAuthRequestScopedErrors[channel]; !ok {
c.JSON(404, gin.H{"error": "channel not found"})
return
}
delete(h.cfg.OAuthRequestScopedErrors, channel)
if len(h.cfg.OAuthRequestScopedErrors) == 0 {
h.cfg.OAuthRequestScopedErrors = nil
}
h.persist(c)
}

// codex-api-key: []CodexKey
func (h *Handler) GetCodexKeys(c *gin.Context) {
c.JSON(200, gin.H{"codex-api-key": h.codexKeysWithAuthIndex()})
Expand Down Expand Up @@ -2090,3 +2187,25 @@ func normalizeAPIKeysList(keys []string) []string {
}
return out
}

func sanitizedOAuthRequestScopedErrors(entries map[string][]config.RequestScopedErrorRule) map[string][]config.RequestScopedErrorRule {
if len(entries) == 0 {
return nil
}
copied := make(map[string][]config.RequestScopedErrorRule, len(entries))
for channel, rules := range entries {
if len(rules) == 0 {
continue
}
copied[channel] = append([]config.RequestScopedErrorRule(nil), rules...)
}
if len(copied) == 0 {
return nil
}
cfg := config.Config{OAuthRequestScopedErrors: copied}
cfg.SanitizeOAuthRequestScopedErrors()
if len(cfg.OAuthRequestScopedErrors) == 0 {
return nil
}
return cfg.OAuthRequestScopedErrors
}
2 changes: 2 additions & 0 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
managementasset.SetCurrentConfig(cfg)
auth.SetQuotaCooldownDisabled(cfg.DisableCooling)
auth.SetTransientErrorCooldownSeconds(cfg.TransientErrorCooldownSeconds)
auth.SetQuotaCooldownFloorSeconds(cfg.QuotaCooldownFloorSeconds)
auth.SetTransientCooldownByStatus(cfg.TransientCooldownByStatus)
applySignatureCacheConfig(nil, cfg)
// Initialize management handler
s.mgmt = managementHandlers.NewHandler(cfg, configFilePath, authManager)
Expand Down
5 changes: 5 additions & 0 deletions internal/api/server_management.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ func (s *Server) registerManagementRoutes() {
mgmt.PATCH("/oauth-model-alias", s.mgmt.PatchOAuthModelAlias)
mgmt.DELETE("/oauth-model-alias", s.mgmt.DeleteOAuthModelAlias)

mgmt.GET("/oauth-request-scoped-errors", s.mgmt.GetOAuthRequestScopedErrors)
mgmt.PUT("/oauth-request-scoped-errors", s.mgmt.PutOAuthRequestScopedErrors)
mgmt.PATCH("/oauth-request-scoped-errors", s.mgmt.PatchOAuthRequestScopedErrors)
mgmt.DELETE("/oauth-request-scoped-errors", s.mgmt.DeleteOAuthRequestScopedErrors)

mgmt.GET("/auth-files", s.mgmt.ListAuthFiles)
mgmt.GET("/auth-files/models", s.mgmt.GetAuthFileModels)
mgmt.GET("/model-definitions/:channel", s.mgmt.GetStaticModelDefinitions)
Expand Down
22 changes: 22 additions & 0 deletions internal/api/server_reload.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ import (
"gopkg.in/yaml.v3"
)

func transientCooldownByStatusEqual(a, b []config.TransientCooldownByStatusRule) bool {
if len(a) != len(b) {
return false
}
m := make(map[int]int, len(a))
for _, r := range a {
m[r.Status] = r.CooldownSeconds
}
for _, r := range b {
if m[r.Status] != r.CooldownSeconds {
return false
}
}
return true
}

func (s *Server) applyAccessConfig(oldCfg, newCfg *config.Config) bool {
if s == nil || s.accessManager == nil || newCfg == nil {
return false
Expand Down Expand Up @@ -111,6 +127,12 @@ func (s *Server) UpdateClientsContext(ctx context.Context, cfg *config.Config) b
if oldCfg == nil || oldCfg.TransientErrorCooldownSeconds != cfg.TransientErrorCooldownSeconds {
auth.SetTransientErrorCooldownSeconds(cfg.TransientErrorCooldownSeconds)
}
if oldCfg == nil || oldCfg.QuotaCooldownFloorSeconds != cfg.QuotaCooldownFloorSeconds {
auth.SetQuotaCooldownFloorSeconds(cfg.QuotaCooldownFloorSeconds)
}
if oldCfg == nil || !transientCooldownByStatusEqual(oldCfg.TransientCooldownByStatus, cfg.TransientCooldownByStatus) {
auth.SetTransientCooldownByStatus(cfg.TransientCooldownByStatus)
}

if oldCfg != nil && oldCfg.DisableImageGeneration != cfg.DisableImageGeneration {
log.Infof("disable-image-generation updated: %v -> %v", oldCfg.DisableImageGeneration, cfg.DisableImageGeneration)
Expand Down
Loading
Loading