Commit Graph

39 Commits

Author SHA1 Message Date
世界
cd5007ffbb fix(ccm,ocm): track external credential poll failures and re-poll on user connect
External credentials now properly increment consecutivePollFailures on
poll errors (matching defaultCredential behavior), marking the credential
as temporarily blocked. When a user with external_credential connects
and the credential is not usable, a forced poll is triggered to check
recovery.
2026-03-26 22:16:02 +08:00
世界
1774d98793 fix(ccm,ocm): restore fixed usage polling
Remove the poll_interval config surface from CCM and OCM so both services fall back to the built-in 1h polling cadence again. Also isolate CCM credential lock mocking per test instance so the access-token refresh tests stop racing on shared global state.
2026-03-26 14:01:24 +08:00
世界
4592164a7a Align CCM and OCM rate limits 2026-03-24 22:06:10 +08:00
世界
99d9e06dd0 fix(ccm,ocm): handle upstream 400 by marking external credentials rejected and polling default credentials
External credentials returning 400 are marked unavailable for pollInterval
duration; status stream/poll success clears the rejection early. Default
credentials trigger a stale poll to let the usage API detect account issues
without causing 429 storms.
2026-03-21 10:31:17 +08:00
世界
2fe1e37b17 fix(ccm,ocm): add missing isFirstUpdate to external credential usage logging 2026-03-18 01:00:55 +08:00
世界
3bcfdd5455 fix(ccm,ocm): remove external context from pollUsage/pollIfStale
pollUsage(ctx) accepted caller context, and service_status.go passed
r.Context() which gets canceled on client disconnect or service shutdown.
This caused incrementPollFailures → interruptConnections on transient
cancellations. Each implementation now uses its own persistent context:
defaultCredential uses serviceContext, externalCredential uses
getReverseContext().
2026-03-18 00:54:01 +08:00
世界
b119d08764 fix(ccm,ocm): add usage logging to status stream, remove redundant isFirstUpdate
connectStatusStream updated credential state silently — no log on
first frame or value changes. After restart, external credentials
get usage via stream before any request, so pollIfStale skips them
and no usage log ever appears.

Add the same change-detection log to connectStatusStream. Also remove
redundant isFirstUpdate guards from pollUsage and updateStateFromHeaders:
when old values are zero, any non-zero new value already satisfies the
integer-percent comparison.
2026-03-17 22:37:38 +08:00
世界
6b8838d323 fix(ccm,ocm): restart status stream when receiver gets reverse session
statusStreamLoop started on start() before any reverse session existed,
got a non-retryable error, and exited permanently. Restart it when
setReverseSession transitions receiver credentials to available.
2026-03-17 22:08:30 +08:00
世界
99e19e7033 service: stop retrying fatal watch status errors 2026-03-17 20:47:52 +08:00
世界
969defeef0 ccm,ocm: validate external status response fields 2026-03-17 20:17:56 +08:00
世界
f57eff33bb ccm,ocm: fix WS push lifecycle, deduplicate rate_limits, stabilize reset aggregation
- Add closed channel to webSocketSession for push goroutine shutdown
  on connection close, preventing session leak and Service.Close() hang
- Intercept upstream codex.rate_limits events instead of forwarding;
  push goroutine is now the sole sender of aggregated rate_limits
- Emit status updates on reset-only changes (fiveHourResetChanged,
  weeklyResetChanged) so push goroutine picks up reset advances
- Skip expired resets (hours <= 0) in aggregation instead of clamping
  to now, avoiding unstable reset_at output and spurious status ticks
- Delete stale upstream reset headers when aggregated reset is zero
- Hardcode "codex" identifier everywhere: handleWebSocketRateLimitsEvent,
  buildSyntheticRateLimitsEvent, rewriteResponseHeaders
- Remove rewriteWebSocketRateLimits, rewriteWebSocketRateLimitWindow,
  identifier tracking (TypedValue), and unused imports
2026-03-17 20:00:54 +08:00
世界
0a054b9aa4 ccm,ocm: propagate reset times, rewrite headers for all users, add WS status push
- Add fiveHourReset/weeklyReset to statusPayload and aggregatedStatus
  with weight-averaged reset time aggregation across credential pools
- Rewrite response headers (utilization + reset times) for all users,
  not just external credential users
- Rewrite WebSocket rate_limits events for all users with aggregated values
- Add proactive WebSocket status push: synthetic codex.rate_limits events
  sent on connection start and on status changes via statusObserver
- Remove one-shot stream forward compatibility (statusStreamHeader,
  restoreLastUpdatedIfUnchanged, oneShot detection)
2026-03-17 18:13:54 +08:00
世界
7d15d9d282 ccm: emit status updates for plan-weight-only changes 2026-03-17 16:46:54 +08:00
世界
4a6a211775 ccm,ocm: reduce status emission noise, simplify emit-guard pattern
Guard updateStateFromHeaders emission with value-change detection to
avoid unnecessary computeAggregatedUtilization scans on every proxied
response. Replace statusAggregateStateLocked two-value return with
comparable statusSnapshot struct. Define statusPayload type for the
status wire format, replacing anonymous structs and map literals.
2026-03-17 16:10:59 +08:00
世界
f84832a369 Add stream watch endpoint 2026-03-17 16:03:35 +08:00
世界
14ade76956 ccm,ocm: remove dead code, fix timer leaks, eliminate redundant lookups
- Remove unused onBecameUnusable field from CCM credential structs
  (OCM wires it for WebSocket interruption; CCM has no equivalent)
- Replace time.After with time.NewTimer in doHTTPWithRetry and
  connectorLoop to avoid timer leaks on context cancellation
- Pass already-resolved provider to rewriteResponseHeadersForExternalUser
  instead of re-resolving via credentialForUser
- Hoist reverseYamuxConfig to package-level var (immutable, no need to
  allocate on every call)
2026-03-15 20:42:41 +08:00
世界
656b09d1be ccm,ocm: never treat external usage endpoint failures as over-limit 2026-03-15 18:48:53 +08:00
世界
bc6e72408d ccm,ocm: block API key headers from being forwarded upstream 2026-03-15 18:48:52 +08:00
世界
6878ad0d35 ccm,ocm: fix naming and error-handling convention violations
- Rename credential interface to Credential (exported), cred to credential
- Rename mutex/saveMutex to access/saveAccess per go-syntax.md
- Fix abbreviations: reverseHttpClient, allCreds, credOpt, extCred,
  credDialer, reverseCredDialer, portStr
- Replace errors.Is(http.ErrServerClosed) with E.IsClosed
- Add E.IsClosedOrCanceled guard before streaming write error logs
2026-03-15 18:48:51 +08:00
世界
04bd63b455 ccm,ocm: reorganize files and improve naming conventions
Split credential_state.go (1500+ lines) into credential.go,
credential_default.go, credential_provider.go, credential_builder.go.

Split service.go (900+ lines) into service.go, service_handler.go,
service_status.go.

Rename credential.go to credential_oauth.go to avoid name conflict
with the credential interface.

Apply naming fixes: accessMutex→access, stateMutex→stateAccess,
sessionMutex→sessionAccess, webSocketMutex→webSocketAccess,
httpTransport()→httpClient(), httpClient field→forwardHTTPClient,
weeklyWindowDuration→weeklyWindowHours.
2026-03-15 18:48:51 +08:00
世界
51d564c9ff ccm,ocm: merge fallback into balancer strategy, use hyphenated constant names
Merge the fallback credential type into balancer as a strategy
(C.BalancerStrategyFallback). Replace raw string literals with
C.BalancerStrategyXxx constants and switch to hyphens (least-used,
round-robin) per project convention.
2026-03-15 18:48:50 +08:00
世界
4d8baf7175 ccm: fix nil pointer in pollUsage for connector-mode credentials
Connector-mode credentials (URL + reverse: true) never assigned
httpClient, causing a nil dereference when pollUsage accessed
httpClient.Transport.

Also extract poll request logic into doPollUsageRequest to try
reverse transport first (single attempt), then fall back to
forward transport with retries if the reverse session disconnects.
2026-03-15 18:48:50 +08:00
世界
d1e5426bc8 ccm,ocm: add exponential backoff with cap for poll retry
Replace flat 1-minute poll retry interval with exponential backoff
(1m → 2m → 4m → 5m cap). Suppress error logs after reaching the cap.
2026-03-15 18:48:50 +08:00
世界
4d907bc49d ccm,ocm: allow URL-based credentials to accept reverse connections
Previously, findReceiverCredential required baseURL == reverseProxyBaseURL,
so only credentials with no URL could accept incoming reverse connections.
Now credentials with a normal URL also accept reverse connections, preferring
the reverse session when active and falling back to the direct URL when not.
2026-03-15 18:48:49 +08:00
世界
f871113832 ccm,ocm: add balancer session rebalancing with per-credential interrupt
When a sticky session's credential utilization exceeds the least-used
credential by a weight-adjusted threshold, force reassign all sessions
on that credential and cancel in-flight requests scoped to the balancer.

Threshold formula: effective = rebalance_threshold / planWeight, so a
config value of 20 triggers at 2% delta for Max 20x (w=10), 4% for
Max 5x (w=5), and 20% for Pro (w=1).
2026-03-15 18:48:49 +08:00
世界
6f433937ba ccm,ocm: auto-detect plan weight for external credentials via status endpoint 2026-03-15 18:48:47 +08:00
世界
8984b45ded ccm,ocm: improve balancer least_used with plan-weighted scoring and reset urgency
Scale remaining capacity by plan weight (Pro=1, Max 5x=5, Max 20x=10
for CCM; Plus=1, Pro=10 for OCM) so higher-tier accounts contribute
proportionally more. Factor in weekly reset proximity so credentials
about to reset are preferred ("use it or lose it").

Auto-detect plan weight from subscriptionType + rateLimitTier (CCM)
or plan_type (OCM). Fetch /api/oauth/profile when rateLimitTier is
missing from the credential file. External credentials accept a
manual plan_weight option.
2026-03-15 18:48:46 +08:00
世界
1628272507 ccm,ocm: mark credentials unusable on usage poll failure and trigger poll on upstream error 2026-03-15 18:48:46 +08:00
世界
7f93c76b1a ccm,ocm: add limit options and fix aggregated utilization scaling
Add limit_5h and limit_weekly options as alternatives to reserve_5h
and reserve_weekly for capping credential utilization. The two are
mutually exclusive per window.

Fix computeAggregatedUtilization to scale per-credential utilization
relative to each credential's cap before averaging, so external users
see correct available capacity regardless of per-credential caps.

Fix pickLeastUsed to compare remaining capacity (cap - utilization)
instead of raw utilization, ensuring fair comparison across credentials
with different caps.
2026-03-15 18:48:44 +08:00
世界
22376472d0 ccm,ocm: fix passive usage update for WebSocket connections
WebSocket 101 upgrade responses do not include utilization headers
(confirmed via codex CLI source). Rate limit data is delivered
exclusively through in-band events (codex.rate_limits and error
events with status 429).

Previously, updateStateFromHeaders unconditionally bumped lastUpdated
even when no utilization headers were found, which suppressed polling
and left credential utilization permanently stale during WebSocket
sessions.

- Only bump lastUpdated when actual utilization data is parsed
- Parse in-band codex.rate_limits events to update credential state
- Detect in-band 429 error events to markRateLimited
- Fix WebSocket 429 retry to update old credential state before retry
2026-03-15 18:48:43 +08:00
世界
74bf20d349 ccm,ocm: fix reverse session shutdown race 2026-03-15 18:48:43 +08:00
世界
ff8585f7c6 ccm,ocm: block utilization decrease within same rate-limit window
updateStateFromHeaders unconditionally applied header utilization
values even when they were lower than the current state, causing
poll-sourced values to be overwritten by stale header values.

Parse reset timestamps before utilization and only allow decreases
when the reset timestamp changes (indicating a new rate-limit
window). Also add math.Ceil to CCM external credential for
consistency with default credential.
2026-03-15 18:48:42 +08:00
世界
3b177df05e ccm,ocm: fix data race on reverseContext/reverseCancel
InterfaceUpdated() writes reverseContext and reverseCancel without
synchronization while connectorLoop/connectorConnect goroutines
read them concurrently. close() also accesses reverseCancel without
a lock.

Fix by extending reverseAccess mutex to protect these fields:
- Add getReverseContext()/resetReverseContext() methods
- Pass context as parameter to connectorConnect
- Merge close() into a single lock acquisition
- Use resetReverseContext() in InterfaceUpdated()
2026-03-15 18:48:42 +08:00
世界
02a1409e9a ccm,ocm: unify HTTP request retry with fast retry and exponential backoff 2026-03-15 18:48:41 +08:00
世界
af94ea9089 Fix reverse external credential handling 2026-03-15 18:48:41 +08:00
世界
970951f369 ccm,ocm: add reverse proxy support for external credentials
Allow two CCM/OCM instances to share credentials when only one has a
public IP, using yamux-multiplexed reverse connections.

Three credential modes:
- Normal: URL set, reverse=false — standard HTTP proxy
- Receiver: URL empty — waits for incoming reverse connection
- Connector: URL set, reverse=true — dials out to establish connection

Extend InterfaceUpdated to services so network changes trigger
reverse connection reconnection.
2026-03-15 18:48:40 +08:00
世界
15f3619995 ccm,ocm: strip reverse proxy headers before forwarding to upstream 2026-03-15 18:48:40 +08:00
世界
8e5811a8c7 ccm,ocm: watch credential_path and allow delayed credentials 2026-03-15 18:48:40 +08:00
世界
da8ff6f578 ccm/ocm: Add external credential support for cross-instance usage sharing
Extract credential interface from *defaultCredential to support both
default (OAuth) and external (remote proxy) credential types. External
credentials proxy requests to a remote ccm/ocm instance with bearer
token auth, poll a /status endpoint for utilization, and parse
aggregated rate limit headers from responses.

Add allow_external_usage user flag to control whether balancer/fallback
providers may select external credentials. Add status endpoint
(/ccm/v1/status, /ocm/v1/status) returning averaged utilization across
eligible credentials. Rewrite response rate limit headers for external
users with aggregated values.
2026-03-15 18:48:39 +08:00