- Hapus src/lib/ipv6-pool.ts dan test
- Hapus fetchViaCurl() dari fetch-utils.ts
- Hapus parameter ipv6Source dari router, ai-proxy, anthropic-proxy
- Hapus ipv6Only: false dari Bun.serve(), default bind ke 0.0.0.0
- Hapus dokumentasi IPv6 dari CLAUDE.md dan README.md
- SSRF protection untuk IPv6 private/loopback tetap ada
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix streaming timeout: use AbortController for connection-only timeout
instead of AbortSignal.timeout() that kills active SSE streams
- Fix fetchViaCurl: stream body via ReadableStream instead of buffering
entire response in memory
- Fix JWT/aichat race condition: add Promise dedup to prevent concurrent
bootstrap calls (10 requests = 1 bootstrap, not 10)
- Fix ACTIVE_READERS memory leak: auto-remove readers on stream completion
- Fix WebSocket backpressure: log warning when client buffer exceeds 1MB
- Add SSE heartbeat/keepalive: send ': keepalive' every 15s to prevent
LB/proxy timeout during AI thinking
- Fix SSELineBuffer: graceful overflow handling (warn + discard instead
of throwing error that crashes stream)
- Fix transformStream tight loop: yield to event loop after each chunk
to prevent starvation
- Fix fetchViaCurl process cleanup: use SIGKILL + proper timeout cleanup
- Add retry on 502/504: retry transient server errors before returning
to caller (both fetchWithRetry and fetchWithSessionRetry)
Ubah strategi retry di fetchWithRetry dan fetchWithSessionRetry:
- Langsung (direct) sebagai percobaan pertama sebelum proxy pool
- Proxy sebagai fallback jika direct gagal
Co-Authored-By: Claude <noreply@anthropic.com>
ProxyPool:
- Add cooldowns map (host:port::model -> expiry) and cooldownDuration (60s default)
- Add markRateLimited(model) — puts current proxy in cooldown for a model
- Add isProxyInCooldown, isIndexInCooldown, isCurrentInCooldown checks
- Modify rotate(model?) — skip proxies in cooldown for the given model
- Add setCooldownDuration(ms) for configuration
SessionProxyPool:
- Add markRateLimited(sessionId, model) — delegates to underlying pool
- Modify rotateNow(sessionId, model?) — skip cooldown proxies
- Modify acquire(sessionId, model?) — skip cooldown when picking least-loaded
- Modify pickLeastUsedIndex(model?) — skip cooldown proxies in scan
fetch-utils.ts:
- Add extractModel helper — extracts model from context string
- On HTTP 429: call markRateLimited(model) before markFailed
- Pass model to rotate() / rotateNow() / acquire() throughout
Co-Authored-By: Claude <noreply@anthropic.com>
fetchWithRetry:
- Dynamically calculate maxAttempts = pool.size + 1 (direct fallback)
- Try every proxy in pool via rotation, then direct as last resort
- Only classify as error when even direct produces no response
fetchWithSessionRetry:
- Default maxRetries = sessionPool.size + 1 instead of hardcoded 3
- Try pool.size proxy attempts (each on a different proxy via rotateNow),
then 1 direct attempt (no proxy) before giving up
- Only classify as error if direct also returns no response
ProxyPool.rotate():
- Skip proxies that have exceeded the failure threshold (isFailed)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add rotateNow() to SessionProxyPool — force-rotate session to a
different proxy immediately (excludes current index to ensure real
rotation). Uses round-robin scan from oldIndex+1 so all proxies
get used, not just bouncing between two.
- fetchWithSessionRetry: call rotateNow() on every failure instead of
markFailed() which only rotated after threshold. Return last HTTP
response (e.g. 429) instead of classifying as 502 when we have one.
- fetchWithRetry: rotate pool on every failure for consistency.
Co-Authored-By: Claude <noreply@anthropic.com>
- fetch-utils.ts: logProxy() — logs every attempt, proxy used, success/failure
with sessionId prefix for traceability
- proxy-pool.ts: logPool() — logs acquire/release/rotate/markFailed/markSuccess
with active session count and proxy host info
- Both use consistent [prefix] HH:MM:SS.mmm key=value format
Co-Authored-By: Claude <noreply@anthropic.com>
- fetchWithRetry: use proxy on every attempt (not just fallback after direct)
- fetchWithSessionRetry: call acquire() on attempt 0 so new sessions get
a least-loaded proxy assigned, enabling IP spread across concurrent users
- Update docstrings to reflect new strategy
Co-Authored-By: Claude <noreply@anthropic.com>
Add SessionProxyPool for per-session sticky proxy allocation with
load-balanced least-used selection and auto-rotation on failure.
Introduce fetchWithSessionRetry for transparent retry with proxy
rotation. Wire into AI proxy handlers (OpenAI + Anthropic) with
stream lifecycle cleanup.
Co-Authored-By: Claude <noreply@anthropic.com>
Introduce a centralized `fetch-utils.ts` to handle retry logic with proxy fallback, SSE line buffering to prevent chunk-boundary corruption, and graceful shutdown via active reader tracking.
Key changes:
- Add `fetchWithRetry` for automatic direct-to-proxy failover.
- Implement `SSELineBuffer` to ensure reliable parsing of split SSE chunks.
- Add `createStreamBodyLimiter` to enforce payload limits on streaming requests.
- Refactor `ProxyPool` to decouple failure marking from rotation.
- Standardize CORS handling and environment variable configuration.
- Clean up documentation and remove obsolete skill files.